From 9835e800ec1bba39ba7d14fb6809c46d6ec398fa Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Sun, 16 Feb 2020 18:28:03 -0500
Subject: [PATCH] Fixed: individual audio generators now either are or are not
 stereo. The speaker acts accordingly.

---
 Components/6560/6560.hpp                      |  3 +-
 Components/AY38910/AY38910.cpp                | 65 ++++++++-----------
 Components/AY38910/AY38910.hpp                | 10 +--
 Machines/Apple/Macintosh/Audio.hpp            |  1 +
 Machines/Apple/Macintosh/DeferredAudio.hpp    |  2 +-
 Machines/Oric/Oric.cpp                        |  9 +--
 Machines/ZX8081/ZX8081.cpp                    |  5 +-
 .../Speaker/Implementation/LowpassSpeaker.hpp | 26 ++++----
 8 files changed, 58 insertions(+), 63 deletions(-)

diff --git a/Components/6560/6560.hpp b/Components/6560/6560.hpp
index f7dc74d4b..05e1fb2e0 100644
--- a/Components/6560/6560.hpp
+++ b/Components/6560/6560.hpp
@@ -30,6 +30,7 @@ class AudioGenerator: public ::Outputs::Speaker::SampleSource {
 		void get_samples(std::size_t number_of_samples, int16_t *target);
 		void skip_samples(std::size_t number_of_samples);
 		void set_sample_volume_range(std::int16_t range);
+		static constexpr bool get_is_stereo() { return false; }
 
 	private:
 		Concurrency::DeferringAsyncTaskQueue &audio_queue_;
@@ -433,7 +434,7 @@ template <class BusHandler> class MOS6560 {
 
 		Concurrency::DeferringAsyncTaskQueue audio_queue_;
 		AudioGenerator audio_generator_;
-		Outputs::Speaker::LowpassSpeaker<AudioGenerator, false> speaker_;
+		Outputs::Speaker::LowpassSpeaker<AudioGenerator> speaker_;
 
 		Cycles cycles_since_speaker_update_;
 		void update_audio() {
diff --git a/Components/AY38910/AY38910.cpp b/Components/AY38910/AY38910.cpp
index e7b52a4cf..28292bcc8 100644
--- a/Components/AY38910/AY38910.cpp
+++ b/Components/AY38910/AY38910.cpp
@@ -6,13 +6,17 @@
 //  Copyright 2016 Thomas Harte. All rights reserved.
 //
 
+#include <cmath>
+
 #include "AY38910.hpp"
 
-#include <cmath>
+//namespace GI {
+//namespace AY38910 {
 
 using namespace GI::AY38910;
 
-AY38910::AY38910(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) {
+template <bool is_stereo>
+AY38910<is_stereo>::AY38910(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) {
 	// Don't use the low bit of the envelope position if this is an AY.
 	envelope_position_mask_ |= personality == Personality::AY38910;
 
@@ -70,7 +74,7 @@ AY38910::AY38910(Personality personality, Concurrency::DeferringAsyncTaskQueue &
 	set_sample_volume_range(0);
 }
 
-void AY38910::set_sample_volume_range(std::int16_t range) {
+template <bool is_stereo> void AY38910<is_stereo>::set_sample_volume_range(std::int16_t range) {
 	// Set up volume lookup table; the function below is based on a combination of the graph
 	// from the YM's datasheet, showing a clear power curve, and fitting that to observed
 	// values reported elsewhere.
@@ -85,15 +89,10 @@ void AY38910::set_sample_volume_range(std::int16_t range) {
 		volumes_[v] -= volumes_[0];
 	}
 
-	if(is_stereo_) {
-		evaluate_output_volume<true>();
-	} else {
-		evaluate_output_volume<false>();
-	}
+	evaluate_output_volume();
 }
 
-void AY38910::set_output_mixing(bool is_stereo, float a_left, float b_left, float c_left, float a_right, float b_right, float c_right) {
-	is_stereo_ = is_stereo;
+template <bool is_stereo> void AY38910<is_stereo>::set_output_mixing(float a_left, float b_left, float c_left, float a_right, float b_right, float c_right) {
 	a_left_ = uint8_t(a_left * 255.0f);
 	b_left_ = uint8_t(b_left * 255.0f);
 	c_left_ = uint8_t(c_left * 255.0f);
@@ -102,15 +101,7 @@ void AY38910::set_output_mixing(bool is_stereo, float a_left, float b_left, floa
 	c_right_ = uint8_t(c_right * 255.0f);
 }
 
-void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) {
-	if(is_stereo_) {
-		get_samples<true>(number_of_samples, target);
-	} else {
-		get_samples<false>(number_of_samples, target);
-	}
-}
-
-template <bool is_stereo> void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) {
+template <bool is_stereo> void AY38910<is_stereo>::get_samples(std::size_t number_of_samples, int16_t *target) {
 	// Note on structure below: the real AY has a built-in divider of 8
 	// prior to applying its tone and noise dividers. But the YM fills the
 	// same total periods for noise and tone with double-precision envelopes.
@@ -165,7 +156,7 @@ template <bool is_stereo> void AY38910::get_samples(std::size_t number_of_sample
 			if(envelope_position_ == 64) envelope_position_ = envelope_overflow_masks_[output_registers_[13]];
 		}
 
-		evaluate_output_volume<is_stereo>();
+		evaluate_output_volume();
 
 		for(int ic = 0; ic < 4 && c < number_of_samples; ic++) {
 			if constexpr (is_stereo) {
@@ -181,7 +172,7 @@ template <bool is_stereo> void AY38910::get_samples(std::size_t number_of_sample
 	master_divider_ &= 3;
 }
 
-template <bool is_stereo> void AY38910::evaluate_output_volume() {
+template <bool is_stereo> void AY38910<is_stereo>::evaluate_output_volume() {
 	int envelope_volume = envelope_shapes_[output_registers_[13]][envelope_position_ | envelope_position_mask_];
 
 	// The output level for a channel is:
@@ -243,18 +234,18 @@ template <bool is_stereo> void AY38910::evaluate_output_volume() {
 	}
 }
 
-bool AY38910::is_zero_level() {
+template <bool is_stereo> bool AY38910<is_stereo>::is_zero_level() {
 	// Confirm that the AY is trivially at the zero level if all three volume controls are set to fixed zero.
 	return output_registers_[0x8] == 0 && output_registers_[0x9] == 0 && output_registers_[0xa] == 0;
 }
 
 // MARK: - Register manipulation
 
-void AY38910::select_register(uint8_t r) {
+template <bool is_stereo> void AY38910<is_stereo>::select_register(uint8_t r) {
 	selected_register_ = r;
 }
 
-void AY38910::set_register_value(uint8_t value) {
+template <bool is_stereo> void AY38910<is_stereo>::set_register_value(uint8_t value) {
 	// There are only 16 registers.
 	if(selected_register_ > 15) return;
 
@@ -297,11 +288,7 @@ void AY38910::set_register_value(uint8_t value) {
 			// Store a copy of the current register within the storage used by the audio generation
 			// thread, and apply any changes to output volume.
 			output_registers_[selected_register] = masked_value;
-			if(is_stereo_) {
-				evaluate_output_volume<true>();
-			} else {
-				evaluate_output_volume<false>();
-			}
+			evaluate_output_volume();
 		});
 	}
 
@@ -327,7 +314,7 @@ void AY38910::set_register_value(uint8_t value) {
 	if(update_port_a) set_port_output(false);
 }
 
-uint8_t AY38910::get_register_value() {
+template <bool is_stereo> uint8_t AY38910<is_stereo>::get_register_value() {
 	// This table ensures that bits that aren't defined within the AY are returned as 0s
 	// when read, conforming to CPC-sourced unit tests.
 	const uint8_t register_masks[16] = {
@@ -341,24 +328,24 @@ uint8_t AY38910::get_register_value() {
 
 // MARK: - Port querying
 
-uint8_t AY38910::get_port_output(bool port_b) {
+template <bool is_stereo> uint8_t AY38910<is_stereo>::get_port_output(bool port_b) {
 	return registers_[port_b ? 15 : 14];
 }
 
 // MARK: - Bus handling
 
-void AY38910::set_port_handler(PortHandler *handler) {
+template <bool is_stereo> void AY38910<is_stereo>::set_port_handler(PortHandler *handler) {
 	port_handler_ = handler;
 	set_port_output(true);
 	set_port_output(false);
 }
 
-void AY38910::set_data_input(uint8_t r) {
+template <bool is_stereo> void AY38910<is_stereo>::set_data_input(uint8_t r) {
 	data_input_ = r;
 	update_bus();
 }
 
-void AY38910::set_port_output(bool port_b) {
+template <bool is_stereo> void AY38910<is_stereo>::set_port_output(bool port_b) {
 	// Per the data sheet: "each [IO] pin is provided with an on-chip pull-up resistor,
 	// so that when in the "input" mode, all pins will read normally high". Therefore,
 	// report programmer selection of input mode as creating an output of 0xff.
@@ -368,7 +355,7 @@ void AY38910::set_port_output(bool port_b) {
 	}
 }
 
-uint8_t AY38910::get_data_output() {
+template <bool is_stereo> uint8_t AY38910<is_stereo>::get_data_output() {
 	if(control_state_ == Read && selected_register_ >= 14 && selected_register_ < 16) {
 		// Per http://cpctech.cpc-live.com/docs/psgnotes.htm if a port is defined as output then the
 		// value returned to the CPU when reading it is the and of the output value and any input.
@@ -384,7 +371,7 @@ uint8_t AY38910::get_data_output() {
 	return data_output_;
 }
 
-void AY38910::set_control_lines(ControlLines control_lines) {
+template <bool is_stereo> void AY38910<is_stereo>::set_control_lines(ControlLines control_lines) {
 	switch(int(control_lines)) {
 		default:					control_state_ = Inactive;		break;
 
@@ -399,7 +386,7 @@ void AY38910::set_control_lines(ControlLines control_lines) {
 	update_bus();
 }
 
-void AY38910::update_bus() {
+template <bool is_stereo> void AY38910<is_stereo>::update_bus() {
 	// Assume no output, unless this turns out to be a read.
 	data_output_ = 0xff;
 	switch(control_state_) {
@@ -409,3 +396,7 @@ void AY38910::update_bus() {
 		case Read:			data_output_ = get_register_value();	break;
 	}
 }
+
+// Ensure both mono and stereo versions of the AY are built.
+template class GI::AY38910::AY38910<true>;
+template class GI::AY38910::AY38910<false>;
diff --git a/Components/AY38910/AY38910.hpp b/Components/AY38910/AY38910.hpp
index 9e6e4bd93..bd9afbefd 100644
--- a/Components/AY38910/AY38910.hpp
+++ b/Components/AY38910/AY38910.hpp
@@ -66,7 +66,7 @@ enum class Personality {
 
 	This AY has an attached mono or stereo mixer.
 */
-class AY38910: public ::Outputs::Speaker::SampleSource {
+template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource {
 	public:
 		/// Creates a new AY38910.
 		AY38910(Personality, Concurrency::DeferringAsyncTaskQueue &);
@@ -103,12 +103,13 @@ class AY38910: public ::Outputs::Speaker::SampleSource {
 
 			a_left = 0.5, a_right = 0.5 will make A half volume on both outputs.
 		*/
-		void set_output_mixing(bool is_stereo, float a_left, float b_left, float c_left, float a_right, float b_right, float c_right);
+		void set_output_mixing(float a_left, float b_left, float c_left, float a_right = 1.0, float b_right = 1.0, float c_right = 1.0);
 
 		// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter.
 		void get_samples(std::size_t number_of_samples, int16_t *target);
 		bool is_zero_level();
 		void set_sample_volume_range(std::int16_t range);
+		static constexpr bool get_is_stereo() { return is_stereo; }
 
 	private:
 		Concurrency::DeferringAsyncTaskQueue &task_queue_;
@@ -155,16 +156,15 @@ class AY38910: public ::Outputs::Speaker::SampleSource {
 		PortHandler *port_handler_ = nullptr;
 		void set_port_output(bool port_b);
 
-		template <bool is_stereo> void get_samples(std::size_t number_of_samples, int16_t *target);
-		template <bool is_stereo> void evaluate_output_volume();
+		void evaluate_output_volume();
 
 		// Output mixing control.
-		bool is_stereo_ = false;
 		uint8_t a_left_ = 255, a_right_ = 255;
 		uint8_t b_left_ = 255, b_right_ = 255;
 		uint8_t c_left_ = 255, c_right_ = 255;
 };
 
+
 }
 }
 
diff --git a/Machines/Apple/Macintosh/Audio.hpp b/Machines/Apple/Macintosh/Audio.hpp
index cfec007d6..39eb774a5 100644
--- a/Machines/Apple/Macintosh/Audio.hpp
+++ b/Machines/Apple/Macintosh/Audio.hpp
@@ -55,6 +55,7 @@ class Audio: public ::Outputs::Speaker::SampleSource {
 		void get_samples(std::size_t number_of_samples, int16_t *target);
 		bool is_zero_level();
 		void set_sample_volume_range(std::int16_t range);
+		constexpr static bool get_is_stereo() { return false; }
 
 	private:
 		Concurrency::DeferringAsyncTaskQueue &task_queue_;
diff --git a/Machines/Apple/Macintosh/DeferredAudio.hpp b/Machines/Apple/Macintosh/DeferredAudio.hpp
index 50ba7f714..6fda448fa 100644
--- a/Machines/Apple/Macintosh/DeferredAudio.hpp
+++ b/Machines/Apple/Macintosh/DeferredAudio.hpp
@@ -18,7 +18,7 @@ namespace Macintosh {
 struct DeferredAudio {
 	Concurrency::DeferringAsyncTaskQueue queue;
 	Audio audio;
-	Outputs::Speaker::LowpassSpeaker<Audio, false> speaker;
+	Outputs::Speaker::LowpassSpeaker<Audio> speaker;
 	HalfCycles time_since_update;
 
 	DeferredAudio() : audio(queue), speaker(audio) {}
diff --git a/Machines/Oric/Oric.cpp b/Machines/Oric/Oric.cpp
index 043ab14cf..b25162f6e 100644
--- a/Machines/Oric/Oric.cpp
+++ b/Machines/Oric/Oric.cpp
@@ -43,7 +43,8 @@
 namespace Oric {
 
 using DiskInterface = Analyser::Static::Oric::Target::DiskInterface;
-using Speaker = Outputs::Speaker::LowpassSpeaker<GI::AY38910::AY38910, false>;
+using AY = GI::AY38910::AY38910<false>;
+using Speaker = Outputs::Speaker::LowpassSpeaker<AY>;
 
 enum ROM {
 	BASIC10 = 0, BASIC11, Microdisc, Colour
@@ -147,7 +148,7 @@ class TapePlayer: public Storage::Tape::BinaryTapePlayer {
 */
 class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
 	public:
-		VIAPortHandler(Concurrency::DeferringAsyncTaskQueue &audio_queue, GI::AY38910::AY38910 &ay8910, Speaker &speaker, TapePlayer &tape_player, Keyboard &keyboard) :
+		VIAPortHandler(Concurrency::DeferringAsyncTaskQueue &audio_queue, AY &ay8910, Speaker &speaker, TapePlayer &tape_player, Keyboard &keyboard) :
 			audio_queue_(audio_queue), ay8910_(ay8910), speaker_(speaker), tape_player_(tape_player), keyboard_(keyboard) {}
 
 		/*!
@@ -210,7 +211,7 @@ class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
 		HalfCycles cycles_since_ay_update_;
 
 		Concurrency::DeferringAsyncTaskQueue &audio_queue_;
-		GI::AY38910::AY38910 &ay8910_;
+		AY &ay8910_;
 		Speaker &speaker_;
 		TapePlayer &tape_player_;
 		Keyboard &keyboard_;
@@ -692,7 +693,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
 		VideoOutput video_output_;
 
 		Concurrency::DeferringAsyncTaskQueue audio_queue_;
-		GI::AY38910::AY38910 ay8910_;
+		GI::AY38910::AY38910<false> ay8910_;
 		Speaker speaker_;
 
 		// Inputs
diff --git a/Machines/ZX8081/ZX8081.cpp b/Machines/ZX8081/ZX8081.cpp
index 2ffbdf62d..16d1f1db5 100644
--- a/Machines/ZX8081/ZX8081.cpp
+++ b/Machines/ZX8081/ZX8081.cpp
@@ -467,8 +467,9 @@ template<bool is_zx81> class ConcreteMachine:
 
 		// MARK: - Audio
 		Concurrency::DeferringAsyncTaskQueue audio_queue_;
-		GI::AY38910::AY38910 ay_;
-		Outputs::Speaker::LowpassSpeaker<GI::AY38910::AY38910, false> speaker_;
+		using AY = GI::AY38910::AY38910<false>;
+		AY ay_;
+		Outputs::Speaker::LowpassSpeaker<AY> speaker_;
 		HalfCycles time_since_ay_update_;
 		inline void ay_set_register(uint8_t value) {
 			update_audio();
diff --git a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp
index 97f51ad92..44f76e506 100644
--- a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp
+++ b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp
@@ -28,7 +28,7 @@ namespace Speaker {
 	source of a high-frequency stream of audio which it filters down to a
 	lower-frequency output.
 */
-template <typename SampleSource, bool is_stereo> class LowpassSpeaker: public Speaker {
+template <typename SampleSource> class LowpassSpeaker: public Speaker {
 	public:
 		LowpassSpeaker(SampleSource &sample_source) : sample_source_(sample_source) {
 			sample_source.set_sample_volume_range(32767);
@@ -66,11 +66,11 @@ template <typename SampleSource, bool is_stereo> class LowpassSpeaker: public Sp
 
 			filter_parameters_.output_cycles_per_second = cycles_per_second;
 			filter_parameters_.parameters_are_dirty = true;
-			output_buffer_.resize(std::size_t(buffer_size) * (is_stereo ? 2 : 1));
+			output_buffer_.resize(std::size_t(buffer_size) * (SampleSource::get_is_stereo() ? 2 : 1));
 		}
 
 		bool get_is_stereo() final {
-			return is_stereo;
+			return SampleSource::get_is_stereo();
 		}
 
 		/*!
@@ -144,14 +144,14 @@ template <typename SampleSource, bool is_stereo> class LowpassSpeaker: public Sp
 			switch(conversion_) {
 				case Conversion::Copy:
 					while(cycles_remaining) {
-						const auto cycles_to_read = std::min((output_buffer_.size() - output_buffer_pointer_) / (is_stereo ? 2 : 1), cycles_remaining);
+						const auto cycles_to_read = std::min((output_buffer_.size() - output_buffer_pointer_) / (SampleSource::get_is_stereo() ? 2 : 1), cycles_remaining);
 						sample_source_.get_samples(cycles_to_read, &output_buffer_[output_buffer_pointer_ ]);
-						output_buffer_pointer_ += cycles_to_read * (is_stereo ? 2 : 1);
+						output_buffer_pointer_ += cycles_to_read * (SampleSource::get_is_stereo() ? 2 : 1);
 
 						// Announce to delegate if full.
 						if(output_buffer_pointer_ == output_buffer_.size()) {
 							output_buffer_pointer_ = 0;
-							did_complete_samples(this, output_buffer_, is_stereo);
+							did_complete_samples(this, output_buffer_, SampleSource::get_is_stereo());
 						}
 
 						cycles_remaining -= cycles_to_read;
@@ -160,10 +160,10 @@ template <typename SampleSource, bool is_stereo> class LowpassSpeaker: public Sp
 
 				case Conversion::ResampleSmaller:
 					while(cycles_remaining) {
-						const auto cycles_to_read = std::min((input_buffer_.size() - input_buffer_depth_) / (is_stereo ? 2 : 1), cycles_remaining);
+						const auto cycles_to_read = std::min((input_buffer_.size() - input_buffer_depth_) / (SampleSource::get_is_stereo() ? 2 : 1), cycles_remaining);
 
 						sample_source_.get_samples(cycles_to_read, &input_buffer_[input_buffer_depth_]);
-						input_buffer_depth_ += cycles_to_read * (is_stereo ? 2 : 1);
+						input_buffer_depth_ += cycles_to_read * (SampleSource::get_is_stereo() ? 2 : 1);
 
 						if(input_buffer_depth_ == input_buffer_.size()) {
 							resample_input_buffer();
@@ -248,7 +248,7 @@ template <typename SampleSource, bool is_stereo> class LowpassSpeaker: public Sp
 					// Reize the input buffer only if absolutely necessary; if sizing downward
 					// such that a sample would otherwise be lost then output it now. Keep anything
 					// currently in the input buffer that hasn't yet been processed.
-					const size_t required_buffer_size = size_t(number_of_taps) * (is_stereo ? 2 : 1);
+					const size_t required_buffer_size = size_t(number_of_taps) * (SampleSource::get_is_stereo() ? 2 : 1);
 					if(input_buffer_.size() != required_buffer_size) {
 						if(input_buffer_depth_ >= required_buffer_size) {
 							resample_input_buffer();
@@ -261,7 +261,7 @@ template <typename SampleSource, bool is_stereo> class LowpassSpeaker: public Sp
 		}
 
 		inline void resample_input_buffer() {
-			if constexpr (is_stereo) {
+			if constexpr (SampleSource::get_is_stereo()) {
 				output_buffer_[output_buffer_pointer_ + 0] = filter_->apply(input_buffer_.data(), 2);
 				output_buffer_[output_buffer_pointer_ + 1] = filter_->apply(input_buffer_.data() + 1, 2);
 				output_buffer_pointer_+= 2;
@@ -273,13 +273,13 @@ template <typename SampleSource, bool is_stereo> class LowpassSpeaker: public Sp
 			// Announce to delegate if full.
 			if(output_buffer_pointer_ == output_buffer_.size()) {
 				output_buffer_pointer_ = 0;
-				did_complete_samples(this, output_buffer_, is_stereo);
+				did_complete_samples(this, output_buffer_, SampleSource::get_is_stereo());
 			}
 
 			// If the next loop around is going to reuse some of the samples just collected, use a memmove to
 			// preserve them in the correct locations (TODO: use a longer buffer to fix that?) and don't skip
 			// anything. Otherwise skip as required to get to the next sample batch and don't expect to reuse.
-			const auto steps = stepper_->step() * (is_stereo ? 2 : 1);
+			const auto steps = stepper_->step() * (SampleSource::get_is_stereo() ? 2 : 1);
 			if(steps < input_buffer_.size()) {
 				auto *const input_buffer = input_buffer_.data();
 				std::memmove(	input_buffer,
@@ -288,7 +288,7 @@ template <typename SampleSource, bool is_stereo> class LowpassSpeaker: public Sp
 				input_buffer_depth_ -= steps;
 			} else {
 				if(steps > input_buffer_.size()) {
-					sample_source_.skip_samples((steps - input_buffer_.size()) / (is_stereo ? 2 : 1));
+					sample_source_.skip_samples((steps - input_buffer_.size()) / (SampleSource::get_is_stereo() ? 2 : 1));
 				}
 				input_buffer_depth_ = 0;
 			}