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,9 +40,12 @@ 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:
void update() {}
};
protected: struct EnqueueDelegate {
void update() {} virtual std::function<void(void)> prepare_enqueue() = 0;
}; };
/*! /*!
@@ -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) {
@@ -56,45 +58,45 @@ template <Action action, typename IteratorT, typename SampleT> void fill(Iterato
*/ */
template <typename SourceT, bool stereo> template <typename SourceT, bool stereo>
class BufferSource { class BufferSource {
public: public:
/*! /*!
Indicates whether this component will write stereo samples. Indicates whether this component will write stereo samples.
*/ */
static constexpr bool is_stereo = stereo; static constexpr bool is_stereo = stereo;
/*! /*!
Should 'apply' the next @c number_of_samples to @c target ; application means applying @c action which can be achieved either via the Should 'apply' the next @c number_of_samples to @c target ; application means applying @c action which can be achieved either via the
helper functions above — @c apply and @c fill — or by semantic inspection (primarily, if an obvious quick route for @c Action::Ignore is available). helper functions above — @c apply and @c fill — or by semantic inspection (primarily, if an obvious quick route for @c Action::Ignore is available).
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
fill the target with zeroes; @c false if a call might return all zeroes or fill the target with zeroes; @c false if a call might return all zeroes or
might not. 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 Sets the proper output range for this sample source; it should write values
between 0 and volume. 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 Permits a sample source to declare that, averaged over time, it will use only
a certain proportion of the allocated volume range. This commonly happens a certain proportion of the allocated volume range. This commonly happens
in sample sources that use a time-multiplexed sound output — for example, if in sample sources that use a time-multiplexed sound output — for example, if
one were to output only every other sample then it would return 0.5. one were to output only every other sample then it would return 0.5.
This is permitted to vary over time but there is no contract as to when it will be This is permitted to vary over time but there is no contract as to when it will be
used by a speaker. If it varies, it should do so very infrequently and only to used by a speaker. If it varies, it should do so very infrequently and only to
represent changes in hardware configuration. represent changes in hardware configuration.
*/ */
double average_output_peak() const { return 1.0; } double average_output_peak() const { return 1.0; }
}; };
/// ///
@@ -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_));
}
};
}