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:
@@ -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)>>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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_));
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user