1
0
mirror of https://github.com/TomHarte/CLK.git synced 2026-04-20 10:17:05 +00:00

With the Electron as a test bed, start to simplify audio class groups.

This commit is contained in:
Thomas Harte
2025-11-14 22:39:53 -05:00
parent 176bda9eb8
commit ffababdb45
9 changed files with 137 additions and 73 deletions
+15 -2
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)>>;
+16 -25
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,9 @@
#include "ClockReceiver/JustInTime.hpp"
#include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#include "Outputs/Speaker/SpeakerQueue.hpp"
#include "Interrupts.hpp"
#include "Keyboard.hpp"
#include "Plus3.hpp"
@@ -58,8 +60,7 @@ public:
hard_drive_(scsi_bus_, 0),
scsi_device_(scsi_bus_.add_device()),
video_(ram_),
sound_generator_(audio_queue_),
speaker_(sound_generator_) {
audio_(SoundGenerator::clock_rate_divider) {
memset(key_states_, 0, sizeof(key_states_));
for(int c = 0; c < 16; c++)
memset(roms_[c], 0xff, 16384);
@@ -67,8 +68,8 @@ public:
tape_.set_delegate(this);
set_clock_rate(2000000);
speaker_.set_input_rate(2000000 / SoundGenerator::clock_rate_divider);
speaker_.set_high_frequency_cutoff(6000);
audio_.speaker.set_input_rate(2000000 / SoundGenerator::clock_rate_divider);
audio_.speaker.set_high_frequency_cutoff(6000);
::ROM::Request request = ::ROM::Request(::ROM::Name::AcornBASICII) && ::ROM::Request(::ROM::Name::AcornElectronMOS100);
if(target.has_pres_adfs) {
@@ -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_.generator.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_.generator.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;
@@ -771,9 +761,10 @@ private:
// Outputs
VideoOutput video_;
Concurrency::AsyncTaskQueue<false> audio_queue_;
SoundGenerator sound_generator_;
Outputs::Speaker::PullLowpass<SoundGenerator> speaker_;
Outputs::Speaker::SpeakerQueue<
Outputs::Speaker::PullLowpass<SoundGenerator>,
SoundGenerator
> audio_;
bool speaker_is_enabled_ = false;
+3 -3
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;
+3 -3
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;
@@ -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 */,
);
@@ -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>;
}
+38 -36
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();
@@ -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:
+52
View File
@@ -0,0 +1,52 @@
//
// 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 SpeakerT, typename GeneratorT>
struct SpeakerQueue: private Concurrency::EnqueueDelegate {
private:
TaskQueue queue_;
public:
SpeakerQueue(const Cycles divider) : generator(queue_), speaker(generator), divider_(divider) {
queue_.set_enqueue_delegate(this);
}
GeneratorT generator;
SpeakerT speaker;
void operator += (const Cycles &duration) {
cycles_since_update_ += duration;
}
void stop() {
queue_.stop();
}
void perform() {
// TODO: is there a way to avoid the empty lambda?
queue_.enqueue([]() {});
queue_.perform();
}
private:
Cycles divider_;
Cycles cycles_since_update_;
std::function<void(void)> prepare_enqueue() {
return speaker.update_for(cycles_since_update_.divide(divider_));
}
};
}