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; }