diff --git a/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.cpp b/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.cpp index a6f412ca4..1465771e0 100644 --- a/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.cpp +++ b/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.cpp @@ -37,12 +37,23 @@ float MultiSpeaker::get_ideal_clock_rate_in_range(float minimum, float maximum) return ideal / static_cast(speakers_.size()); } -void MultiSpeaker::set_computed_output_rate(float cycles_per_second, int buffer_size) { +void MultiSpeaker::set_computed_output_rate(float cycles_per_second, int buffer_size, bool stereo) { + stereo_output_ = stereo; for(const auto &speaker: speakers_) { - speaker->set_computed_output_rate(cycles_per_second, buffer_size); + speaker->set_computed_output_rate(cycles_per_second, buffer_size, stereo); } } +bool MultiSpeaker::get_is_stereo() { + // Return as stereo if any subspeaker is stereo. + for(const auto &speaker: speakers_) { + if(speaker->get_is_stereo()) { + return true; + } + } + return false; +} + void MultiSpeaker::set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) { delegate_ = delegate; } @@ -53,7 +64,7 @@ void MultiSpeaker::speaker_did_complete_samples(Speaker *speaker, const std::vec std::lock_guard lock_guard(front_speaker_mutex_); if(speaker != front_speaker_) return; } - did_complete_samples(this, buffer); + did_complete_samples(this, buffer, stereo_output_); } void MultiSpeaker::speaker_did_change_input_clock(Speaker *speaker) { diff --git a/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.hpp b/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.hpp index 1dcf74736..3c75654db 100644 --- a/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.hpp +++ b/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.hpp @@ -39,8 +39,9 @@ class MultiSpeaker: public Outputs::Speaker::Speaker, Outputs::Speaker::Speaker: // Below is the standard Outputs::Speaker::Speaker interface; see there for documentation. float get_ideal_clock_rate_in_range(float minimum, float maximum) override; - void set_computed_output_rate(float cycles_per_second, int buffer_size) override; + void set_computed_output_rate(float cycles_per_second, int buffer_size, bool stereo) override; void set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) override; + bool get_is_stereo() override; private: void speaker_did_complete_samples(Speaker *speaker, const std::vector &buffer) final; @@ -51,6 +52,8 @@ class MultiSpeaker: public Outputs::Speaker::Speaker, Outputs::Speaker::Speaker: Outputs::Speaker::Speaker *front_speaker_ = nullptr; Outputs::Speaker::Speaker::Delegate *delegate_ = nullptr; std::mutex front_speaker_mutex_; + + bool stereo_output_ = false; }; } diff --git a/Components/6560/6560.hpp b/Components/6560/6560.hpp index e975ef596..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_; diff --git a/Components/AY38910/AY38910.cpp b/Components/AY38910/AY38910.cpp index b789cc152..28292bcc8 100644 --- a/Components/AY38910/AY38910.cpp +++ b/Components/AY38910/AY38910.cpp @@ -6,13 +6,17 @@ // Copyright 2016 Thomas Harte. All rights reserved. // +#include + #include "AY38910.hpp" -#include +//namespace GI { +//namespace AY38910 { using namespace GI::AY38910; -AY38910::AY38910(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) { +template +AY38910::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 void AY38910::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. @@ -84,10 +88,20 @@ void AY38910::set_sample_volume_range(std::int16_t range) { for(int v = 31; v >= 0; --v) { volumes_[v] -= volumes_[0]; } + evaluate_output_volume(); } -void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) { +template void AY38910::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); + a_right_ = uint8_t(a_right * 255.0f); + b_right_ = uint8_t(b_right * 255.0f); + c_right_ = uint8_t(c_right * 255.0f); +} + +template void AY38910::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. @@ -99,7 +113,11 @@ void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) { std::size_t c = 0; while((master_divider_&3) && c < number_of_samples) { - target[c] = output_volume_; + if constexpr (is_stereo) { + reinterpret_cast(target)[c] = output_volume_; + } else { + target[c] = int16_t(output_volume_); + } master_divider_++; c++; } @@ -141,7 +159,11 @@ void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) { evaluate_output_volume(); for(int ic = 0; ic < 4 && c < number_of_samples; ic++) { - target[c] = output_volume_; + if constexpr (is_stereo) { + reinterpret_cast(target)[c] = output_volume_; + } else { + target[c] = int16_t(output_volume_); + } c++; master_divider_++; } @@ -150,7 +172,7 @@ void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) { master_divider_ &= 3; } -void AY38910::evaluate_output_volume() { +template void AY38910::evaluate_output_volume() { int envelope_volume = envelope_shapes_[output_registers_[13]][envelope_position_ | envelope_position_mask_]; // The output level for a channel is: @@ -190,26 +212,40 @@ void AY38910::evaluate_output_volume() { }; #undef channel_volume - // Mix additively. - output_volume_ = int16_t( - volumes_[volumes[0]] * channel_levels[0] + - volumes_[volumes[1]] * channel_levels[1] + - volumes_[volumes[2]] * channel_levels[2] - ); + // Mix additively, weighting if in stereo. + if constexpr (is_stereo) { + int16_t *const output_volumes = reinterpret_cast(&output_volume_); + output_volumes[0] = int16_t(( + volumes_[volumes[0]] * channel_levels[0] * a_left_ + + volumes_[volumes[1]] * channel_levels[1] * b_left_ + + volumes_[volumes[2]] * channel_levels[2] * c_left_ + ) >> 8); + output_volumes[1] = int16_t(( + volumes_[volumes[0]] * channel_levels[0] * a_right_ + + volumes_[volumes[1]] * channel_levels[1] * b_right_ + + volumes_[volumes[2]] * channel_levels[2] * c_right_ + ) >> 8); + } else { + output_volume_ = uint32_t( + volumes_[volumes[0]] * channel_levels[0] + + volumes_[volumes[1]] * channel_levels[1] + + volumes_[volumes[2]] * channel_levels[2] + ); + } } -bool AY38910::is_zero_level() { +template bool AY38910::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 void AY38910::select_register(uint8_t r) { selected_register_ = r; } -void AY38910::set_register_value(uint8_t value) { +template void AY38910::set_register_value(uint8_t value) { // There are only 16 registers. if(selected_register_ > 15) return; @@ -278,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 uint8_t AY38910::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] = { @@ -292,24 +328,24 @@ uint8_t AY38910::get_register_value() { // MARK: - Port querying -uint8_t AY38910::get_port_output(bool port_b) { +template uint8_t AY38910::get_port_output(bool port_b) { return registers_[port_b ? 15 : 14]; } // MARK: - Bus handling -void AY38910::set_port_handler(PortHandler *handler) { +template void AY38910::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 void AY38910::set_data_input(uint8_t r) { data_input_ = r; update_bus(); } -void AY38910::set_port_output(bool port_b) { +template void AY38910::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. @@ -319,7 +355,7 @@ void AY38910::set_port_output(bool port_b) { } } -uint8_t AY38910::get_data_output() { +template uint8_t AY38910::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. @@ -335,7 +371,7 @@ uint8_t AY38910::get_data_output() { return data_output_; } -void AY38910::set_control_lines(ControlLines control_lines) { +template void AY38910::set_control_lines(ControlLines control_lines) { switch(int(control_lines)) { default: control_state_ = Inactive; break; @@ -350,7 +386,7 @@ void AY38910::set_control_lines(ControlLines control_lines) { update_bus(); } -void AY38910::update_bus() { +template void AY38910::update_bus() { // Assume no output, unless this turns out to be a read. data_output_ = 0xff; switch(control_state_) { @@ -360,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; +template class GI::AY38910::AY38910; diff --git a/Components/AY38910/AY38910.hpp b/Components/AY38910/AY38910.hpp index 2a1d777c4..bd9afbefd 100644 --- a/Components/AY38910/AY38910.hpp +++ b/Components/AY38910/AY38910.hpp @@ -63,8 +63,10 @@ enum class Personality { Provides emulation of an AY-3-8910 / YM2149, which is a three-channel sound chip with a noise generator and a volume envelope generator, which also provides two bidirectional interface ports. + + This AY has an attached mono or stereo mixer. */ -class AY38910: public ::Outputs::Speaker::SampleSource { +template class AY38910: public ::Outputs::Speaker::SampleSource { public: /// Creates a new AY38910. AY38910(Personality, Concurrency::DeferringAsyncTaskQueue &); @@ -91,10 +93,23 @@ class AY38910: public ::Outputs::Speaker::SampleSource { */ void set_port_handler(PortHandler *); + /*! + Enables or disables stereo output; if stereo output is enabled then also sets the weight of each of the AY's + channels in each of the output channels. + + If a_left_ = b_left = c_left = a_right = b_right = c_right = 1.0 then you'll get output that's effectively mono. + + a_left = 0.0, a_right = 1.0 will make A full volume on the right output, and silent on the left. + + a_left = 0.5, a_right = 0.5 will make A half volume on both outputs. + */ + 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_; @@ -135,14 +150,21 @@ class AY38910: public ::Outputs::Speaker::SampleSource { uint8_t data_input_, data_output_; - int16_t output_volume_; - void evaluate_output_volume(); + uint32_t output_volume_; void update_bus(); PortHandler *port_handler_ = nullptr; void set_port_output(bool port_b); + + void evaluate_output_volume(); + + // Output mixing control. + 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/Components/AudioToggle/AudioToggle.hpp b/Components/AudioToggle/AudioToggle.hpp index cd9d0e483..c009fe781 100644 --- a/Components/AudioToggle/AudioToggle.hpp +++ b/Components/AudioToggle/AudioToggle.hpp @@ -24,6 +24,7 @@ class Toggle: public Outputs::Speaker::SampleSource { void get_samples(std::size_t number_of_samples, std::int16_t *target); void set_sample_volume_range(std::int16_t range); void skip_samples(const std::size_t number_of_samples); + static constexpr bool get_is_stereo() { return false; } void set_output(bool enabled); bool get_output(); diff --git a/Components/KonamiSCC/KonamiSCC.hpp b/Components/KonamiSCC/KonamiSCC.hpp index 21828ed5a..e524614d4 100644 --- a/Components/KonamiSCC/KonamiSCC.hpp +++ b/Components/KonamiSCC/KonamiSCC.hpp @@ -32,6 +32,7 @@ class SCC: public ::Outputs::Speaker::SampleSource { /// As per ::SampleSource; provides audio output. void get_samples(std::size_t number_of_samples, std::int16_t *target); void set_sample_volume_range(std::int16_t range); + static constexpr bool get_is_stereo() { return false; } /// Writes to the SCC. void write(uint16_t address, uint8_t value); diff --git a/Components/SN76489/SN76489.hpp b/Components/SN76489/SN76489.hpp index dbc2e5cab..eafac818a 100644 --- a/Components/SN76489/SN76489.hpp +++ b/Components/SN76489/SN76489.hpp @@ -32,6 +32,7 @@ class SN76489: public Outputs::Speaker::SampleSource { void get_samples(std::size_t number_of_samples, std::int16_t *target); bool is_zero_level(); void set_sample_volume_range(std::int16_t range); + static constexpr bool get_is_stereo() { return false; } private: int master_divider_ = 0; diff --git a/Machines/AmstradCPC/AmstradCPC.cpp b/Machines/AmstradCPC/AmstradCPC.cpp index 1e319eae1..4a8ff2677 100644 --- a/Machines/AmstradCPC/AmstradCPC.cpp +++ b/Machines/AmstradCPC/AmstradCPC.cpp @@ -126,6 +126,9 @@ class AYDeferrer { /// Constructs a new AY instance and sets its clock rate. AYDeferrer() : ay_(GI::AY38910::Personality::AY38910, audio_queue_), speaker_(ay_) { speaker_.set_input_rate(1000000); + // Per the CPC Wiki: + // "A is output to the right, channel C is output left, and channel B is output to both left and right". + ay_.set_output_mixing(0.0, 0.5, 1.0, 1.0, 0.5, 0.0); } ~AYDeferrer() { @@ -153,14 +156,14 @@ class AYDeferrer { } /// @returns the AY itself. - GI::AY38910::AY38910 &ay() { + GI::AY38910::AY38910 &ay() { return ay_; } private: Concurrency::DeferringAsyncTaskQueue audio_queue_; - GI::AY38910::AY38910 ay_; - Outputs::Speaker::LowpassSpeaker speaker_; + GI::AY38910::AY38910 ay_; + Outputs::Speaker::LowpassSpeaker> speaker_; HalfCycles cycles_since_update_; }; 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/Atari/2600/TIASound.hpp b/Machines/Atari/2600/TIASound.hpp index ec41e40de..8bc84cc2a 100644 --- a/Machines/Atari/2600/TIASound.hpp +++ b/Machines/Atari/2600/TIASound.hpp @@ -29,6 +29,7 @@ class TIASound: public Outputs::Speaker::SampleSource { // To satisfy ::SampleSource. void get_samples(std::size_t number_of_samples, int16_t *target); void set_sample_volume_range(std::int16_t range); + static constexpr bool get_is_stereo() { return false; } private: Concurrency::DeferringAsyncTaskQueue &audio_queue_; diff --git a/Machines/Atari/ST/AtariST.cpp b/Machines/Atari/ST/AtariST.cpp index 9e65aa52c..c8056e9e1 100644 --- a/Machines/Atari/ST/AtariST.cpp +++ b/Machines/Atari/ST/AtariST.cpp @@ -497,8 +497,8 @@ class ConcreteMachine: JustInTimeActor midi_acia_; Concurrency::DeferringAsyncTaskQueue audio_queue_; - GI::AY38910::AY38910 ay_; - Outputs::Speaker::LowpassSpeaker speaker_; + GI::AY38910::AY38910 ay_; + Outputs::Speaker::LowpassSpeaker> speaker_; HalfCycles cycles_since_audio_update_; JustInTimeActor dma_; diff --git a/Machines/ColecoVision/ColecoVision.cpp b/Machines/ColecoVision/ColecoVision.cpp index 1a641d479..02aacc6df 100644 --- a/Machines/ColecoVision/ColecoVision.cpp +++ b/Machines/ColecoVision/ColecoVision.cpp @@ -405,9 +405,9 @@ class ConcreteMachine: Concurrency::DeferringAsyncTaskQueue audio_queue_; TI::SN76489 sn76489_; - GI::AY38910::AY38910 ay_; - Outputs::Speaker::CompoundSource mixer_; - Outputs::Speaker::LowpassSpeaker> speaker_; + GI::AY38910::AY38910 ay_; + Outputs::Speaker::CompoundSource> mixer_; + Outputs::Speaker::LowpassSpeaker>> speaker_; std::vector bios_; std::vector cartridge_; diff --git a/Machines/Electron/SoundGenerator.hpp b/Machines/Electron/SoundGenerator.hpp index 9df5769da..fff2ba15c 100644 --- a/Machines/Electron/SoundGenerator.hpp +++ b/Machines/Electron/SoundGenerator.hpp @@ -28,6 +28,7 @@ class SoundGenerator: 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_; diff --git a/Machines/MSX/MSX.cpp b/Machines/MSX/MSX.cpp index 79f8c532e..150f9eeb5 100644 --- a/Machines/MSX/MSX.cpp +++ b/Machines/MSX/MSX.cpp @@ -760,11 +760,11 @@ class ConcreteMachine: Intel::i8255::i8255 i8255_; Concurrency::DeferringAsyncTaskQueue audio_queue_; - GI::AY38910::AY38910 ay_; + GI::AY38910::AY38910 ay_; Audio::Toggle audio_toggle_; Konami::SCC scc_; - Outputs::Speaker::CompoundSource mixer_; - Outputs::Speaker::LowpassSpeaker> speaker_; + Outputs::Speaker::CompoundSource, Audio::Toggle, Konami::SCC> mixer_; + Outputs::Speaker::LowpassSpeaker, Audio::Toggle, Konami::SCC>> speaker_; Storage::Tape::BinaryTapePlayer tape_player_; bool tape_player_is_sleeping_ = false; diff --git a/Machines/Oric/Oric.cpp b/Machines/Oric/Oric.cpp index a7273d20f..b25162f6e 100644 --- a/Machines/Oric/Oric.cpp +++ b/Machines/Oric/Oric.cpp @@ -43,6 +43,8 @@ namespace Oric { using DiskInterface = Analyser::Static::Oric::Target::DiskInterface; +using AY = GI::AY38910::AY38910; +using Speaker = Outputs::Speaker::LowpassSpeaker; enum ROM { BASIC10 = 0, BASIC11, Microdisc, Colour @@ -146,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, Outputs::Speaker::LowpassSpeaker &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) {} /*! @@ -209,8 +211,8 @@ class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler { HalfCycles cycles_since_ay_update_; Concurrency::DeferringAsyncTaskQueue &audio_queue_; - GI::AY38910::AY38910 &ay8910_; - Outputs::Speaker::LowpassSpeaker &speaker_; + AY &ay8910_; + Speaker &speaker_; TapePlayer &tape_player_; Keyboard &keyboard_; }; @@ -691,8 +693,8 @@ template class Co VideoOutput video_output_; Concurrency::DeferringAsyncTaskQueue audio_queue_; - GI::AY38910::AY38910 ay8910_; - Outputs::Speaker::LowpassSpeaker speaker_; + GI::AY38910::AY38910 ay8910_; + Speaker speaker_; // Inputs Oric::KeyboardMapper keyboard_mapper_; diff --git a/Machines/ZX8081/ZX8081.cpp b/Machines/ZX8081/ZX8081.cpp index 8ea7a0781..16d1f1db5 100644 --- a/Machines/ZX8081/ZX8081.cpp +++ b/Machines/ZX8081/ZX8081.cpp @@ -467,8 +467,9 @@ template class ConcreteMachine: // MARK: - Audio Concurrency::DeferringAsyncTaskQueue audio_queue_; - GI::AY38910::AY38910 ay_; - Outputs::Speaker::LowpassSpeaker speaker_; + using AY = GI::AY38910::AY38910; + AY ay_; + Outputs::Speaker::LowpassSpeaker speaker_; HalfCycles time_since_ay_update_; inline void ay_set_register(uint8_t value) { update_audio(); diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal Kiosk.xcscheme b/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal Kiosk.xcscheme index 1d7e2ee72..3fddce824 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal Kiosk.xcscheme +++ b/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal Kiosk.xcscheme @@ -60,6 +60,10 @@ argument = ""/Users/thomasharte/Library/Mobile Documents/com~apple~CloudDocs/Desktop/Soft/Master System/R-Type (NTSC).sms"" isEnabled = "YES"> + + diff --git a/OSBindings/Mac/Clock Signal/Audio/CSAudioQueue.h b/OSBindings/Mac/Clock Signal/Audio/CSAudioQueue.h index aac792cb0..370bea589 100644 --- a/OSBindings/Mac/Clock Signal/Audio/CSAudioQueue.h +++ b/OSBindings/Mac/Clock Signal/Audio/CSAudioQueue.h @@ -25,10 +25,12 @@ Creates an instance of CSAudioQueue. @param samplingRate The output audio rate. + @param isStereo @c YES if audio buffers will contain stereo audio, @c NO otherwise. @returns An instance of CSAudioQueue if successful; @c nil otherwise. */ -- (nonnull instancetype)initWithSamplingRate:(Float64)samplingRate; +- (nonnull instancetype)initWithSamplingRate:(Float64)samplingRate isStereo:(BOOL)isStereo NS_DESIGNATED_INITIALIZER; +- (nonnull instancetype)init __attribute((unavailable)); /*! Enqueues a buffer for playback. diff --git a/OSBindings/Mac/Clock Signal/Audio/CSAudioQueue.m b/OSBindings/Mac/Clock Signal/Audio/CSAudioQueue.m index b2769381e..f65432cc6 100644 --- a/OSBindings/Mac/Clock Signal/Audio/CSAudioQueue.m +++ b/OSBindings/Mac/Clock Signal/Audio/CSAudioQueue.m @@ -73,7 +73,7 @@ static void audioOutputCallback( #pragma mark - Standard object lifecycle -- (instancetype)initWithSamplingRate:(Float64)samplingRate { +- (instancetype)initWithSamplingRate:(Float64)samplingRate isStereo:(BOOL)isStereo { self = [super init]; if(self) { @@ -98,10 +98,11 @@ static void audioOutputCallback( outputDescription.mFormatID = kAudioFormatLinearPCM; outputDescription.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger; - outputDescription.mBytesPerPacket = 2; + + outputDescription.mChannelsPerFrame = isStereo ? 2 : 1; outputDescription.mFramesPerPacket = 1; - outputDescription.mBytesPerFrame = 2; - outputDescription.mChannelsPerFrame = 1; + outputDescription.mBytesPerFrame = 2 * outputDescription.mChannelsPerFrame; + outputDescription.mBytesPerPacket = outputDescription.mBytesPerFrame * outputDescription.mFramesPerPacket; outputDescription.mBitsPerChannel = 16; outputDescription.mReserved = 0; @@ -123,10 +124,6 @@ static void audioOutputCallback( return self; } -- (instancetype)init { - return [self initWithSamplingRate:[[self class] preferredSamplingRate]]; -} - - (void)dealloc { [CSAudioQueueDeallocLock lock]; if(_audioQueue) { diff --git a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift index 54e85c1b0..327d447a8 100644 --- a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift +++ b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift @@ -228,11 +228,12 @@ class MachineDocument: // TODO: this needs to be threadsafe. FIX! let maximumSamplingRate = CSAudioQueue.preferredSamplingRate() let selectedSamplingRate = self.machine.idealSamplingRate(from: NSRange(location: 0, length: NSInteger(maximumSamplingRate))) + let isStereo = self.machine.isStereo() if selectedSamplingRate > 0 { - self.audioQueue = CSAudioQueue(samplingRate: Float64(selectedSamplingRate)) + self.audioQueue = CSAudioQueue(samplingRate: Float64(selectedSamplingRate), isStereo:isStereo) self.audioQueue.delegate = self self.machine.audioQueue = self.audioQueue - self.machine.setAudioSamplingRate(selectedSamplingRate, bufferSize:audioQueue.preferredBufferSize) + self.machine.setAudioSamplingRate(selectedSamplingRate, bufferSize:audioQueue.preferredBufferSize, stereo:isStereo) } } diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.h b/OSBindings/Mac/Clock Signal/Machine/CSMachine.h index 844f20243..dee3fff39 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.h +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.h @@ -58,7 +58,8 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) { - (nullable instancetype)initWithAnalyser:(nonnull CSStaticAnalyser *)result missingROMs:(nullable inout NSMutableArray *)missingROMs NS_DESIGNATED_INITIALIZER; - (float)idealSamplingRateFromRange:(NSRange)range; -- (void)setAudioSamplingRate:(float)samplingRate bufferSize:(NSUInteger)bufferSize; +- (BOOL)isStereo; +- (void)setAudioSamplingRate:(float)samplingRate bufferSize:(NSUInteger)bufferSize stereo:(BOOL)stereo; - (void)setView:(nullable CSOpenGLView *)view aspectRatio:(float)aspectRatio; diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm index 58705ecb2..a671a87b7 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm @@ -262,17 +262,27 @@ struct ActivityObserver: public Activity::Observer { } } -- (void)setAudioSamplingRate:(float)samplingRate bufferSize:(NSUInteger)bufferSize { - @synchronized(self) { - [self setSpeakerDelegate:&_speakerDelegate sampleRate:samplingRate bufferSize:bufferSize]; - } -} - -- (BOOL)setSpeakerDelegate:(Outputs::Speaker::Speaker::Delegate *)delegate sampleRate:(float)sampleRate bufferSize:(NSUInteger)bufferSize { +- (BOOL)isStereo { @synchronized(self) { Outputs::Speaker::Speaker *speaker = _machine->crt_machine()->get_speaker(); if(speaker) { - speaker->set_output_rate(sampleRate, (int)bufferSize); + return speaker->get_is_stereo(); + } + return NO; + } +} + +- (void)setAudioSamplingRate:(float)samplingRate bufferSize:(NSUInteger)bufferSize stereo:(BOOL)stereo { + @synchronized(self) { + [self setSpeakerDelegate:&_speakerDelegate sampleRate:samplingRate bufferSize:bufferSize stereo:stereo]; + } +} + +- (BOOL)setSpeakerDelegate:(Outputs::Speaker::Speaker::Delegate *)delegate sampleRate:(float)sampleRate bufferSize:(NSUInteger)bufferSize stereo:(BOOL)stereo { + @synchronized(self) { + Outputs::Speaker::Speaker *speaker = _machine->crt_machine()->get_speaker(); + if(speaker) { + speaker->set_output_rate(sampleRate, (int)bufferSize, stereo); speaker->set_delegate(delegate); return YES; } diff --git a/OSBindings/SDL/main.cpp b/OSBindings/SDL/main.cpp index dee03b587..dec48042a 100644 --- a/OSBindings/SDL/main.cpp +++ b/OSBindings/SDL/main.cpp @@ -168,10 +168,12 @@ struct MachineRunner { struct SpeakerDelegate: public Outputs::Speaker::Speaker::Delegate { // This is empirically the best that I can seem to do with SDL's timer precision. - static constexpr int buffer_size = 1024; + static constexpr size_t buffered_samples = 1024; + bool is_stereo = false; void speaker_did_complete_samples(Outputs::Speaker::Speaker *speaker, const std::vector &buffer) final { std::lock_guard lock_guard(audio_buffer_mutex_); + const size_t buffer_size = buffered_samples * (is_stereo ? 2 : 1); if(audio_buffer_.size() > buffer_size) { audio_buffer_.erase(audio_buffer_.begin(), audio_buffer_.end() - buffer_size); } @@ -181,9 +183,10 @@ struct SpeakerDelegate: public Outputs::Speaker::Speaker::Delegate { void audio_callback(Uint8 *stream, int len) { std::lock_guard lock_guard(audio_buffer_mutex_); - std::size_t sample_length = static_cast(len) / sizeof(int16_t); - std::size_t copy_length = std::min(sample_length, audio_buffer_.size()); - int16_t *target = static_cast(static_cast(stream)); + // SDL buffer length is in bytes, so there's no need to adjust for stereo/mono in here. + const std::size_t sample_length = static_cast(len) / sizeof(int16_t); + const std::size_t copy_length = std::min(sample_length, audio_buffer_.size()); + int16_t *const target = static_cast(static_cast(stream)); std::memcpy(stream, audio_buffer_.data(), copy_length * sizeof(int16_t)); if(copy_length < sample_length) { @@ -660,14 +663,15 @@ int main(int argc, char *argv[]) { SDL_zero(desired_audio_spec); desired_audio_spec.freq = 48000; // TODO: how can I get SDL to reveal the output rate of this machine? desired_audio_spec.format = AUDIO_S16; - desired_audio_spec.channels = 1; - desired_audio_spec.samples = SpeakerDelegate::buffer_size; + desired_audio_spec.channels = 1 + int(speaker->get_is_stereo()); + desired_audio_spec.samples = Uint16(SpeakerDelegate::buffered_samples); desired_audio_spec.callback = SpeakerDelegate::SDL_audio_callback; desired_audio_spec.userdata = &speaker_delegate; speaker_delegate.audio_device = SDL_OpenAudioDevice(nullptr, 0, &desired_audio_spec, &obtained_audio_spec, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE); - speaker->set_output_rate(obtained_audio_spec.freq, desired_audio_spec.samples); + speaker->set_output_rate(obtained_audio_spec.freq, desired_audio_spec.samples, obtained_audio_spec.channels == 2); + speaker_delegate.is_stereo = obtained_audio_spec.channels == 2; speaker->set_delegate(&speaker_delegate); SDL_PauseAudioDevice(speaker_delegate.audio_device, 0); } @@ -865,23 +869,22 @@ int main(int argc, char *argv[]) { SDL_FreeSurface(surface); break; } + } + // Syphon off alt+enter (toggle full-screen) upon key up only; this was previously a key down action, + // but the SDL_KEYDOWN announcement was found to be reposted after changing graphics mode on some + // systems, causing a loop of changes, so key up is safer. + if(event.type == SDL_KEYUP && event.key.keysym.sym == SDLK_RETURN && (SDL_GetModState()&KMOD_ALT)) { + fullscreen_mode ^= SDL_WINDOW_FULLSCREEN_DESKTOP; + SDL_SetWindowFullscreen(window, fullscreen_mode); + SDL_ShowCursor((fullscreen_mode&SDL_WINDOW_FULLSCREEN_DESKTOP) ? SDL_DISABLE : SDL_ENABLE); - // Syphon off alt+enter (toggle full-screen) upon key up only; this was previously a key down action, - // but the SDL_KEYDOWN announcement was found to be reposted after changing graphics mode on some - // systems so key up is safer. - if(event.type == SDL_KEYUP && event.key.keysym.sym == SDLK_RETURN && (SDL_GetModState()&KMOD_ALT)) { - fullscreen_mode ^= SDL_WINDOW_FULLSCREEN_DESKTOP; - SDL_SetWindowFullscreen(window, fullscreen_mode); - SDL_ShowCursor((fullscreen_mode&SDL_WINDOW_FULLSCREEN_DESKTOP) ? SDL_DISABLE : SDL_ENABLE); - - // Announce a potential discontinuity in keyboard input. - const auto keyboard_machine = machine->keyboard_machine(); - if(keyboard_machine) { - keyboard_machine->get_keyboard().reset_all_keys(); - } - break; + // Announce a potential discontinuity in keyboard input. + const auto keyboard_machine = machine->keyboard_machine(); + if(keyboard_machine) { + keyboard_machine->get_keyboard().reset_all_keys(); } + break; } const bool is_pressed = event.type == SDL_KEYDOWN; diff --git a/Outputs/Speaker/Implementation/CompoundSource.hpp b/Outputs/Speaker/Implementation/CompoundSource.hpp index bafed9986..8a6c8f1d9 100644 --- a/Outputs/Speaker/Implementation/CompoundSource.hpp +++ b/Outputs/Speaker/Implementation/CompoundSource.hpp @@ -33,7 +33,7 @@ template class CompoundSource: } void get_samples(std::size_t number_of_samples, std::int16_t *target) { - source_holder_.get_samples(number_of_samples, target); + source_holder_.template get_samples(number_of_samples, target); } void skip_samples(const std::size_t number_of_samples) { @@ -57,6 +57,8 @@ template class CompoundSource: push_volumes(); } + static constexpr bool get_is_stereo() { return CompoundSourceHolder::get_is_stereo(); } + private: void push_volumes() { source_holder_.set_scaled_volume_range(volume_range_, volumes_.data()); @@ -64,7 +66,7 @@ template class CompoundSource: template class CompoundSourceHolder: public Outputs::Speaker::SampleSource { public: - void get_samples(std::size_t number_of_samples, std::int16_t *target) { + template void get_samples(std::size_t number_of_samples, std::int16_t *target) { std::memset(target, 0, sizeof(std::int16_t) * number_of_samples); } @@ -73,24 +75,49 @@ template class CompoundSource: std::size_t size() { return 0; } + + static constexpr bool get_is_stereo() { + return false; + } }; template class CompoundSourceHolder { public: CompoundSourceHolder(S &source, R &...next) : source_(source), next_source_(next...) {} - void get_samples(std::size_t number_of_samples, std::int16_t *target) { + template void get_samples(std::size_t number_of_samples, std::int16_t *target) { + // Get the rest of the output. + next_source_.template get_samples(number_of_samples, target); + if(source_.is_zero_level()) { + // This component is currently outputting silence; therefore don't add anything to the output + // audio — just pass the call onward. source_.skip_samples(number_of_samples); - next_source_.get_samples(number_of_samples, target); + return; + } + + // Get this component's output. + auto buffer_size = number_of_samples * (output_stereo ? 2 : 1); + int16_t local_samples[buffer_size]; + source_.get_samples(number_of_samples, local_samples); + + // Merge it in; furthermore if total output is stereo but this source isn't, + // map it to stereo. + if constexpr (output_stereo == S::get_is_stereo()) { + while(buffer_size--) { + target[buffer_size] += local_samples[buffer_size]; + } } else { - int16_t next_samples[number_of_samples]; - next_source_.get_samples(number_of_samples, next_samples); - source_.get_samples(number_of_samples, target); - while(number_of_samples--) { - target[number_of_samples] += next_samples[number_of_samples]; + // This will happen only if mapping from mono to stereo, never in the + // other direction, because the compound source outputs stereo if any + // subcomponent does. So it outputs mono only if no stereo devices are + // in the mixing chain. + while(buffer_size--) { + target[buffer_size] += local_samples[buffer_size >> 1]; } } + + // TODO: accelerate above? } void skip_samples(const std::size_t number_of_samples) { @@ -107,6 +134,10 @@ template class CompoundSource: return 1+next_source_.size(); } + static constexpr bool get_is_stereo() { + return S::get_is_stereo() || CompoundSourceHolder::get_is_stereo(); + } + private: S &source_; CompoundSourceHolder next_source_; diff --git a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp index da1cc692b..44f76e506 100644 --- a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp +++ b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp @@ -28,9 +28,9 @@ namespace Speaker { source of a high-frequency stream of audio which it filters down to a lower-frequency output. */ -template class LowpassSpeaker: public Speaker { +template class LowpassSpeaker: public Speaker { public: - LowpassSpeaker(T &sample_source) : sample_source_(sample_source) { + LowpassSpeaker(SampleSource &sample_source) : sample_source_(sample_source) { sample_source.set_sample_volume_range(32767); } @@ -58,7 +58,7 @@ template class LowpassSpeaker: public Speaker { } // Implemented as per Speaker. - void set_computed_output_rate(float cycles_per_second, int buffer_size) final { + void set_computed_output_rate(float cycles_per_second, int buffer_size, bool stereo) final { std::lock_guard lock_guard(filter_parameters_mutex_); if(filter_parameters_.output_cycles_per_second == cycles_per_second && size_t(buffer_size) == output_buffer_.size()) { return; @@ -66,7 +66,11 @@ template class LowpassSpeaker: public Speaker { filter_parameters_.output_cycles_per_second = cycles_per_second; filter_parameters_.parameters_are_dirty = true; - output_buffer_.resize(std::size_t(buffer_size)); + output_buffer_.resize(std::size_t(buffer_size) * (SampleSource::get_is_stereo() ? 2 : 1)); + } + + bool get_is_stereo() final { + return SampleSource::get_is_stereo(); } /*! @@ -140,15 +144,14 @@ template class LowpassSpeaker: public Speaker { switch(conversion_) { case Conversion::Copy: while(cycles_remaining) { - const auto cycles_to_read = std::min(output_buffer_.size() - output_buffer_pointer_, 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 * (SampleSource::get_is_stereo() ? 2 : 1); - sample_source_.get_samples(cycles_to_read, &output_buffer_[output_buffer_pointer_]); - output_buffer_pointer_ += cycles_to_read; - - // announce to delegate if full + // Announce to delegate if full. if(output_buffer_pointer_ == output_buffer_.size()) { output_buffer_pointer_ = 0; - did_complete_samples(this, output_buffer_); + did_complete_samples(this, output_buffer_, SampleSource::get_is_stereo()); } cycles_remaining -= cycles_to_read; @@ -157,14 +160,16 @@ template class LowpassSpeaker: public Speaker { case Conversion::ResampleSmaller: while(cycles_remaining) { - const auto cycles_to_read = std::min(cycles_remaining, input_buffer_.size() - input_buffer_depth_); + 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_]); - cycles_remaining -= cycles_to_read; - input_buffer_depth_ += cycles_to_read; + input_buffer_depth_ += cycles_to_read * (SampleSource::get_is_stereo() ? 2 : 1); if(input_buffer_depth_ == input_buffer_.size()) { resample_input_buffer(); } + + cycles_remaining -= cycles_to_read; } break; @@ -174,7 +179,7 @@ template class LowpassSpeaker: public Speaker { } } - T &sample_source_; + SampleSource &sample_source_; std::size_t output_buffer_pointer_ = 0; std::size_t input_buffer_depth_ = 0; @@ -239,35 +244,42 @@ template class LowpassSpeaker: public Speaker { // that means nothing to do. default: break; - case Conversion::ResampleSmaller: + case Conversion::ResampleSmaller: { // 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. - if(input_buffer_.size() != size_t(number_of_taps)) { - if(input_buffer_depth_ >= size_t(number_of_taps)) { + 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(); - input_buffer_depth_ %= size_t(number_of_taps); + input_buffer_depth_ %= required_buffer_size; } - input_buffer_.resize(size_t(number_of_taps)); + input_buffer_.resize(required_buffer_size); } - break; + } break; } } inline void resample_input_buffer() { - output_buffer_[output_buffer_pointer_] = filter_->apply(input_buffer_.data()); - output_buffer_pointer_++; + 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; + } else { + output_buffer_[output_buffer_pointer_] = filter_->apply(input_buffer_.data()); + output_buffer_pointer_++; + } // Announce to delegate if full. if(output_buffer_pointer_ == output_buffer_.size()) { output_buffer_pointer_ = 0; - did_complete_samples(this, output_buffer_); + 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(); + 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, @@ -275,8 +287,9 @@ template class LowpassSpeaker: public Speaker { sizeof(int16_t) * (input_buffer_.size() - steps)); input_buffer_depth_ -= steps; } else { - if(steps > input_buffer_.size()) - sample_source_.skip_samples(steps - input_buffer_.size()); + if(steps > input_buffer_.size()) { + sample_source_.skip_samples((steps - input_buffer_.size()) / (SampleSource::get_is_stereo() ? 2 : 1)); + } input_buffer_depth_ = 0; } } diff --git a/Outputs/Speaker/Speaker.hpp b/Outputs/Speaker/Speaker.hpp index 941621729..f32a5ee5f 100644 --- a/Outputs/Speaker/Speaker.hpp +++ b/Outputs/Speaker/Speaker.hpp @@ -23,33 +23,90 @@ class Speaker { public: virtual ~Speaker() {} + /*! + @returns The best output clock rate for the audio being supplied to this speaker, from the range given. + */ virtual float get_ideal_clock_rate_in_range(float minimum, float maximum) = 0; - void set_output_rate(float cycles_per_second, int buffer_size) { + + /*! + @returns @c true if the device would most ideally output stereo sound; @c false otherwise. + */ + virtual bool get_is_stereo() = 0; + + /*! + Sets the actual output rate; packets provided to the delegate will conform to these + specifications regardless of the input. + */ + void set_output_rate(float cycles_per_second, int buffer_size, bool stereo) { output_cycles_per_second_ = cycles_per_second; output_buffer_size_ = buffer_size; + stereo_output_ = stereo; compute_output_rate(); } + + /*! + Speeds a speed multiplier for this machine, e.g. that it is currently being run at 2.0x its normal rate. + This will affect the number of input samples that are combined to produce one output sample. + */ void set_input_rate_multiplier(float multiplier) { input_rate_multiplier_ = multiplier; compute_output_rate(); } + /*! + @returns The number of sample sets so far delivered to the delegate. + */ int completed_sample_sets() const { return completed_sample_sets_; } + /*! + Defines a receiver for audio packets. + */ struct Delegate { + /*! + Indicates that a new audio packet is ready. If the output is stereo, samples will be interleaved with the first + being left, the second being right, etc. + */ virtual void speaker_did_complete_samples(Speaker *speaker, const std::vector &buffer) = 0; + + /*! + Provides the delegate with a hint that the input clock rate has changed, which provides an opportunity to + renegotiate the ideal clock rate, if desired. + */ virtual void speaker_did_change_input_clock(Speaker *speaker) {} }; virtual void set_delegate(Delegate *delegate) { delegate_ = delegate; } - virtual void set_computed_output_rate(float cycles_per_second, int buffer_size) = 0; + virtual void set_computed_output_rate(float cycles_per_second, int buffer_size, bool stereo) = 0; protected: - void did_complete_samples(Speaker *speaker, const std::vector &buffer) { + void did_complete_samples(Speaker *speaker, const std::vector &buffer, bool is_stereo) { ++completed_sample_sets_; - delegate_->speaker_did_complete_samples(this, buffer); + + // Hope for the fast path first: producer and consumer agree about + // number of channels. + if(is_stereo == stereo_output_) { + delegate_->speaker_did_complete_samples(this, buffer); + return; + } + + // Producer and consumer don't agree, so mix two channels to one, or double out one to two. + if(is_stereo) { + // Mix down. + mix_buffer_.resize(buffer.size() / 2); + for(size_t c = 0; c < mix_buffer_.size(); ++c) { + mix_buffer_[c] = (buffer[(c << 1) + 0] + buffer[(c << 1) + 1]) >> 1; + // TODO: is there an Accelerate framework solution to this? + } + } else { + // Double up. + mix_buffer_.resize(buffer.size() * 2); + for(size_t c = 0; c < buffer.size(); ++c) { + mix_buffer_[(c << 1) + 0] = mix_buffer_[(c << 1) + 1] = buffer[c]; + } + } + delegate_->speaker_did_complete_samples(this, mix_buffer_); } Delegate *delegate_ = nullptr; @@ -57,13 +114,15 @@ class Speaker { void compute_output_rate() { // The input rate multiplier is actually used as an output rate divider, // to confirm to the public interface of a generic speaker being output-centric. - set_computed_output_rate(output_cycles_per_second_ / input_rate_multiplier_, output_buffer_size_); + set_computed_output_rate(output_cycles_per_second_ / input_rate_multiplier_, output_buffer_size_, stereo_output_); } int completed_sample_sets_ = 0; float input_rate_multiplier_ = 1.0f; float output_cycles_per_second_ = 1.0f; int output_buffer_size_ = 1; + bool stereo_output_ = false; + std::vector mix_buffer_; }; } diff --git a/SignalProcessing/FIRFilter.hpp b/SignalProcessing/FIRFilter.hpp index 8ee2d09bd..23f7fd3ca 100644 --- a/SignalProcessing/FIRFilter.hpp +++ b/SignalProcessing/FIRFilter.hpp @@ -11,6 +11,7 @@ #ifdef __APPLE__ #include +#define USE_ACCELERATE #endif #include @@ -49,15 +50,15 @@ class FIRFilter { @param src The source buffer to apply the filter to. @returns The result of applying the filter. */ - inline short apply(const short *src) const { - #ifdef __APPLE__ + inline short apply(const short *src, size_t stride = 1) const { + #ifdef USE_ACCELERATE short result; - vDSP_dotpr_s1_15(filter_coefficients_.data(), 1, src, 1, &result, filter_coefficients_.size()); + vDSP_dotpr_s1_15(filter_coefficients_.data(), 1, src, vDSP_Stride(stride), &result, filter_coefficients_.size()); return result; #else int outputValue = 0; for(std::size_t c = 0; c < filter_coefficients_.size(); ++c) { - outputValue += filter_coefficients_[c] * src[c]; + outputValue += filter_coefficients_[c] * src[c * stride]; } return static_cast(outputValue >> FixedShift); #endif