1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-11-23 21:17:42 +00:00

Compare commits

...

8 Commits

Author SHA1 Message Date
Thomas Harte
4be5ee5b35 Fix value interactions. 2025-11-17 23:25:47 -05:00
Thomas Harte
92e6dc64d4 Merge branch 'master' into QueueDelegate 2025-11-17 23:21:24 -05:00
Thomas Harte
f422cda553 Adapt the Enterprise, accepting possible need for HalfCycles. 2025-11-16 08:04:47 -05:00
Thomas Harte
2c44d3a7d3 Adapt the Plus 4. 2025-11-15 22:31:17 -05:00
Thomas Harte
051ce98ecb Adapt Vic-20. 2025-11-15 22:18:46 -05:00
Thomas Harte
33ae24c961 Attempt to shrink repetition even further. 2025-11-15 21:41:34 -05:00
Thomas Harte
4247d0ef40 Adapt Atari 2600. 2025-11-14 22:58:41 -05:00
Thomas Harte
ffababdb45 With the Electron as a test bed, start to simplify audio class groups. 2025-11-14 22:39:53 -05:00
22 changed files with 303 additions and 194 deletions

View File

@@ -12,7 +12,7 @@
using namespace MOS::MOS6560; using namespace MOS::MOS6560;
AudioGenerator::AudioGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue) : AudioGenerator::AudioGenerator(Outputs::Speaker::TaskQueue &audio_queue) :
audio_queue_(audio_queue) {} audio_queue_(audio_queue) {}
void AudioGenerator::set_volume(const uint8_t volume) { void AudioGenerator::set_volume(const uint8_t volume) {

View File

@@ -19,7 +19,7 @@ namespace MOS::MOS6560 {
// audio state // audio state
class AudioGenerator: public Outputs::Speaker::BufferSource<AudioGenerator, false> { class AudioGenerator: public Outputs::Speaker::BufferSource<AudioGenerator, false> {
public: public:
AudioGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue); AudioGenerator(Outputs::Speaker::TaskQueue &audio_queue);
void set_volume(uint8_t); void set_volume(uint8_t);
void set_control(int channel, uint8_t value); void set_control(int channel, uint8_t value);
@@ -30,7 +30,7 @@ public:
void set_sample_volume_range(std::int16_t); void set_sample_volume_range(std::int16_t);
private: private:
Concurrency::AsyncTaskQueue<false> &audio_queue_; Outputs::Speaker::TaskQueue &audio_queue_;
unsigned int counters_[4] = {2, 1, 0, 0}; // create a slight phase offset for the three channels unsigned int counters_[4] = {2, 1, 0, 0}; // create a slight phase offset for the three channels
unsigned int shift_registers_[4] = {0, 0, 0, 0}; unsigned int shift_registers_[4] = {0, 0, 0, 0};
@@ -64,8 +64,7 @@ public:
MOS6560(BusHandler &bus_handler) : MOS6560(BusHandler &bus_handler) :
bus_handler_(bus_handler), bus_handler_(bus_handler),
crt_(65*4, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Luminance8Phase8), crt_(65*4, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Luminance8Phase8),
audio_generator_(audio_queue_), audio_(Cycles(4))
speaker_(audio_generator_)
{ {
// default to s-video output // default to s-video output
crt_.set_display_type(Outputs::Display::DisplayType::SVideo); crt_.set_display_type(Outputs::Display::DisplayType::SVideo);
@@ -75,11 +74,11 @@ public:
} }
~MOS6560() { ~MOS6560() {
audio_queue_.lock_flush(); audio_.stop();
} }
void set_clock_rate(const double clock_rate) { void set_clock_rate(const double clock_rate) {
speaker_.set_input_rate(float(clock_rate / 4.0)); audio_.speaker().set_input_rate(float(clock_rate / 4.0));
} }
void set_scan_target(Outputs::Display::ScanTarget *const scan_target) { void set_scan_target(Outputs::Display::ScanTarget *const scan_target) {
@@ -95,11 +94,11 @@ public:
return crt_.get_display_type(); return crt_.get_display_type();
} }
Outputs::Speaker::Speaker *get_speaker() { Outputs::Speaker::Speaker *get_speaker() {
return &speaker_; return &audio_.speaker();
} }
void set_high_frequency_cutoff(const float cutoff) { void set_high_frequency_cutoff(const float cutoff) {
speaker_.set_high_frequency_cutoff(cutoff); audio_.speaker().set_high_frequency_cutoff(cutoff);
} }
/*! /*!
@@ -180,7 +179,7 @@ public:
*/ */
inline void run_for(const Cycles cycles) { inline void run_for(const Cycles cycles) {
// keep track of the amount of time since the speaker was updated; lazy updates are applied // keep track of the amount of time since the speaker was updated; lazy updates are applied
cycles_since_speaker_update_ += cycles; audio_ += cycles;
auto number_of_cycles = cycles.as_integral(); auto number_of_cycles = cycles.as_integral();
while(number_of_cycles--) { while(number_of_cycles--) {
@@ -377,8 +376,7 @@ public:
Causes the 6560 to flush as much pending CRT and speaker communications as possible. Causes the 6560 to flush as much pending CRT and speaker communications as possible.
*/ */
inline void flush() { inline void flush() {
update_audio(); audio_.perform();
audio_queue_.perform();
} }
/*! /*!
@@ -420,14 +418,12 @@ public:
case 0xb: case 0xb:
case 0xc: case 0xc:
case 0xd: case 0xd:
update_audio(); audio_->set_control(address - 0xa, value);
audio_generator_.set_control(address - 0xa, value);
break; break;
case 0xe: case 0xe:
update_audio();
registers_.auxiliary_colour = colours_[value >> 4]; registers_.auxiliary_colour = colours_[value >> 4];
audio_generator_.set_volume(value & 0xf); audio_->set_volume(value & 0xf);
break; break;
case 0xf: { case 0xf: {
@@ -467,14 +463,7 @@ private:
BusHandler &bus_handler_; BusHandler &bus_handler_;
Outputs::CRT::CRT crt_; Outputs::CRT::CRT crt_;
Concurrency::AsyncTaskQueue<false> audio_queue_; Outputs::Speaker::PullLowpassSpeakerQueue<Cycles, AudioGenerator> audio_;
AudioGenerator audio_generator_;
Outputs::Speaker::PullLowpass<AudioGenerator> speaker_;
Cycles cycles_since_speaker_update_;
void update_audio() {
speaker_.run_for(audio_queue_, Cycles(cycles_since_speaker_update_.divide(Cycles(4))));
}
// register state // register state
struct { struct {

View File

@@ -40,11 +40,14 @@ private:
/// An implementation detail; provides a no-op implementation of time advances for TaskQueues without a Performer. /// An implementation detail; provides a no-op implementation of time advances for TaskQueues without a Performer.
template <> struct TaskQueueStorage<void> { template <> struct TaskQueueStorage<void> {
TaskQueueStorage() {} TaskQueueStorage() {}
protected: protected:
void update() {} void update() {}
}; };
struct EnqueueDelegate {
virtual std::function<void(void)> prepare_enqueue() = 0;
};
/*! /*!
A task queue allows a caller to enqueue @c void(void) functions. Those functions are guaranteed A task queue allows a caller to enqueue @c void(void) functions. Those functions are guaranteed
to be performed serially and asynchronously from the caller. to be performed serially and asynchronously from the caller.
@@ -67,10 +70,15 @@ template <> struct TaskQueueStorage<void> {
template < template <
bool perform_automatically, bool perform_automatically,
bool start_immediately = true, bool start_immediately = true,
bool use_enqueue_delegate = false,
typename Performer = void typename Performer = void
> >
class AsyncTaskQueue: public TaskQueueStorage<Performer> { class AsyncTaskQueue: public TaskQueueStorage<Performer> {
public: public:
void set_enqueue_delegate(EnqueueDelegate *const delegate) {
enqueue_delegate_ = delegate;
}
template <typename... Args> AsyncTaskQueue(Args&&... args) : template <typename... Args> AsyncTaskQueue(Args&&... args) :
TaskQueueStorage<Performer>(std::forward<Args>(args)...) { TaskQueueStorage<Performer>(std::forward<Args>(args)...) {
if constexpr (start_immediately) { if constexpr (start_immediately) {
@@ -90,6 +98,9 @@ public:
/// to 'now'. /// to 'now'.
void enqueue(const std::function<void(void)> &post_action) { void enqueue(const std::function<void(void)> &post_action) {
const std::lock_guard guard(condition_mutex_); const std::lock_guard guard(condition_mutex_);
if constexpr (use_enqueue_delegate) {
actions_.push_back(enqueue_delegate_->prepare_enqueue());
}
actions_.push_back(post_action); actions_.push_back(post_action);
if constexpr (perform_automatically) { if constexpr (perform_automatically) {
@@ -206,6 +217,8 @@ private:
}; };
} }
EnqueueDelegate *enqueue_delegate_ = nullptr;
// The list of actions waiting be performed. These will be elided, // The list of actions waiting be performed. These will be elided,
// increasing their latency, if the emulation thread falls behind. // increasing their latency, if the emulation thread falls behind.
using ActionVector = std::vector<std::function<void(void)>>; using ActionVector = std::vector<std::function<void(void)>>;

View File

@@ -15,7 +15,6 @@
#include "ClockReceiver/ClockReceiver.hpp" #include "ClockReceiver/ClockReceiver.hpp"
#include "ClockReceiver/ForceInline.hpp" #include "ClockReceiver/ForceInline.hpp"
#include "Configurable/StandardOptions.hpp" #include "Configurable/StandardOptions.hpp"
#include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#include "Processors/6502/6502.hpp" #include "Processors/6502/6502.hpp"
#include "Storage/MassStorage/SCSI/SCSI.hpp" #include "Storage/MassStorage/SCSI/SCSI.hpp"
@@ -27,6 +26,8 @@
#include "ClockReceiver/JustInTime.hpp" #include "ClockReceiver/JustInTime.hpp"
#include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#include "Interrupts.hpp" #include "Interrupts.hpp"
#include "Keyboard.hpp" #include "Keyboard.hpp"
#include "Plus3.hpp" #include "Plus3.hpp"
@@ -58,8 +59,11 @@ public:
hard_drive_(scsi_bus_, 0), hard_drive_(scsi_bus_, 0),
scsi_device_(scsi_bus_.add_device()), scsi_device_(scsi_bus_.add_device()),
video_(ram_), video_(ram_),
sound_generator_(audio_queue_), audio_(
speaker_(sound_generator_) { 2000000.0 / SoundGenerator::clock_rate_divider,
SoundGenerator::clock_rate_divider,
6000.0f
) {
memset(key_states_, 0, sizeof(key_states_)); memset(key_states_, 0, sizeof(key_states_));
for(int c = 0; c < 16; c++) for(int c = 0; c < 16; c++)
memset(roms_[c], 0xff, 16384); memset(roms_[c], 0xff, 16384);
@@ -67,9 +71,6 @@ public:
tape_.set_delegate(this); tape_.set_delegate(this);
set_clock_rate(2000000); set_clock_rate(2000000);
speaker_.set_input_rate(2000000 / SoundGenerator::clock_rate_divider);
speaker_.set_high_frequency_cutoff(6000);
::ROM::Request request = ::ROM::Request(::ROM::Name::AcornBASICII) && ::ROM::Request(::ROM::Name::AcornElectronMOS100); ::ROM::Request request = ::ROM::Request(::ROM::Name::AcornBASICII) && ::ROM::Request(::ROM::Name::AcornElectronMOS100);
if(target.has_pres_adfs) { if(target.has_pres_adfs) {
request = request && ::ROM::Request(::ROM::Name::PRESADFSSlot1) && ::ROM::Request(::ROM::Name::PRESADFSSlot2); request = request && ::ROM::Request(::ROM::Name::PRESADFSSlot1) && ::ROM::Request(::ROM::Name::PRESADFSSlot2);
@@ -143,7 +144,7 @@ public:
} }
~ConcreteMachine() { ~ConcreteMachine() {
audio_queue_.lock_flush(); audio_.stop();
} }
void set_key_state(uint16_t key, bool isPressed) final { void set_key_state(uint16_t key, bool isPressed) final {
@@ -234,8 +235,7 @@ public:
const auto [cycles, video_interrupts] = run_for_access(address); const auto [cycles, video_interrupts] = run_for_access(address);
signal_interrupt(video_interrupts); signal_interrupt(video_interrupts);
cycles_since_audio_update_ += cycles; audio_ += cycles;
if(cycles_since_audio_update_ > Cycles(16384)) update_audio();
tape_.run_for(cycles); tape_.run_for(cycles);
if(typer_) typer_->run_for(cycles); if(typer_) typer_->run_for(cycles);
@@ -278,8 +278,7 @@ public:
// update speaker mode // update speaker mode
bool new_speaker_is_enabled = (*value & 6) == 2; bool new_speaker_is_enabled = (*value & 6) == 2;
if(new_speaker_is_enabled != speaker_is_enabled_) { if(new_speaker_is_enabled != speaker_is_enabled_) {
update_audio(); audio_->set_is_enabled(new_speaker_is_enabled);
sound_generator_.set_is_enabled(new_speaker_is_enabled);
speaker_is_enabled_ = new_speaker_is_enabled; speaker_is_enabled_ = new_speaker_is_enabled;
} }
@@ -340,8 +339,7 @@ public:
break; break;
case 0xfe06: case 0xfe06:
if(!is_read(operation)) { if(!is_read(operation)) {
update_audio(); audio_->set_divider(*value);
sound_generator_.set_divider(*value);
tape_.set_counter(*value); tape_.set_counter(*value);
} }
break; break;
@@ -510,8 +508,7 @@ public:
void flush_output(int outputs) final { void flush_output(int outputs) final {
if(outputs & Output::Audio) { if(outputs & Output::Audio) {
update_audio(); audio_.perform();
audio_queue_.perform();
} }
} }
@@ -532,7 +529,7 @@ public:
} }
Outputs::Speaker::Speaker *get_speaker() final { Outputs::Speaker::Speaker *get_speaker() final {
return &speaker_; return &audio_.speaker();
} }
void run_for(const Cycles cycles) final { void run_for(const Cycles cycles) final {
@@ -682,10 +679,6 @@ private:
} }
// MARK: - Work deferral updates. // MARK: - Work deferral updates.
inline void update_audio() {
speaker_.run_for(audio_queue_, cycles_since_audio_update_.divide(Cycles(SoundGenerator::clock_rate_divider)));
}
inline void signal_interrupt(uint8_t interrupt) { inline void signal_interrupt(uint8_t interrupt) {
if(!interrupt) { if(!interrupt) {
return; return;
@@ -732,9 +725,6 @@ private:
uint8_t key_states_[14]; uint8_t key_states_[14];
Electron::KeyboardMapper keyboard_mapper_; Electron::KeyboardMapper keyboard_mapper_;
// Counters related to simultaneous subsystems
Cycles cycles_since_audio_update_ = 0;
// Tape // Tape
Tape tape_; Tape tape_;
bool use_fast_tape_hack_ = false; bool use_fast_tape_hack_ = false;
@@ -770,10 +760,7 @@ private:
// Outputs // Outputs
VideoOutput video_; VideoOutput video_;
Outputs::Speaker::PullLowpassSpeakerQueue<Cycles, SoundGenerator> audio_;
Concurrency::AsyncTaskQueue<false> audio_queue_;
SoundGenerator sound_generator_;
Outputs::Speaker::PullLowpass<SoundGenerator> speaker_;
bool speaker_is_enabled_ = false; bool speaker_is_enabled_ = false;

View File

@@ -12,7 +12,7 @@
using namespace Electron; using namespace Electron;
SoundGenerator::SoundGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue) : SoundGenerator::SoundGenerator(Outputs::Speaker::TaskQueue &audio_queue) :
audio_queue_(audio_queue) {} audio_queue_(audio_queue) {}
void SoundGenerator::set_sample_volume_range(std::int16_t range) { void SoundGenerator::set_sample_volume_range(std::int16_t range) {
@@ -40,13 +40,13 @@ template void SoundGenerator::apply_samples<Outputs::Speaker::Action::Mix>(std::
template void SoundGenerator::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *); template void SoundGenerator::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *);
template void SoundGenerator::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *); template void SoundGenerator::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *);
void SoundGenerator::set_divider(uint8_t divider) { void SoundGenerator::set_divider(const uint8_t divider) {
audio_queue_.enqueue([this, divider]() { audio_queue_.enqueue([this, divider]() {
divider_ = divider * 32 / clock_rate_divider; divider_ = divider * 32 / clock_rate_divider;
}); });
} }
void SoundGenerator::set_is_enabled(bool is_enabled) { void SoundGenerator::set_is_enabled(const bool is_enabled) {
audio_queue_.enqueue([this, is_enabled]() { audio_queue_.enqueue([this, is_enabled]() {
is_enabled_ = is_enabled; is_enabled_ = is_enabled;
counter_ = 0; counter_ = 0;

View File

@@ -9,13 +9,13 @@
#pragma once #pragma once
#include "Outputs/Speaker/Implementation/BufferSource.hpp" #include "Outputs/Speaker/Implementation/BufferSource.hpp"
#include "Concurrency/AsyncTaskQueue.hpp" #include "Outputs/Speaker/SpeakerQueue.hpp"
namespace Electron { namespace Electron {
class SoundGenerator: public ::Outputs::Speaker::BufferSource<SoundGenerator, false> { class SoundGenerator: public ::Outputs::Speaker::BufferSource<SoundGenerator, false> {
public: public:
SoundGenerator(Concurrency::AsyncTaskQueue<false> &); SoundGenerator(Outputs::Speaker::TaskQueue &);
void set_divider(uint8_t); void set_divider(uint8_t);
void set_is_enabled(bool); void set_is_enabled(bool);
@@ -28,7 +28,7 @@ public:
void set_sample_volume_range(std::int16_t range); void set_sample_volume_range(std::int16_t range);
private: private:
Concurrency::AsyncTaskQueue<false> &audio_queue_; Outputs::Speaker::TaskQueue &audio_queue_;
unsigned int counter_ = 0; unsigned int counter_ = 0;
unsigned int divider_ = 0; unsigned int divider_ = 0;
bool is_enabled_ = false; bool is_enabled_ = false;

View File

@@ -159,7 +159,7 @@ public:
// to satisfy CRTMachine::Machine // to satisfy CRTMachine::Machine
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final { void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
bus_->speaker_.set_input_rate(float(get_clock_rate() / double(CPUTicksPerAudioTick))); bus_->audio_.speaker().set_input_rate(float(get_clock_rate() / double(CPUTicksPerAudioTick)));
bus_->tia_.set_crt_delegate(&frequency_mismatch_warner_); bus_->tia_.set_crt_delegate(&frequency_mismatch_warner_);
bus_->tia_.set_scan_target(scan_target); bus_->tia_.set_scan_target(scan_target);
} }
@@ -169,7 +169,7 @@ public:
} }
Outputs::Speaker::Speaker *get_speaker() final { Outputs::Speaker::Speaker *get_speaker() final {
return &bus_->speaker_; return &bus_->audio_.speaker();
} }
void run_for(const Cycles cycles) final { void run_for(const Cycles cycles) final {
@@ -206,11 +206,11 @@ private:
// a confidence counter // a confidence counter
Analyser::Dynamic::ConfidenceCounter confidence_counter_; Analyser::Dynamic::ConfidenceCounter confidence_counter_;
void set_is_ntsc(bool is_ntsc) { void set_is_ntsc(const bool is_ntsc) {
bus_->tia_.set_output_mode(is_ntsc ? TIA::OutputMode::NTSC : TIA::OutputMode::PAL); bus_->tia_.set_output_mode(is_ntsc ? TIA::OutputMode::NTSC : TIA::OutputMode::PAL);
const double clock_rate = is_ntsc ? NTSC_clock_rate : PAL_clock_rate; const double clock_rate = is_ntsc ? NTSC_clock_rate : PAL_clock_rate;
bus_->speaker_.set_input_rate(float(clock_rate) / float(CPUTicksPerAudioTick)); bus_->audio_.speaker().set_input_rate(float(clock_rate) / float(CPUTicksPerAudioTick));
bus_->speaker_.set_high_frequency_cutoff(float(clock_rate) / float(CPUTicksPerAudioTick * 2)); bus_->audio_.speaker().set_high_frequency_cutoff(float(clock_rate) / float(CPUTicksPerAudioTick * 2));
set_clock_rate(clock_rate); set_clock_rate(clock_rate);
} }
}; };

View File

@@ -21,12 +21,10 @@ namespace Atari2600 {
class Bus { class Bus {
public: public:
Bus() : Bus() : audio_(Cycles(CPUTicksPerAudioTick * 3)) {}
tia_sound_(audio_queue_),
speaker_(tia_sound_) {}
virtual ~Bus() { virtual ~Bus() {
audio_queue_.lock_flush(); audio_.stop();
} }
virtual void run_for(const Cycles cycles) = 0; virtual void run_for(const Cycles cycles) = 0;
@@ -34,31 +32,22 @@ public:
virtual void set_reset_line(bool state) = 0; virtual void set_reset_line(bool state) = 0;
virtual void flush() = 0; virtual void flush() = 0;
// the RIOT, TIA and speaker // The RIOT, TIA and speaker.
PIA mos6532_; PIA mos6532_;
TIA tia_; TIA tia_;
Outputs::Speaker::PullLowpassSpeakerQueue<Cycles, TIASound> audio_;
Concurrency::AsyncTaskQueue<false> audio_queue_; // Joystick state.
TIASound tia_sound_;
Outputs::Speaker::PullLowpass<TIASound> speaker_;
// joystick state
uint8_t tia_input_value_[2] = {0xff, 0xff}; uint8_t tia_input_value_[2] = {0xff, 0xff};
protected: protected:
// speaker backlog accumlation counter // Video backlog accumulation counter.
Cycles cycles_since_speaker_update_;
inline void update_audio() {
speaker_.run_for(audio_queue_, cycles_since_speaker_update_.divide(Cycles(CPUTicksPerAudioTick * 3)));
}
// video backlog accumulation counter
Cycles cycles_since_video_update_; Cycles cycles_since_video_update_;
inline void update_video() { inline void update_video() {
tia_.run_for(cycles_since_video_update_.flush<Cycles>()); tia_.run_for(cycles_since_video_update_.flush<Cycles>());
} }
// RIOT backlog accumulation counter // RIOT backlog accumulation counter.
Cycles cycles_since_6532_update_; Cycles cycles_since_6532_update_;
inline void update_6532() { inline void update_6532() {
mos6532_.run_for(cycles_since_6532_update_.flush<Cycles>()); mos6532_.run_for(cycles_since_6532_update_.flush<Cycles>());

View File

@@ -70,10 +70,11 @@ public:
// leap to the end of ready only once ready is signalled because on a 6502 ready doesn't take // leap to the end of ready only once ready is signalled because on a 6502 ready doesn't take
// effect until the next read; therefore it isn't safe to assume that signalling ready immediately // effect until the next read; therefore it isn't safe to assume that signalling ready immediately
// skips to the end of the line. // skips to the end of the line.
if(operation == CPU::MOS6502::BusOperation::Ready) if(operation == CPU::MOS6502::BusOperation::Ready) {
cycles_run_for = tia_.get_cycles_until_horizontal_blank(cycles_since_video_update_); cycles_run_for = tia_.get_cycles_until_horizontal_blank(cycles_since_video_update_);
}
cycles_since_speaker_update_ += Cycles(cycles_run_for); audio_ += Cycles(cycles_run_for);
cycles_since_video_update_ += Cycles(cycles_run_for); cycles_since_video_update_ += Cycles(cycles_run_for);
cycles_since_6532_update_ += Cycles(cycles_run_for / 3); cycles_since_6532_update_ += Cycles(cycles_run_for / 3);
bus_extender_.advance_cycles(cycles_run_for / 3); bus_extender_.advance_cycles(cycles_run_for / 3);
@@ -171,11 +172,11 @@ public:
case 0x2c: update_video(); tia_.clear_collision_flags(); break; case 0x2c: update_video(); tia_.clear_collision_flags(); break;
case 0x15: case 0x15:
case 0x16: update_audio(); tia_sound_.set_control(decodedAddress - 0x15, *value); break; case 0x16: audio_->set_control(decodedAddress - 0x15, *value); break;
case 0x17: case 0x17:
case 0x18: update_audio(); tia_sound_.set_divider(decodedAddress - 0x17, *value); break; case 0x18: audio_->set_divider(decodedAddress - 0x17, *value); break;
case 0x19: case 0x19:
case 0x1a: update_audio(); tia_sound_.set_volume(decodedAddress - 0x19, *value); break; case 0x1a: audio_->set_volume(decodedAddress - 0x19, *value); break;
} }
} }
} }
@@ -201,9 +202,8 @@ public:
} }
void flush() override { void flush() override {
update_audio();
update_video(); update_video();
audio_queue_.perform(); audio_.perform();
} }
protected: protected:

View File

@@ -10,7 +10,7 @@
using namespace Atari2600; using namespace Atari2600;
Atari2600::TIASound::TIASound(Concurrency::AsyncTaskQueue<false> &audio_queue) : Atari2600::TIASound::TIASound(Outputs::Speaker::TaskQueue &audio_queue) :
audio_queue_(audio_queue) audio_queue_(audio_queue)
{} {}

View File

@@ -9,7 +9,7 @@
#pragma once #pragma once
#include "Outputs/Speaker/Implementation/BufferSource.hpp" #include "Outputs/Speaker/Implementation/BufferSource.hpp"
#include "Concurrency/AsyncTaskQueue.hpp" #include "Outputs/Speaker/SpeakerQueue.hpp"
namespace Atari2600 { namespace Atari2600 {
@@ -19,7 +19,7 @@ constexpr int CPUTicksPerAudioTick = 2;
class TIASound: public Outputs::Speaker::BufferSource<TIASound, false> { class TIASound: public Outputs::Speaker::BufferSource<TIASound, false> {
public: public:
TIASound(Concurrency::AsyncTaskQueue<false> &); TIASound(Outputs::Speaker::TaskQueue &);
void set_volume(int channel, uint8_t volume); void set_volume(int channel, uint8_t volume);
void set_divider(int channel, uint8_t divider); void set_divider(int channel, uint8_t divider);
@@ -30,7 +30,7 @@ public:
void set_sample_volume_range(std::int16_t); void set_sample_volume_range(std::int16_t);
private: private:
Concurrency::AsyncTaskQueue<false> &audio_queue_; Outputs::Speaker::TaskQueue &audio_queue_;
uint8_t volume_[2]; uint8_t volume_[2];
uint8_t divider_[2]; uint8_t divider_[2];

View File

@@ -9,13 +9,13 @@
#pragma once #pragma once
#include "Outputs/Speaker/Implementation/BufferSource.hpp" #include "Outputs/Speaker/Implementation/BufferSource.hpp"
#include "Concurrency/AsyncTaskQueue.hpp" #include "Outputs/Speaker/SpeakerQueue.hpp"
namespace Commodore::Plus4 { namespace Commodore::Plus4 {
class Audio: public Outputs::Speaker::BufferSource<Audio, false> { class Audio: public Outputs::Speaker::BufferSource<Audio, false> {
public: public:
Audio(Concurrency::AsyncTaskQueue<false> &audio_queue) : Audio(Outputs::Speaker::TaskQueue &audio_queue) :
audio_queue_(audio_queue) {} audio_queue_(audio_queue) {}
template <Outputs::Speaker::Action action> template <Outputs::Speaker::Action action>
@@ -122,7 +122,7 @@ public:
private: private:
// Calling-thread state. // Calling-thread state.
Concurrency::AsyncTaskQueue<false> &audio_queue_; Outputs::Speaker::TaskQueue &audio_queue_;
// Audio-thread state. // Audio-thread state.
int16_t external_volume_ = 0; int16_t external_volume_ = 0;

View File

@@ -186,13 +186,11 @@ public:
interrupts_(*this), interrupts_(*this),
timers_(interrupts_), timers_(interrupts_),
video_(video_map_, interrupts_), video_(video_map_, interrupts_),
audio_(audio_queue_), audio_(clock_rate(false), Cycles(1))
speaker_(audio_)
{ {
const auto clock = clock_rate(false); const auto clock = clock_rate(false);
media_divider_ = Cycles(clock); media_divider_ = Cycles(clock);
set_clock_rate(clock); set_clock_rate(clock);
speaker_.set_input_rate(float(clock));
const auto kernel = ROM::Name::Plus4KernelPALv5; const auto kernel = ROM::Name::Plus4KernelPALv5;
const auto basic = ROM::Name::Plus4BASIC; const auto basic = ROM::Name::Plus4BASIC;
@@ -236,7 +234,7 @@ public:
} }
~ConcreteMachine() { ~ConcreteMachine() {
audio_queue_.lock_flush(); audio_.stop();
} }
// HACK. NOCOMMIT. // HACK. NOCOMMIT.
@@ -258,8 +256,7 @@ public:
c1541_->run_for(c1541_cycles_.divide(media_divider_)); c1541_->run_for(c1541_cycles_.divide(media_divider_));
} }
audio_ += length;
time_since_audio_update_ += length;
} }
if(operation == CPU::MOS6502Mk2::BusOperation::Ready) { if(operation == CPU::MOS6502Mk2::BusOperation::Ready) {
@@ -532,8 +529,7 @@ public:
case 0xff06: video_.write<0xff06>(value); break; case 0xff06: video_.write<0xff06>(value); break;
case 0xff07: case 0xff07:
video_.write<0xff07>(value); video_.write<0xff07>(value);
update_audio(); audio_->set_divider(value);
audio_.set_divider(value);
break; break;
case 0xff08: case 0xff08:
// Observation here: the kernel posts a 0 to this // Observation here: the kernel posts a 0 to this
@@ -560,23 +556,19 @@ public:
case 0xff0d: video_.write<0xff0d>(value); break; case 0xff0d: video_.write<0xff0d>(value); break;
case 0xff0e: case 0xff0e:
ff0e_ = value; ff0e_ = value;
update_audio(); audio_->set_frequency_low<0>(value);
audio_.set_frequency_low<0>(value);
break; break;
case 0xff0f: case 0xff0f:
ff0f_ = value; ff0f_ = value;
update_audio(); audio_->set_frequency_low<1>(value);
audio_.set_frequency_low<1>(value);
break; break;
case 0xff10: case 0xff10:
ff10_ = value; ff10_ = value;
update_audio(); audio_->set_frequency_high<1>(value);
audio_.set_frequency_high<1>(value);
break; break;
case 0xff11: case 0xff11:
ff11_ = value; ff11_ = value;
update_audio(); audio_->set_control(value);
audio_.set_control(value);
break; break;
case 0xff12: case 0xff12:
ff12_ = value & 0x3f; ff12_ = value & 0x3f;
@@ -588,8 +580,7 @@ public:
page_video_ram(); page_video_ram();
} }
update_audio(); audio_->set_frequency_high<0>(value);
audio_.set_frequency_high<0>(value);
break; break;
case 0xff13: case 0xff13:
ff13_ = value & 0xfe; ff13_ = value & 0xfe;
@@ -633,7 +624,7 @@ private:
CPU::MOS6502Mk2::Processor<CPU::MOS6502Mk2::Model::M6502, M6502Traits> m6502_; CPU::MOS6502Mk2::Processor<CPU::MOS6502Mk2::Model::M6502, M6502Traits> m6502_;
Outputs::Speaker::Speaker *get_speaker() override { Outputs::Speaker::Speaker *get_speaker() override {
return &speaker_; return &audio_.speaker();
} }
void set_activity_observer(Activity::Observer *const observer) final { void set_activity_observer(Activity::Observer *const observer) final {
@@ -687,16 +678,12 @@ private:
void run_for(const Cycles cycles) final { void run_for(const Cycles cycles) final {
m6502_.run_for(cycles); m6502_.run_for(cycles);
audio_.perform();
// I don't know why.
update_audio();
audio_queue_.perform();
} }
void flush_output(int outputs) override { void flush_output(int outputs) override {
if(outputs & Output::Audio) { if(outputs & Output::Audio) {
update_audio(); audio_.perform();
audio_queue_.perform();
} }
} }
@@ -723,14 +710,7 @@ private:
Cycles timers_subcycles_; Cycles timers_subcycles_;
Timers timers_; Timers timers_;
Video video_; Video video_;
Outputs::Speaker::PullLowpassSpeakerQueue<Cycles, Audio> audio_;
Concurrency::AsyncTaskQueue<false> audio_queue_;
Audio audio_;
Cycles time_since_audio_update_;
Outputs::Speaker::PullLowpass<Audio> speaker_;
void update_audio() {
speaker_.run_for(audio_queue_, time_since_audio_update_.flush<Cycles>());
}
// MARK: - MappedKeyboardMachine. // MARK: - MappedKeyboardMachine.
MappedKeyboardMachine::KeyboardMapper *get_keyboard_mapper() override { MappedKeyboardMachine::KeyboardMapper *get_keyboard_mapper() override {

View File

@@ -12,7 +12,7 @@ using namespace Enterprise::Dave;
// MARK: - Audio generator // MARK: - Audio generator
Audio::Audio(Concurrency::AsyncTaskQueue<false> &audio_queue) : Audio::Audio(Outputs::Speaker::TaskQueue &audio_queue) :
audio_queue_(audio_queue) {} audio_queue_(audio_queue) {}
void Audio::write(uint16_t address, const uint8_t value) { void Audio::write(uint16_t address, const uint8_t value) {

View File

@@ -11,7 +11,7 @@
#include <cstdint> #include <cstdint>
#include "ClockReceiver/ClockReceiver.hpp" #include "ClockReceiver/ClockReceiver.hpp"
#include "Concurrency/AsyncTaskQueue.hpp" #include "Outputs/Speaker/SpeakerQueue.hpp"
#include "Numeric/LFSR.hpp" #include "Numeric/LFSR.hpp"
#include "Outputs/Speaker/Implementation/BufferSource.hpp" #include "Outputs/Speaker/Implementation/BufferSource.hpp"
@@ -28,7 +28,7 @@ enum class Interrupt: uint8_t {
*/ */
class Audio: public Outputs::Speaker::BufferSource<Audio, true> { class Audio: public Outputs::Speaker::BufferSource<Audio, true> {
public: public:
Audio(Concurrency::AsyncTaskQueue<false> &audio_queue); Audio(Outputs::Speaker::TaskQueue &);
/// Modifies an register in the audio range; only the low 4 bits are /// Modifies an register in the audio range; only the low 4 bits are
/// used for register decoding so it's assumed that the caller has /// used for register decoding so it's assumed that the caller has
@@ -41,7 +41,7 @@ public:
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::StereoSample *target); void apply_samples(std::size_t number_of_samples, Outputs::Speaker::StereoSample *target);
private: private:
Concurrency::AsyncTaskQueue<false> &audio_queue_; Outputs::Speaker::TaskQueue &audio_queue_;
// Global divider (i.e. 8MHz/12Mhz switch). // Global divider (i.e. 8MHz/12Mhz switch).
uint8_t global_divider_; uint8_t global_divider_;

View File

@@ -103,12 +103,10 @@ public:
min_ram_slot_(min_ram_slot(target)), min_ram_slot_(min_ram_slot(target)),
z80_(*this), z80_(*this),
nick_(ram_.end() - 65536), nick_(ram_.end() - 65536),
dave_audio_(audio_queue_), audio_(float(clock_rate) / float(DaveDivider), DaveDivider) {
speaker_(dave_audio_) {
// Request a clock of 4Mhz; this'll be mapped upwards for Nick and downwards for Dave elsewhere. // Request a clock of 4Mhz; this'll be mapped upwards for Nick and downwards for Dave elsewhere.
set_clock_rate(clock_rate); set_clock_rate(clock_rate);
speaker_.set_input_rate(float(clock_rate) / float(dave_divider));
ROM::Request request; ROM::Request request;
using Target = Analyser::Static::Enterprise::Target; using Target = Analyser::Static::Enterprise::Target;
@@ -257,7 +255,7 @@ public:
} }
~ConcreteMachine() { ~ConcreteMachine() {
audio_queue_.lock_flush(); audio_.stop();
} }
// MARK: - Z80::BusHandler. // MARK: - Z80::BusHandler.
@@ -344,7 +342,7 @@ public:
} }
const HalfCycles full_length = cycle.length + penalty; const HalfCycles full_length = cycle.length + penalty;
time_since_audio_update_ += full_length; audio_ += full_length;
advance_nick(full_length); advance_nick(full_length);
if(dave_timer_ += full_length) { if(dave_timer_ += full_length) {
set_interrupts(dave_timer_.last_valid()->get_new_interrupts(), dave_timer_.last_sequence_point_overrun()); set_interrupts(dave_timer_.last_valid()->get_new_interrupts(), dave_timer_.last_sequence_point_overrun());
@@ -475,8 +473,7 @@ public:
case 0xa4: case 0xa5: case 0xa6: case 0xa7: case 0xa4: case 0xa5: case 0xa6: case 0xa7:
case 0xa8: case 0xa9: case 0xaa: case 0xab: case 0xa8: case 0xa9: case 0xaa: case 0xab:
case 0xac: case 0xad: case 0xae: case 0xaf: case 0xac: case 0xad: case 0xae: case 0xaf:
update_audio(); audio_->write(address, *cycle.value);
dave_audio_.write(address, *cycle.value);
dave_timer_->write(address, *cycle.value); dave_timer_->write(address, *cycle.value);
break; break;
@@ -563,8 +560,7 @@ public:
nick_.flush(); nick_.flush();
} }
if(outputs & Output::Audio) { if(outputs & Output::Audio) {
update_audio(); audio_.perform();
audio_queue_.perform();
} }
} }
@@ -650,7 +646,7 @@ private:
// MARK: - AudioProducer // MARK: - AudioProducer
Outputs::Speaker::Speaker *get_speaker() final { Outputs::Speaker::Speaker *get_speaker() final {
return &speaker_; return &audio_.speaker();
} }
// MARK: - TimedMachine // MARK: - TimedMachine
@@ -726,20 +722,13 @@ private:
bool previous_nick_interrupt_line_ = false; bool previous_nick_interrupt_line_ = false;
// Cf. timing guesses above. // Cf. timing guesses above.
Concurrency::AsyncTaskQueue<false> audio_queue_; Outputs::Speaker::PullLowpassSpeakerQueue<HalfCycles, Dave::Audio> audio_;
Dave::Audio dave_audio_;
Outputs::Speaker::PullLowpass<Dave::Audio> speaker_;
HalfCycles time_since_audio_update_;
HalfCycles dave_delay_ = HalfCycles(2); HalfCycles dave_delay_ = HalfCycles(2);
// The divider supplied to the JustInTimeActor and the manual divider used in // The divider supplied to the JustInTimeActor and the manual divider used in
// update_audio() should match. // the spekaer queue should match.
static constexpr int dave_divider = 8; static constexpr int DaveDivider = 8;
JustInTimeActor<Dave::TimedInterruptSource, HalfCycles, 1, dave_divider> dave_timer_; JustInTimeActor<Dave::TimedInterruptSource, HalfCycles, 1, DaveDivider> dave_timer_;
inline void update_audio() {
speaker_.run_for(audio_queue_, time_since_audio_update_.divide_cycles(Cycles(dave_divider)));
}
// MARK: - EXDos card. // MARK: - EXDos card.
EXDos exdos_; EXDos exdos_;

View File

@@ -2523,6 +2523,7 @@
4BEF6AA81D35CE9E00E73575 /* DigitalPhaseLockedLoopBridge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DigitalPhaseLockedLoopBridge.h; sourceTree = "<group>"; }; 4BEF6AA81D35CE9E00E73575 /* DigitalPhaseLockedLoopBridge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DigitalPhaseLockedLoopBridge.h; sourceTree = "<group>"; };
4BEF6AA91D35CE9E00E73575 /* DigitalPhaseLockedLoopBridge.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = DigitalPhaseLockedLoopBridge.mm; sourceTree = "<group>"; }; 4BEF6AA91D35CE9E00E73575 /* DigitalPhaseLockedLoopBridge.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = DigitalPhaseLockedLoopBridge.mm; sourceTree = "<group>"; };
4BEF6AAB1D35D1C400E73575 /* DPLLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DPLLTests.swift; sourceTree = "<group>"; }; 4BEF6AAB1D35D1C400E73575 /* DPLLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DPLLTests.swift; sourceTree = "<group>"; };
4BEF9CA92EC8294E00DDD0F6 /* SpeakerQueue.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = SpeakerQueue.hpp; sourceTree = "<group>"; };
4BF0BC67297108D100CCA2B5 /* MemorySlotHandler.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = MemorySlotHandler.cpp; sourceTree = "<group>"; }; 4BF0BC67297108D100CCA2B5 /* MemorySlotHandler.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = MemorySlotHandler.cpp; sourceTree = "<group>"; };
4BF0BC6F2973318E00CCA2B5 /* RP5C01.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = RP5C01.cpp; sourceTree = "<group>"; }; 4BF0BC6F2973318E00CCA2B5 /* RP5C01.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = RP5C01.cpp; sourceTree = "<group>"; };
4BF0BC702973318E00CCA2B5 /* RP5C01.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = RP5C01.hpp; sourceTree = "<group>"; }; 4BF0BC702973318E00CCA2B5 /* RP5C01.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = RP5C01.hpp; sourceTree = "<group>"; };
@@ -5312,6 +5313,7 @@
4BD060A41FE49D3C006E14BE /* Speaker */ = { 4BD060A41FE49D3C006E14BE /* Speaker */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
4BEF9CA92EC8294E00DDD0F6 /* SpeakerQueue.hpp */,
4BD060A51FE49D3C006E14BE /* Speaker.hpp */, 4BD060A51FE49D3C006E14BE /* Speaker.hpp */,
4B8EF6051FE5AF830076CCDD /* Implementation */, 4B8EF6051FE5AF830076CCDD /* Implementation */,
); );

View File

@@ -39,7 +39,7 @@
namespace { namespace {
struct MachineUpdater { struct MachineUpdater {
void perform(Time::Nanos duration) { void perform(const Time::Nanos duration) {
// Top out at 1/20th of a second; this is a safeguard against a negative // Top out at 1/20th of a second; this is a safeguard against a negative
// feedback loop if emulation starts running slowly. // feedback loop if emulation starts running slowly.
const auto seconds = std::min(Time::seconds(duration), 0.05); const auto seconds = std::min(Time::seconds(duration), 0.05);
@@ -51,7 +51,7 @@ struct MachineUpdater {
MachineTypes::TimedMachine *timed_machine = nullptr; MachineTypes::TimedMachine *timed_machine = nullptr;
}; };
using Updater = Concurrency::AsyncTaskQueue<true, false, MachineUpdater>; using Updater = Concurrency::AsyncTaskQueue<true, false, false, MachineUpdater>;
} }

View File

@@ -0,0 +1,83 @@
//
// SIDTests.mm
// Clock SignalTests
//
// Created by Thomas Harte on 11/11/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#import <XCTest/XCTest.h>
#include "Components/SID/SID.hpp"
@interface SIDTests : XCTestCase
@end
@implementation SIDTests
- (void)testOscillator {
MOS::SID::Voice prior;
MOS::SID::Voice voice;
const uint32_t pulse_width = 0x02'3;
voice.oscillator.pitch = 0x00'1000'00;
voice.oscillator.pulse_width = pulse_width << 20;
voice.oscillator.reset_phase();
int c = 0;
// Run for first half of a cycle.
while(!voice.oscillator.did_raise_b23()) {
// Force envelope.
voice.adsr.envelope = 255;
// Test sawtooth.
voice.set_control(0x20);
XCTAssertEqual(voice.output(prior), c);
// Test triangle.
voice.set_control(0x10);
XCTAssertEqual(voice.output(prior), c << 1);
// Test pulse.
voice.set_control(0x40);
XCTAssertEqual(voice.output(prior), (c < pulse_width) ? 0 : 4095);
// Advance.
voice.update();
++c;
}
// B23 should go up halfway through the 12-bit range.
XCTAssertEqual(c, 2048);
// Run for second half of a cycle.
while(c < 4096) {
// Force envelope.
voice.adsr.envelope = 255;
// Test sawtooth.
voice.set_control(0x20);
XCTAssertEqual(voice.output(prior), c);
// Test triangle.
voice.set_control(0x10);
XCTAssertEqual(voice.output(prior), 4095 - ((c << 1) & 4095));
// Test pulse.
voice.set_control(0x40);
XCTAssertEqual(voice.output(prior), (c <= pulse_width) ? 0 : 4095);
// Advance.
voice.update();
++c;
XCTAssert(!voice.oscillator.did_raise_b23());
}
// Check that B23 doesn't false rise again.
voice.update();
XCTAssert(!voice.oscillator.did_raise_b23());
}
@end

View File

@@ -9,6 +9,7 @@
#pragma once #pragma once
#include "Outputs/Speaker/Speaker.hpp" #include "Outputs/Speaker/Speaker.hpp"
#include "Concurrency/AsyncTaskQueue.hpp"
#include <algorithm> #include <algorithm>
#include <array> #include <array>
@@ -26,7 +27,7 @@ enum class Action {
Ignore, Ignore,
}; };
template <Action action, typename SampleT> void apply(SampleT &lhs, SampleT rhs) { template <Action action, typename SampleT> void apply(SampleT &lhs, const SampleT rhs) {
switch(action) { switch(action) {
case Action::Mix: lhs += rhs; break; case Action::Mix: lhs += rhs; break;
case Action::Store: lhs = rhs; break; case Action::Store: lhs = rhs; break;
@@ -34,7 +35,8 @@ template <Action action, typename SampleT> void apply(SampleT &lhs, SampleT rhs)
} }
} }
template <Action action, typename IteratorT, typename SampleT> void fill(IteratorT begin, IteratorT end, SampleT value) { template <Action action, typename IteratorT, typename SampleT>
void fill(IteratorT begin, const IteratorT end, const SampleT value) {
switch(action) { switch(action) {
case Action::Mix: case Action::Mix:
while(begin != end) { while(begin != end) {
@@ -69,7 +71,7 @@ class BufferSource {
No default implementation is provided. No default implementation is provided.
*/ */
template <Action action> template <Action action>
void apply_samples(std::size_t number_of_samples, typename SampleT<stereo>::type *target); void apply_samples(std::size_t number_of_samples, typename SampleT<stereo>::type *);
/*! /*!
@returns @c true if it is trivially true that a call to get_samples would just @returns @c true if it is trivially true that a call to get_samples would just
@@ -140,7 +142,7 @@ public:
// TODO: use a concept here, when C++20 filters through. // TODO: use a concept here, when C++20 filters through.
// //
// Until then: sample sources should implement this. // Until then: sample sources can implement this rather than apply_samples.
// typename SampleT<stereo>::type level() const; // typename SampleT<stereo>::type level() const;
// void advance(); // void advance();

View File

@@ -10,6 +10,7 @@
#include "BufferSource.hpp" #include "BufferSource.hpp"
#include "Outputs/Speaker/Speaker.hpp" #include "Outputs/Speaker/Speaker.hpp"
#include "Outputs/Speaker/SpeakerQueue.hpp"
#include "SignalProcessing/FIRFilter.hpp" #include "SignalProcessing/FIRFilter.hpp"
#include "ClockReceiver/ClockReceiver.hpp" #include "ClockReceiver/ClockReceiver.hpp"
#include "Concurrency/AsyncTaskQueue.hpp" #include "Concurrency/AsyncTaskQueue.hpp"
@@ -375,10 +376,13 @@ public:
if(cycles == Cycles(0)) { if(cycles == Cycles(0)) {
return; return;
} }
queue.enqueue(update_for(cycles));
}
queue.enqueue([this, cycles] { std::function<void(void)> update_for(const Cycles cycles) {
return [this, cycles] {
run_for(cycles); run_for(cycles);
}); };
} }
private: private:
@@ -414,4 +418,7 @@ private:
} }
}; };
template <typename CyclesT, typename GeneratorT>
using PullLowpassSpeakerQueue = SpeakerQueue<CyclesT, Outputs::Speaker::PullLowpass<GeneratorT>, GeneratorT>;
} }

View File

@@ -0,0 +1,68 @@
//
// SpeakerQueue.hpp
// Clock Signal
//
// Created by Thomas Harte on 14/11/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#pragma once
#include "Concurrency/AsyncTaskQueue.hpp"
#include "ClockReceiver/ClockReceiver.hpp"
namespace Outputs::Speaker {
using TaskQueue = Concurrency::AsyncTaskQueue<false, true, true>;
template <typename CyclesT, typename SpeakerT, typename GeneratorT>
struct SpeakerQueue: private Concurrency::EnqueueDelegate {
constexpr SpeakerQueue(const CyclesT divider) noexcept :
generator_(queue_), speaker_(generator_), divider_(divider)
{
queue_.set_enqueue_delegate(this);
}
constexpr SpeakerQueue(const float input_rate, const CyclesT divider, const float high_cutoff = -1.0f) noexcept :
SpeakerQueue(divider)
{
speaker_.set_input_rate(input_rate);
if(high_cutoff >= 0.0) {
speaker_.set_high_frequency_cutoff(high_cutoff);
}
}
void operator += (const CyclesT &duration) {
time_since_update_ += duration;
}
void stop() {
queue_.stop();
}
void perform() {
// TODO: is there a way to avoid the empty lambda?
queue_.enqueue([]() {});
queue_.perform();
}
SpeakerT &speaker() {
return speaker_;
}
GeneratorT *operator ->() {
return &generator_;
}
private:
TaskQueue queue_;
GeneratorT generator_;
SpeakerT speaker_;
CyclesT divider_;
CyclesT time_since_update_;
std::function<void(void)> prepare_enqueue() final {
return speaker_.update_for(time_since_update_.template divide<Cycles>(divider_));
}
};
}