mirror of
https://github.com/TomHarte/CLK.git
synced 2025-11-23 21:17:42 +00:00
Compare commits
8 Commits
macOSPermi
...
QueueDeleg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4be5ee5b35 | ||
|
|
92e6dc64d4 | ||
|
|
f422cda553 | ||
|
|
2c44d3a7d3 | ||
|
|
051ce98ecb | ||
|
|
33ae24c961 | ||
|
|
4247d0ef40 | ||
|
|
ffababdb45 |
@@ -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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)>>;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>());
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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_;
|
||||||
|
|||||||
@@ -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_;
|
||||||
|
|||||||
@@ -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 */,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
83
OSBindings/Mac/Clock SignalTests/SIDTests.mm
Normal file
83
OSBindings/Mac/Clock SignalTests/SIDTests.mm
Normal 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
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
68
Outputs/Speaker/SpeakerQueue.hpp
Normal file
68
Outputs/Speaker/SpeakerQueue.hpp
Normal 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_));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user