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;
AudioGenerator::AudioGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue) :
AudioGenerator::AudioGenerator(Outputs::Speaker::TaskQueue &audio_queue) :
audio_queue_(audio_queue) {}
void AudioGenerator::set_volume(const uint8_t volume) {

View File

@@ -19,7 +19,7 @@ namespace MOS::MOS6560 {
// audio state
class AudioGenerator: public Outputs::Speaker::BufferSource<AudioGenerator, false> {
public:
AudioGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue);
AudioGenerator(Outputs::Speaker::TaskQueue &audio_queue);
void set_volume(uint8_t);
void set_control(int channel, uint8_t value);
@@ -30,7 +30,7 @@ public:
void set_sample_volume_range(std::int16_t);
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 shift_registers_[4] = {0, 0, 0, 0};
@@ -64,8 +64,7 @@ public:
MOS6560(BusHandler &bus_handler) :
bus_handler_(bus_handler),
crt_(65*4, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Luminance8Phase8),
audio_generator_(audio_queue_),
speaker_(audio_generator_)
audio_(Cycles(4))
{
// default to s-video output
crt_.set_display_type(Outputs::Display::DisplayType::SVideo);
@@ -75,11 +74,11 @@ public:
}
~MOS6560() {
audio_queue_.lock_flush();
audio_.stop();
}
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) {
@@ -95,11 +94,11 @@ public:
return crt_.get_display_type();
}
Outputs::Speaker::Speaker *get_speaker() {
return &speaker_;
return &audio_.speaker();
}
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) {
// 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();
while(number_of_cycles--) {
@@ -377,8 +376,7 @@ public:
Causes the 6560 to flush as much pending CRT and speaker communications as possible.
*/
inline void flush() {
update_audio();
audio_queue_.perform();
audio_.perform();
}
/*!
@@ -420,14 +418,12 @@ public:
case 0xb:
case 0xc:
case 0xd:
update_audio();
audio_generator_.set_control(address - 0xa, value);
audio_->set_control(address - 0xa, value);
break;
case 0xe:
update_audio();
registers_.auxiliary_colour = colours_[value >> 4];
audio_generator_.set_volume(value & 0xf);
audio_->set_volume(value & 0xf);
break;
case 0xf: {
@@ -467,14 +463,7 @@ private:
BusHandler &bus_handler_;
Outputs::CRT::CRT crt_;
Concurrency::AsyncTaskQueue<false> audio_queue_;
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))));
}
Outputs::Speaker::PullLowpassSpeakerQueue<Cycles, AudioGenerator> audio_;
// register state
struct {

View File

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

View File

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

View File

@@ -12,7 +12,7 @@
using namespace Electron;
SoundGenerator::SoundGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue) :
SoundGenerator::SoundGenerator(Outputs::Speaker::TaskQueue &audio_queue) :
audio_queue_(audio_queue) {}
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::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]() {
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]() {
is_enabled_ = is_enabled;
counter_ = 0;

View File

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

View File

@@ -159,7 +159,7 @@ public:
// to satisfy CRTMachine::Machine
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_scan_target(scan_target);
}
@@ -169,7 +169,7 @@ public:
}
Outputs::Speaker::Speaker *get_speaker() final {
return &bus_->speaker_;
return &bus_->audio_.speaker();
}
void run_for(const Cycles cycles) final {
@@ -206,11 +206,11 @@ private:
// a 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);
const double clock_rate = is_ntsc ? NTSC_clock_rate : PAL_clock_rate;
bus_->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_input_rate(float(clock_rate) / float(CPUTicksPerAudioTick));
bus_->audio_.speaker().set_high_frequency_cutoff(float(clock_rate) / float(CPUTicksPerAudioTick * 2));
set_clock_rate(clock_rate);
}
};

View File

@@ -21,12 +21,10 @@ namespace Atari2600 {
class Bus {
public:
Bus() :
tia_sound_(audio_queue_),
speaker_(tia_sound_) {}
Bus() : audio_(Cycles(CPUTicksPerAudioTick * 3)) {}
virtual ~Bus() {
audio_queue_.lock_flush();
audio_.stop();
}
virtual void run_for(const Cycles cycles) = 0;
@@ -34,31 +32,22 @@ public:
virtual void set_reset_line(bool state) = 0;
virtual void flush() = 0;
// the RIOT, TIA and speaker
// The RIOT, TIA and speaker.
PIA mos6532_;
TIA tia_;
Outputs::Speaker::PullLowpassSpeakerQueue<Cycles, TIASound> audio_;
Concurrency::AsyncTaskQueue<false> audio_queue_;
TIASound tia_sound_;
Outputs::Speaker::PullLowpass<TIASound> speaker_;
// joystick state
// Joystick state.
uint8_t tia_input_value_[2] = {0xff, 0xff};
protected:
// speaker backlog accumlation 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
// Video backlog accumulation counter.
Cycles cycles_since_video_update_;
inline void update_video() {
tia_.run_for(cycles_since_video_update_.flush<Cycles>());
}
// RIOT backlog accumulation counter
// RIOT backlog accumulation counter.
Cycles cycles_since_6532_update_;
inline void update_6532() {
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
// effect until the next read; therefore it isn't safe to assume that signalling ready immediately
// 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_since_speaker_update_ += Cycles(cycles_run_for);
audio_ += Cycles(cycles_run_for);
cycles_since_video_update_ += Cycles(cycles_run_for);
cycles_since_6532_update_ += 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 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 0x18: update_audio(); tia_sound_.set_divider(decodedAddress - 0x17, *value); break;
case 0x18: audio_->set_divider(decodedAddress - 0x17, *value); break;
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 {
update_audio();
update_video();
audio_queue_.perform();
audio_.perform();
}
protected:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,7 @@
#include <cstdint>
#include "ClockReceiver/ClockReceiver.hpp"
#include "Concurrency/AsyncTaskQueue.hpp"
#include "Outputs/Speaker/SpeakerQueue.hpp"
#include "Numeric/LFSR.hpp"
#include "Outputs/Speaker/Implementation/BufferSource.hpp"
@@ -28,7 +28,7 @@ enum class Interrupt: uint8_t {
*/
class Audio: public Outputs::Speaker::BufferSource<Audio, true> {
public:
Audio(Concurrency::AsyncTaskQueue<false> &audio_queue);
Audio(Outputs::Speaker::TaskQueue &);
/// 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
@@ -41,7 +41,7 @@ public:
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::StereoSample *target);
private:
Concurrency::AsyncTaskQueue<false> &audio_queue_;
Outputs::Speaker::TaskQueue &audio_queue_;
// Global divider (i.e. 8MHz/12Mhz switch).
uint8_t global_divider_;

View File

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

View File

@@ -2523,6 +2523,7 @@
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>"; };
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>"; };
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>"; };
@@ -5312,6 +5313,7 @@
4BD060A41FE49D3C006E14BE /* Speaker */ = {
isa = PBXGroup;
children = (
4BEF9CA92EC8294E00DDD0F6 /* SpeakerQueue.hpp */,
4BD060A51FE49D3C006E14BE /* Speaker.hpp */,
4B8EF6051FE5AF830076CCDD /* Implementation */,
);

View File

@@ -39,7 +39,7 @@
namespace {
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
// feedback loop if emulation starts running slowly.
const auto seconds = std::min(Time::seconds(duration), 0.05);
@@ -51,7 +51,7 @@ struct MachineUpdater {
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
#include "Outputs/Speaker/Speaker.hpp"
#include "Concurrency/AsyncTaskQueue.hpp"
#include <algorithm>
#include <array>
@@ -26,7 +27,7 @@ enum class Action {
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) {
case Action::Mix: 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) {
case Action::Mix:
while(begin != end) {
@@ -56,45 +58,45 @@ template <Action action, typename IteratorT, typename SampleT> void fill(Iterato
*/
template <typename SourceT, bool stereo>
class BufferSource {
public:
/*!
Indicates whether this component will write stereo samples.
*/
static constexpr bool is_stereo = stereo;
public:
/*!
Indicates whether this component will write stereo samples.
*/
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
helper functions above — @c apply and @c fill — or by semantic inspection (primarily, if an obvious quick route for @c Action::Ignore is available).
/*!
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).
No default implementation is provided.
*/
template <Action action>
void apply_samples(std::size_t number_of_samples, typename SampleT<stereo>::type *target);
No default implementation is provided.
*/
template <Action action>
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
fill the target with zeroes; @c false if a call might return all zeroes or
might not.
*/
// bool is_zero_level() const { return false; }
/*!
@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
might not.
*/
// bool is_zero_level() const { return false; }
/*!
Sets the proper output range for this sample source; it should write values
between 0 and volume.
*/
// void set_sample_volume_range(std::int16_t volume);
/*!
Sets the proper output range for this sample source; it should write values
between 0 and volume.
*/
// void set_sample_volume_range(std::int16_t volume);
/*!
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
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.
/*!
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
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.
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
represent changes in hardware configuration.
*/
double average_output_peak() const { return 1.0; }
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
represent changes in hardware configuration.
*/
double average_output_peak() const { return 1.0; }
};
///
@@ -140,7 +142,7 @@ public:
// 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;
// void advance();

View File

@@ -10,6 +10,7 @@
#include "BufferSource.hpp"
#include "Outputs/Speaker/Speaker.hpp"
#include "Outputs/Speaker/SpeakerQueue.hpp"
#include "SignalProcessing/FIRFilter.hpp"
#include "ClockReceiver/ClockReceiver.hpp"
#include "Concurrency/AsyncTaskQueue.hpp"
@@ -375,10 +376,13 @@ public:
if(cycles == Cycles(0)) {
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);
});
};
}
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_));
}
};
}