From 6dcc13921fc91489a4c699a8b557a39a2b16a1cf Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Tue, 13 Feb 2024 10:51:33 -0500 Subject: [PATCH 1/5] Make first sweep at converting AY to a SampleSource. --- Components/AY38910/AY38910.cpp | 130 ++++++++---------- Components/AY38910/AY38910.hpp | 22 ++- Components/AudioToggle/AudioToggle.hpp | 3 + Components/OPx/OPLL.hpp | 1 + Machines/Apple/AppleIIgs/Sound.hpp | 1 + .../Speaker/Implementation/BufferSource.hpp | 26 ++-- 6 files changed, 91 insertions(+), 92 deletions(-) diff --git a/Components/AY38910/AY38910.cpp b/Components/AY38910/AY38910.cpp index 4bf37fafa..e4cb90027 100644 --- a/Components/AY38910/AY38910.cpp +++ b/Components/AY38910/AY38910.cpp @@ -10,13 +10,10 @@ #include "AY38910.hpp" -//namespace GI { -//namespace AY38910 { - using namespace GI::AY38910; template -AY38910::AY38910(Personality personality, Concurrency::AsyncTaskQueue &task_queue) : task_queue_(task_queue) { +AY38910SampleSource::AY38910SampleSource(Personality personality, Concurrency::AsyncTaskQueue &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; @@ -74,7 +71,8 @@ AY38910::AY38910(Personality personality, Concurrency::AsyncTaskQueue set_sample_volume_range(0); } -template void AY38910::set_sample_volume_range(std::int16_t range) { +template +void AY38910SampleSource::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. @@ -92,7 +90,8 @@ template void AY38910::set_sample_volume_range(std:: evaluate_output_volume(); } -template void AY38910::set_output_mixing(float a_left, float b_left, float c_left, float a_right, float b_right, float c_right) { +template +void AY38910SampleSource::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); @@ -101,12 +100,6 @@ template void AY38910::set_output_mixing(float a_lef c_right_ = uint8_t(c_right * 255.0f); } -template -template -void AY38910::apply_samples( - std::size_t number_of_samples, - typename Outputs::Speaker::SampleT::type *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. @@ -116,14 +109,8 @@ void AY38910::apply_samples( // matching the YM datasheet's depiction of envelope level 31 as equal to // programmatic volume 15, envelope level 29 as equal to programmatic 14, etc. - std::size_t c = 0; - while((master_divider_&3) && c < number_of_samples) { - Outputs::Speaker::apply(target[c], output_volume_); - master_divider_++; - c++; - } - - while(c < number_of_samples) { +template +void AY38910SampleSource::advance() { #define step_channel(c) \ if(tone_counters_[c]) tone_counters_[c]--;\ else {\ @@ -131,52 +118,42 @@ void AY38910::apply_samples( tone_counters_[c] = tone_periods_[c] << 1;\ } - // Update the tone channels. - step_channel(0); - step_channel(1); - step_channel(2); + // Update the tone channels. + step_channel(0); + step_channel(1); + step_channel(2); #undef step_channel - // Update the noise generator. This recomputes the new bit repeatedly but harmlessly, only shifting - // it into the official 17 upon divider underflow. - if(noise_counter_) noise_counter_--; - else { - noise_counter_ = noise_period_ << 1; // To cover the double resolution of envelopes. - noise_output_ ^= noise_shift_register_&1; - noise_shift_register_ |= ((noise_shift_register_ ^ (noise_shift_register_ >> 3))&1) << 17; - noise_shift_register_ >>= 1; - } - - // Update the envelope generator. Table based for pattern lookup, with a 'refill' step: a way of - // implementing non-repeating patterns by locking them to the final table position. - if(envelope_divider_) envelope_divider_--; - else { - envelope_divider_ = envelope_period_; - envelope_position_ ++; - if(envelope_position_ == 64) envelope_position_ = envelope_overflow_masks_[output_registers_[13]]; - } - - evaluate_output_volume(); - - for(int ic = 0; ic < 4 && c < number_of_samples; ic++) { - Outputs::Speaker::apply(target[c], output_volume_); - c++; - master_divider_++; - } + // Update the noise generator. This recomputes the new bit repeatedly but harmlessly, only shifting + // it into the official 17 upon divider underflow. + if(noise_counter_) noise_counter_--; + else { + noise_counter_ = noise_period_ << 1; // To cover the double resolution of envelopes. + noise_output_ ^= noise_shift_register_&1; + noise_shift_register_ |= ((noise_shift_register_ ^ (noise_shift_register_ >> 3))&1) << 17; + noise_shift_register_ >>= 1; } - master_divider_ &= 3; + // Update the envelope generator. Table based for pattern lookup, with a 'refill' step: a way of + // implementing non-repeating patterns by locking them to the final table position. + if(envelope_divider_) envelope_divider_--; + else { + envelope_divider_ = envelope_period_; + envelope_position_ ++; + if(envelope_position_ == 64) envelope_position_ = envelope_overflow_masks_[output_registers_[13]]; + } + + evaluate_output_volume(); } -template void AY38910::apply_samples(std::size_t, typename Outputs::Speaker::SampleT::type *); -template void AY38910::apply_samples(std::size_t, typename Outputs::Speaker::SampleT::type *); -template void AY38910::apply_samples(std::size_t, typename Outputs::Speaker::SampleT::type *); -template void AY38910::apply_samples(std::size_t, typename Outputs::Speaker::SampleT::type *); -template void AY38910::apply_samples(std::size_t, typename Outputs::Speaker::SampleT::type *); -template void AY38910::apply_samples(std::size_t, typename Outputs::Speaker::SampleT::type *); +template +typename Outputs::Speaker::SampleT::type AY38910SampleSource::level() const { + return output_volume_; +} -template void AY38910::evaluate_output_volume() { +template +void AY38910SampleSource::evaluate_output_volume() { int envelope_volume = envelope_shapes_[output_registers_[13]][envelope_position_ | envelope_position_mask_]; // The output level for a channel is: @@ -237,18 +214,21 @@ template void AY38910::evaluate_output_volume() { } } -template bool AY38910::is_zero_level() const { +template +bool AY38910SampleSource::is_zero_level() const { // 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 -template void AY38910::select_register(uint8_t r) { +template +void AY38910SampleSource::select_register(uint8_t r) { selected_register_ = r; } -template void AY38910::set_register_value(uint8_t value) { +template +void AY38910SampleSource::set_register_value(uint8_t value) { // There are only 16 registers. if(selected_register_ > 15) return; @@ -317,7 +297,8 @@ template void AY38910::set_register_value(uint8_t va if(update_port_a) set_port_output(false); } -template uint8_t AY38910::get_register_value() { +template +uint8_t AY38910SampleSource::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] = { @@ -331,24 +312,28 @@ template uint8_t AY38910::get_register_value() { // MARK: - Port querying -template uint8_t AY38910::get_port_output(bool port_b) { +template +uint8_t AY38910SampleSource::get_port_output(bool port_b) { return registers_[port_b ? 15 : 14]; } // MARK: - Bus handling -template void AY38910::set_port_handler(PortHandler *handler) { +template +void AY38910SampleSource::set_port_handler(PortHandler *handler) { port_handler_ = handler; set_port_output(true); set_port_output(false); } -template void AY38910::set_data_input(uint8_t r) { +template +void AY38910SampleSource::set_data_input(uint8_t r) { data_input_ = r; update_bus(); } -template void AY38910::set_port_output(bool port_b) { +template +void AY38910SampleSource::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. @@ -358,7 +343,8 @@ template void AY38910::set_port_output(bool port_b) } } -template uint8_t AY38910::get_data_output() { +template +uint8_t AY38910SampleSource::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. @@ -374,7 +360,8 @@ template uint8_t AY38910::get_data_output() { return data_output_; } -template void AY38910::set_control_lines(ControlLines control_lines) { +template +void AY38910SampleSource::set_control_lines(ControlLines control_lines) { switch(int(control_lines)) { default: control_state_ = Inactive; break; @@ -389,7 +376,8 @@ template void AY38910::set_control_lines(ControlLine update_bus(); } -template void AY38910::update_bus() { +template +void AY38910SampleSource::update_bus() { // Assume no output, unless this turns out to be a read. data_output_ = 0xff; switch(control_state_) { @@ -401,5 +389,5 @@ template void AY38910::update_bus() { } // Ensure both mono and stereo versions of the AY are built. -template class GI::AY38910::AY38910; -template class GI::AY38910::AY38910; +template class GI::AY38910::AY38910SampleSource; +template class GI::AY38910::AY38910SampleSource; diff --git a/Components/AY38910/AY38910.hpp b/Components/AY38910/AY38910.hpp index 722abcba2..3d82d4f3f 100644 --- a/Components/AY38910/AY38910.hpp +++ b/Components/AY38910/AY38910.hpp @@ -66,10 +66,10 @@ enum class Personality { This AY has an attached mono or stereo mixer. */ -template class AY38910: public ::Outputs::Speaker::BufferSource, stereo> { +template class AY38910SampleSource { public: /// Creates a new AY38910. - AY38910(Personality, Concurrency::AsyncTaskQueue &); + AY38910SampleSource(Personality, Concurrency::AsyncTaskQueue &); /// Sets the value the AY would read from its data lines if it were not outputting. void set_data_input(uint8_t r); @@ -105,9 +105,9 @@ template class AY38910: public ::Outputs::Speaker::BufferSource - void apply_samples(std::size_t number_of_samples, typename Outputs::Speaker::SampleT::type *target); + // Sample generation. + typename Outputs::Speaker::SampleT::type level() const; + void advance(); bool is_zero_level() const; void set_sample_volume_range(std::int16_t range); @@ -118,8 +118,6 @@ template class AY38910: public ::Outputs::Speaker::BufferSource class AY38910: public ::Outputs::Speaker::BufferSource struct AY38910: + public AY38910SampleSource, + public Outputs::Speaker::SampleSource, stereo, 4> { + + using AY38910SampleSource::AY38910SampleSource; + + }; + /*! Provides helper code, to provide something closer to the interface exposed by many AY-deploying machines of the era. diff --git a/Components/AudioToggle/AudioToggle.hpp b/Components/AudioToggle/AudioToggle.hpp index ea304e06a..06312225c 100644 --- a/Components/AudioToggle/AudioToggle.hpp +++ b/Components/AudioToggle/AudioToggle.hpp @@ -25,6 +25,9 @@ class Toggle: public Outputs::Speaker::BufferSource { Outputs::Speaker::fill(target, target + number_of_samples, level_); } void set_sample_volume_range(std::int16_t range); + bool is_zero_level() const { + return !level_; + } void set_output(bool enabled); bool get_output() const; diff --git a/Components/OPx/OPLL.hpp b/Components/OPx/OPLL.hpp index da08a3f97..bca92a969 100644 --- a/Components/OPx/OPLL.hpp +++ b/Components/OPx/OPLL.hpp @@ -28,6 +28,7 @@ class OPLL: public OPLBase { template void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target); void set_sample_volume_range(std::int16_t range); + bool is_zero_level() const { return false; } // TODO. // The OPLL is generally 'half' as loud as it's told to be. This won't strictly be true in // rhythm mode, but it's correct for melodic output. diff --git a/Machines/Apple/AppleIIgs/Sound.hpp b/Machines/Apple/AppleIIgs/Sound.hpp index ea25afba7..711873cbd 100644 --- a/Machines/Apple/AppleIIgs/Sound.hpp +++ b/Machines/Apple/AppleIIgs/Sound.hpp @@ -37,6 +37,7 @@ class GLU: public Outputs::Speaker::BufferSource { // TODO: isn't th template void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target); void set_sample_volume_range(std::int16_t range); + bool is_zero_level() const { return false; } // TODO. private: Concurrency::AsyncTaskQueue &audio_queue_; diff --git a/Outputs/Speaker/Implementation/BufferSource.hpp b/Outputs/Speaker/Implementation/BufferSource.hpp index f5d4f7573..3ac7eb323 100644 --- a/Outputs/Speaker/Implementation/BufferSource.hpp +++ b/Outputs/Speaker/Implementation/BufferSource.hpp @@ -76,13 +76,13 @@ class BufferSource { fill the target with zeroes; @c false if a call might return all zeroes or might not. */ - bool is_zero_level() const { return false; } +// bool is_zero_level() const { return false; } /*! Sets the proper output range for this sample source; it should write values between 0 and volume. */ - void set_sample_volume_range(std::int16_t volume); +// void set_sample_volume_range(std::int16_t volume); /*! Permits a sample source to declare that, averaged over time, it will use only @@ -101,13 +101,13 @@ class BufferSource { template struct SampleSource: public BufferSource { public: - template + template void apply_samples(std::size_t number_of_samples, typename SampleT::type *target) { - const auto &source = *static_cast(this); + auto &source = *static_cast(this); if constexpr (divider == 1) { while(number_of_samples--) { - apply(*target, source.level()); + apply(*target, source.level()); ++target; source.advance(); } @@ -117,34 +117,32 @@ struct SampleSource: public BufferSource { // Fill in the tail of any partially-captured level. auto level = source.level(); while(c < number_of_samples && master_divider_ != divider) { - apply(target[c], level); + apply(target[c], level); ++c; ++master_divider_; } source.advance(); // Provide all full levels. - int whole_steps = (number_of_samples - c) / divider; + auto whole_steps = static_cast((number_of_samples - c) / divider); while(whole_steps--) { - fill(&target[c], &target[c + divider], source.level()); + fill(&target[c], &target[c + divider], source.level()); c += divider; source.advance(); } // Provide the head of a further partial capture. level = source.level(); - master_divider_ = number_of_samples - c; - fill(&target[c], &target[number_of_samples], source.level()); + master_divider_ = static_cast(number_of_samples - c); + fill(&target[c], &target[number_of_samples], source.level()); } } // TODO: use a concept here, when C++20 filters through. // // Until then: sample sources should implement this. - auto level() const { - return typename SampleT::type(); - } - void advance() {} +// typename SampleT::type level() const; +// void advance(); private: int master_divider_{}; From e06a66644c970cf9c345e22c0ee24b92c89ae810 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Tue, 13 Feb 2024 10:54:53 -0500 Subject: [PATCH 2/5] Eliminate a macro. --- Components/AY38910/AY38910.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Components/AY38910/AY38910.cpp b/Components/AY38910/AY38910.cpp index e4cb90027..76612c59d 100644 --- a/Components/AY38910/AY38910.cpp +++ b/Components/AY38910/AY38910.cpp @@ -111,20 +111,19 @@ void AY38910SampleSource::set_output_mixing(float a_left, float b_lef template void AY38910SampleSource::advance() { -#define step_channel(c) \ - if(tone_counters_[c]) tone_counters_[c]--;\ - else {\ - tone_outputs_[c] ^= 1;\ - tone_counters_[c] = tone_periods_[c] << 1;\ - } + const auto step_channel = [&](int c) { + if(tone_counters_[c]) --tone_counters_[c]; + else { + tone_outputs_[c] ^= 1; + tone_counters_[c] = tone_periods_[c] << 1; + } + }; // Update the tone channels. step_channel(0); step_channel(1); step_channel(2); -#undef step_channel - // Update the noise generator. This recomputes the new bit repeatedly but harmlessly, only shifting // it into the official 17 upon divider underflow. if(noise_counter_) noise_counter_--; From 1bb82189e959ff381a648e4e9c52cd980171cdfc Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Tue, 13 Feb 2024 10:57:09 -0500 Subject: [PATCH 3/5] Add better exposition. --- Components/AY38910/AY38910.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Components/AY38910/AY38910.cpp b/Components/AY38910/AY38910.cpp index 76612c59d..3fa8462e1 100644 --- a/Components/AY38910/AY38910.cpp +++ b/Components/AY38910/AY38910.cpp @@ -17,7 +17,8 @@ AY38910SampleSource::AY38910SampleSource(Personality personality, Con // Don't use the low bit of the envelope position if this is an AY. envelope_position_mask_ |= personality == Personality::AY38910; - // Set up envelope lookup tables. + // Set up envelope lookup tables; these are based on 32 volume levels as used by the YM2149F. + // The AY38910 will just use only even table entries, and therefore only even volumes. for(int c = 0; c < 16; c++) { for(int p = 0; p < 64; p++) { switch(c) { From a3e104f8e2344d33f88a779901a11f7cce5add26 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Tue, 13 Feb 2024 13:46:27 -0500 Subject: [PATCH 4/5] Clean up commentary. --- Components/AY38910/AY38910.cpp | 23 ++++++++++++++--------- Components/AY38910/AY38910.hpp | 7 +++++-- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/Components/AY38910/AY38910.cpp b/Components/AY38910/AY38910.cpp index 3fa8462e1..c32df440e 100644 --- a/Components/AY38910/AY38910.cpp +++ b/Components/AY38910/AY38910.cpp @@ -12,6 +12,15 @@ using namespace GI::AY38910; +// Note on dividers: 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. +// Therefore this class implements a divider of 4 and doubles the tone +// and noise periods. The envelope ticks along at the divide-by-four rate, +// but if this is an AY rather than a YM then its lowest bit is forced to 1, +// matching the YM datasheet's depiction of envelope level 31 as equal to +// programmatic volume 15, envelope level 29 as equal to programmatic 14, etc. + template AY38910SampleSource::AY38910SampleSource(Personality personality, Concurrency::AsyncTaskQueue &task_queue) : task_queue_(task_queue) { // Don't use the low bit of the envelope position if this is an AY. @@ -101,15 +110,6 @@ void AY38910SampleSource::set_output_mixing(float a_left, float b_lef c_right_ = uint8_t(c_right * 255.0f); } - // 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. - // Therefore this class implements a divider of 4 and doubles the tone - // and noise periods. The envelope ticks along at the divide-by-four rate, - // but if this is an AY rather than a YM then its lowest bit is forced to 1, - // matching the YM datasheet's depiction of envelope level 31 as equal to - // programmatic volume 15, envelope level 29 as equal to programmatic 14, etc. - template void AY38910SampleSource::advance() { const auto step_channel = [&](int c) { @@ -391,3 +391,8 @@ void AY38910SampleSource::update_bus() { // Ensure both mono and stereo versions of the AY are built. template class GI::AY38910::AY38910SampleSource; template class GI::AY38910::AY38910SampleSource; + +// Perform an explicit instantiation of the BufferSource to hope for +// appropriate inlining of advance() and level(). +template struct GI::AY38910::AY38910; +template struct GI::AY38910::AY38910; diff --git a/Components/AY38910/AY38910.hpp b/Components/AY38910/AY38910.hpp index 3d82d4f3f..3e951bbad 100644 --- a/Components/AY38910/AY38910.hpp +++ b/Components/AY38910/AY38910.hpp @@ -164,8 +164,11 @@ template class AY38910SampleSource { friend struct State; }; -/// Define a default AY to be the sample source with a master divider of 4; -/// real AYs have a divide-by-8 step built in but YMs have only a divide-by-4, +/// Defines a default AY to be the sample source with a master divider of 4; +/// real AYs have a divide-by-8 step built in but YMs apply only a divide by 4. +/// +/// The implementation of AY38910SampleSource combines those two worlds +/// by always applying a divide by four and scaling other things as appropriate. template struct AY38910: public AY38910SampleSource, public Outputs::Speaker::SampleSource, stereo, 4> { From 3ba261854748ea69368cc777466a4409191c8a46 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Tue, 13 Feb 2024 13:48:31 -0500 Subject: [PATCH 5/5] Fix formatting, add comment. --- Components/AY38910/AY38910.hpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Components/AY38910/AY38910.hpp b/Components/AY38910/AY38910.hpp index 3e951bbad..0c21c1063 100644 --- a/Components/AY38910/AY38910.hpp +++ b/Components/AY38910/AY38910.hpp @@ -173,9 +173,10 @@ template struct AY38910: public AY38910SampleSource, public Outputs::Speaker::SampleSource, stereo, 4> { + // Use the same constructor as `AY38910SampleSource` (along with inheriting + // the rest of its interface). using AY38910SampleSource::AY38910SampleSource; - - }; +}; /*! Provides helper code, to provide something closer to the interface exposed by many