1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-11-24 13:17:41 +00:00

Compare commits

...

70 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
a9d945d6d2 Merge pull request #1639 from TomHarte/CommodoreNew6502s
Adapt all Commodore machines to 6502Mk2.
2025-11-17 23:16:17 -05:00
Thomas Harte
5e465f1ff4 Avoid function specialisation. 2025-11-17 23:04:33 -05:00
Thomas Harte
5359964fef Make minor style improvements, fix cursor keys. 2025-11-17 22:55:18 -05:00
Thomas Harte
fa8be26f9f Fix read-only bit. 2025-11-17 22:20:20 -05:00
Thomas Harte
aabfe7c284 Observe that there is an attempt to output data. 2025-11-17 21:15:42 -05:00
Thomas Harte
d011b10b5d Update comments, add note-to-self on write mode. 2025-11-17 17:54:09 -05:00
Thomas Harte
332b37063f Adjust for style. 2025-11-17 17:15:26 -05:00
Thomas Harte
b3a9e39be3 Transfer C1540, ensure Plus 4 bus always holds _something_. 2025-11-17 14:39:35 -05:00
Thomas Harte
67590cf06b Adapt Vic-20 to the newer 6502. 2025-11-17 14:23:05 -05:00
Thomas Harte
236fdacb36 Adapt Plus 4 to the newer 6502. 2025-11-17 14:14:25 -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
Thomas Harte
176bda9eb8 Merge pull request #1637 from TomHarte/Voice3Off
SID: support the voice 3 disable bit.
2025-11-14 19:35:05 -05:00
Thomas Harte
9f0a0443a8 SID: support the voice 3 disable bit. 2025-11-14 15:38:54 -05:00
Thomas Harte
fd1a7e78c5 Merge pull request #1636 from TomHarte/BiquadAttribution
Record references for the SID and biquad filter.
2025-11-14 13:29:55 -05:00
Thomas Harte
909fa57b27 Record references for the SID and biquad filter. 2025-11-14 13:29:15 -05:00
Thomas Harte
5630b1c351 Merge pull request #1634 from TomHarte/BeebSID
Investigate the SID via BeebSID.
2025-11-13 20:38:34 -05:00
Thomas Harte
c4fe38a61f Allow potentometer inputs to be set; disable SID by default. 2025-11-13 18:05:13 -05:00
Thomas Harte
5b4f303e35 Mention memory barrier. 2025-11-13 18:02:41 -05:00
Thomas Harte
c9c1bde6e2 Switch to spinning on SID thread synchronisation. 2025-11-13 17:59:24 -05:00
Thomas Harte
d01e1f3bb1 Block and synchronise threads for voice 3 readback. 2025-11-13 17:34:57 -05:00
Thomas Harte
fd32e63459 Clean up: pull out noise generation, remove code from header. 2025-11-13 13:44:53 -05:00
Thomas Harte
dbbb1d60fc Add commentary, use filter reconfiguration to retain sample history. 2025-11-13 13:27:55 -05:00
Thomas Harte
1ce013bcf7 Simplify, update noise tap. 2025-11-13 11:59:56 -05:00
Thomas Harte
86bf019aac Attempt further to improve filter precision. 2025-11-13 11:54:37 -05:00
Thomas Harte
d00546dd77 Add post hoc filter, attempt to juice precision. 2025-11-13 07:31:59 -05:00
Thomas Harte
cf33e17688 Attempt to use biquad filter; fix signs. 2025-11-12 23:08:35 -05:00
Thomas Harte
c5c6c5ff72 Add textbook filter construction. 2025-11-12 22:06:48 -05:00
Thomas Harte
fa0835abd8 Capture all filter parameters. 2025-11-12 17:47:49 -05:00
Thomas Harte
f232b179ed Partition channels into filtered and unfiltered, and apply no-op biquad. 2025-11-12 17:40:14 -05:00
Thomas Harte
a4a0026cab Reintroduce decay stage; flip pulse meaning. 2025-11-11 21:38:27 -05:00
Thomas Harte
eac7493180 Support master volume. 2025-11-11 21:04:07 -05:00
Thomas Harte
989fb32fba Fix clocking, do a linear attack phase. 2025-11-11 20:53:54 -05:00
Thomas Harte
735afcfabb Adopt painful pulse test, temporarily (?). 2025-11-11 18:26:00 -05:00
Thomas Harte
37152a1fad Start testing; I'm now unsure about pulses. 2025-11-11 17:54:31 -05:00
Thomas Harte
4e86184955 Add local hack to ensure good flushing. 2025-11-11 14:40:13 -05:00
Thomas Harte
d23dbb96c2 Support system volume, avoid clipping. 2025-11-11 14:40:04 -05:00
Thomas Harte
4586e4b4c1 Apply envelope. 2025-11-11 14:26:53 -05:00
Thomas Harte
de5cdbf18c Make a complete attempt at ADSR. 2025-11-11 14:25:36 -05:00
Thomas Harte
8c2294fc0d Treat sustain as a volume; start second prescaler table. 2025-11-11 12:43:48 -05:00
Thomas Harte
b0b82782ad Build in initial prescaler. 2025-11-11 12:21:49 -05:00
Thomas Harte
b9f5802c89 Return whatever was written last if read. 2025-11-11 09:19:01 -05:00
Thomas Harte
29235f1276 Adjust noise clocking, make it reactive to the test bit. 2025-11-11 09:16:43 -05:00
Thomas Harte
8c74e2a323 Implement LFSR. 2025-11-10 22:44:00 -05:00
Thomas Harte
ae2936b9c3 Correct clock rate, triangle wave. 2025-11-10 22:35:13 -05:00
Thomas Harte
0d295a6338 Don't capture a reference to parameters. 2025-11-10 22:10:28 -05:00
Thomas Harte
3ebd6c6871 Rejig oscillators, output some vague noise. 2025-11-10 21:52:10 -05:00
Thomas Harte
6e2cd0ace6 Divide state, start adding waveforms. 2025-11-10 17:27:32 -05:00
Thomas Harte
af82a0bcda Add ADSR TODO. 2025-11-10 14:18:24 -05:00
Thomas Harte
6fe208ae77 Honour test and sync bits. 2025-11-10 14:17:54 -05:00
Thomas Harte
f569b86c90 Merge branch 'master' into BeebSID 2025-11-10 14:10:33 -05:00
Thomas Harte
b622cc9536 Merge pull request #1635 from TomHarte/CleanerQueue
Enforce perform_automatically, start_immediately; relax Boolean access order.
2025-11-10 14:09:31 -05:00
Thomas Harte
7dfd5ea0d0 Add phase accumulation, rename to pitch. 2025-11-10 13:27:43 -05:00
Thomas Harte
a81309433c Switch to lambda form. 2025-11-09 21:09:57 -05:00
Thomas Harte
902f388cb1 Enforce perform_automatically, start_immediately; relax Boolean access order. 2025-11-09 00:17:39 -05:00
Thomas Harte
0cc5a9d74f Move thread. 2025-11-08 23:02:54 -05:00
Thomas Harte
5e98e6502d Attempt some basic voice details. 2025-11-08 21:54:41 -05:00
Thomas Harte
fe7a206fc5 Add an empty vessel of a SID. 2025-11-07 22:51:28 -05:00
Thomas Harte
c5704aaaff Merge pull request #1633 from TomHarte/TubeBrevity
Move point of templature to tube processors.
2025-11-07 18:05:19 -05:00
Thomas Harte
e115f09f51 Merge pull request #1632 from TomHarte/6845DeadState
Remove dead state.
2025-11-07 17:54:13 -05:00
Thomas Harte
32cd142629 Move point of templature to tube processors. 2025-11-07 17:27:52 -05:00
Thomas Harte
b00be303aa Remove dead state. 2025-11-07 13:08:12 -05:00
Thomas Harte
273e23bd98 Merge pull request #1631 from TomHarte/SecondProcessorScreenshot
Substitute a screenshot of Second Processor Elite.
2025-11-07 12:54:16 -05:00
Thomas Harte
5063e6943d Substitute a screenshot of Second Processor Elite. 2025-11-07 12:53:09 -05:00
55 changed files with 1707 additions and 627 deletions

View File

@@ -44,6 +44,7 @@ struct BBCMicroTarget: public ::Analyser::Static::Target, public Reflection::Str
bool has_1770dfs = false;
bool has_adfs = false;
bool has_sideways_ram = true;
bool has_beebsid = false;
ReflectableEnum(TubeProcessor, None, WDC65C02, Z80);
TubeProcessor tube_processor = TubeProcessor::None;
@@ -56,6 +57,7 @@ private:
DeclareField(has_1770dfs);
DeclareField(has_adfs);
DeclareField(has_sideways_ram);
DeclareField(has_beebsid);
AnnounceEnum(TubeProcessor);
DeclareField(tube_processor);
}

View File

@@ -325,7 +325,7 @@ public:
/// Flushes all accumulated time.
inline void flush() {
if(!is_flushed_) {
task_queue_.flush();
task_queue_.lock_flush();
object_.run_for(time_since_update_.template flush<TargetTimeScale>());
is_flushed_ = true;
}

View File

@@ -12,10 +12,9 @@
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) {
audio_queue_.enqueue([this, volume]() {
volume_ = int16_t(volume) * range_multiplier_;
@@ -148,7 +147,7 @@ template void AudioGenerator::apply_samples<Outputs::Speaker::Action::Store>(
template void AudioGenerator::apply_samples<Outputs::Speaker::Action::Ignore>(
std::size_t, Outputs::Speaker::MonoSample *);
void AudioGenerator::set_sample_volume_range(std::int16_t range) {
void AudioGenerator::set_sample_volume_range(const std::int16_t range) {
range_multiplier_ = int16_t(range / 64);
}

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_.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

@@ -127,13 +127,13 @@ public:
}
// Per CPC documentation, skew doesn't work on a "type 1 or 2", i.e. an MC6845 or a UM6845R.
if(personality != Personality::UM6845R && personality != Personality::MC6845) {
switch((value >> 4)&3) {
default: display_skew_mask_ = 1; break;
case 1: display_skew_mask_ = 2; break;
case 2: display_skew_mask_ = 4; break;
}
}
// if(personality != Personality::UM6845R && personality != Personality::MC6845) {
// switch((value >> 4)&3) {
// default: display_skew_mask_ = 1; break;
// case 1: display_skew_mask_ = 2; break;
// case 2: display_skew_mask_ = 4; break;
// }
// }
break;
case 9: layout_.vertical.end_line = value; break;
case 10:
@@ -533,7 +533,6 @@ private:
LineAddress line_; // line_counter
LineAddress next_line_; // line_counter_next
RefreshAddress refresh_; // ma_i
uint8_t adjustment_counter_ = 0;
bool character_is_visible_ = false; // h_display
bool row_is_visible_ = false; // v_display
@@ -549,8 +548,7 @@ private:
RefreshAddress line_address_; // ma_row
uint8_t status_ = 0;
int display_skew_mask_ = 1;
unsigned int character_is_visible_shifter_ = 0;
// int display_skew_mask_ = 1;
bool eof_latched_ = false; // eof_latched
bool eom_latched_ = false; // eom_latched
@@ -561,8 +559,6 @@ private:
bool vsync_even_ = false; // vs_even
bool vsync_odd_ = false; // vs_odd
bool reset_ = false;
Numeric::SizedInt<3> cursor_history_; // cursor0, cursor1, cursor2 [TODO]
bool line_is_interlaced_ = false;

433
Components/SID/SID.cpp Normal file
View File

@@ -0,0 +1,433 @@
//
// SID.cpp
// Clock Signal
//
// Created by Thomas Harte on 07/11/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#include "SID.hpp"
// Sources used:
//
// (1) SID Article v0.2 at https://github.com/ImreOlajos/SID-Article
// (2) Technical SID Information/Software stuff at http://www.sidmusic.org/sid/sidtech2.html
// (3) SID 6581/8580 (Sound Interface Device) reference at https://oxyron.de/html/registers_sid.html
using namespace MOS::SID;
SID::SID(Concurrency::AsyncTaskQueue<false> &audio_queue) :
audio_queue_(audio_queue),
output_filter_(
SignalProcessing::BiquadFilter::Type::LowPass,
1000000.0f,
15000.0f
) {}
// MARK: - Programmer interface.
void SID::write(const Numeric::SizedInt<5> address, const uint8_t value) {
last_write_ = value;
audio_queue_.enqueue([=, this] {
const auto voice = [&]() -> Voice & {
return voices_[address.get() / 7];
};
const auto oscillator = [&]() -> Voice::Oscillator & {
return voice().oscillator;
};
const auto adsr = [&]() -> Voice::ADSR & {
return voice().adsr;
};
switch(address.get()) {
case 0x00: case 0x07: case 0x0e:
oscillator().pitch = (oscillator().pitch & 0xff'00'00) | uint32_t(value << 8);
break;
case 0x01: case 0x08: case 0x0f:
oscillator().pitch = (oscillator().pitch & 0x00'ff'00) | uint32_t(value << 16);
break;
case 0x02: case 0x09: case 0x10:
oscillator().pulse_width = (oscillator().pitch & 0xf0'00'00'00) | uint32_t(value << 20);
break;
case 0x03: case 0x0a: case 0x11:
// The top bit of the phase counter is inverted; since it'll be compared directly with the
// pulse width, invert that bit too.
oscillator().pulse_width =
(
(oscillator().pitch & 0x0f'f0'00'00) |
uint32_t(value << 28)
);
break;
case 0x04: case 0x0b: case 0x12:
voice().set_control(value);
break;
case 0x05: case 0x0c: case 0x13:
adsr().attack = value >> 4;
adsr().decay = value;
adsr().set_phase(adsr().phase);
break;
case 0x06: case 0x0d: case 0x14:
adsr().sustain = (value >> 4) | (value & 0xf0);
adsr().release = value;
adsr().set_phase(adsr().phase);
break;
case 0x15:
filter_cutoff_.load<0, 3>(value);
update_filter();
break;
case 0x16:
filter_cutoff_.load<3>(value);
update_filter();
break;
case 0x17:
filter_channels_ = value;
filter_resonance_ = value >> 4;
update_filter();
break;
case 0x18:
volume_ = value & 0x0f;
filter_mode_ = value >> 4;
voice3_disable_ = value & 0x80;
update_filter();
break;
}
});
}
void SID::set_potentometer_input(const int index, const uint8_t value) {
potentometers_[index] = value;
}
void SID::update_filter() {
using Type = SignalProcessing::BiquadFilter::Type;
Type type = Type::AllPass;
switch(filter_mode_.get()) {
case 0:
filter_ = SignalProcessing::BiquadFilter();
return;
case 1:
case 3: type = Type::LowPass; break;
case 2: type = Type::BandPass; break;
case 5: type = Type::Notch; break;
case 4:
case 6: type = Type::HighPass; break;
case 7: type = Type::AllPass; break;
}
filter_.configure(
type,
1'000'000.0f,
30.0f + float(filter_cutoff_.get()) * 5.8f,
0.707f + float(filter_resonance_.get()) * 0.2862f,
6.0f,
true
);
// Filter cutoff: the data sheet provides that it is linear, and "approximate Cutoff Frequency
// ranges between 30Hz and 12KHz [with recommended externally-supplied capacitors]."
//
// It's an 11-bit number, so the above is "approximate"ly right.
// Resonance: a complete from-thin-air guess. The data sheet says merely:
//
// "There are 16 Resonance settings ranging from about 0.707 (Critical Damping) for a count of 0
// to a maximum for a count of 15"
//
// i.e. no information is given on the maximum. I've taken it to be 5-ish per commentary on more general sites
// that 5 is a typical ceiling for the resonance factor.
}
uint8_t SID::read(const Numeric::SizedInt<5> address) {
switch(address.get()) {
default: return last_write_;
case 0x19: return potentometers_[0];
case 0x1a: return potentometers_[1];
case 0x1b:
case 0x1c:
// Ensure all channels are entirely up to date.
audio_queue_.spin_flush();
return (address == 0x1c) ? voices_[2].adsr.envelope : uint8_t(voices_[2].output(voices_[1]) >> 4);
}
}
// MARK: - Oscillators.
void Voice::Oscillator::reset_phase() {
phase = PhaseReload;
}
bool Voice::Oscillator::did_raise_b23() const {
return previous_phase > phase;
}
bool Voice::Oscillator::did_raise_b19() const {
static constexpr int NoiseBit = 1 << (19 + 8);
return (previous_phase ^ phase) & phase & NoiseBit;
}
uint16_t Voice::Oscillator::sawtooth_output() const {
return (phase >> 20) ^ 0x800;
}
// MARK: - Noise generator.
uint16_t Voice::NoiseGenerator::output() const {
// Uses bits: 20, 18, 14, 11, 9, 5, 2 and 0, plus four more zero bits.
const uint16_t output =
((noise >> 9) & 0b1000'0000'0000) | // b20 -> b11
((noise >> 8) & 0b0100'0000'0000) | // b18 -> b10
((noise >> 5) & 0b0010'0000'0000) | // b14 -> b9
((noise >> 3) & 0b0001'0000'0000) | // b11 -> b8
((noise >> 2) & 0b0000'1000'0000) | // b9 -> b7
((noise << 1) & 0b0000'0100'0000) | // b5 -> b6
((noise << 3) & 0b0000'0010'0000) | // b2 -> b5
((noise << 4) & 0b0000'0001'0000); // b0 -> b4
assert(output <= Voice::MaxWaveformValue);
return output;
}
void Voice::NoiseGenerator::update(const bool test) {
noise =
(noise << 1) |
(((noise >> 17) ^ ((noise >> 22) | test)) & 1);
}
// MARK: - ADSR.
void Voice::ADSR::set_phase(const Phase new_phase) {
static constexpr uint16_t rate_prescaler[] = {
9, 32, 63, 95, 149, 220, 267, 313, 392, 977, 1954, 3126, 3907, 11720, 19532, 31251
};
static_assert(sizeof(rate_prescaler) / sizeof(*rate_prescaler) == 16);
phase = new_phase;
switch(phase) {
case Phase::Attack: rate_counter_target = rate_prescaler[attack.get()]; break;
case Phase::DecayAndHold: rate_counter_target = rate_prescaler[decay.get()]; break;
case Phase::Release: rate_counter_target = rate_prescaler[release.get()]; break;
}
}
// MARK: - Voices.
void Voice::set_control(const uint8_t new_control) {
const bool old_gate = gate();
control = new_control;
if(gate() && !old_gate) {
adsr.set_phase(ADSR::Phase::Attack);
} else if(!gate() && old_gate) {
adsr.set_phase(ADSR::Phase::Release);
}
}
bool Voice::noise() const { return control.bit<7>(); }
bool Voice::pulse() const { return control.bit<6>(); }
bool Voice::sawtooth() const { return control.bit<5>(); }
bool Voice::triangle() const { return control.bit<4>(); }
bool Voice::test() const { return control.bit<3>(); }
bool Voice::ring_mod() const { return control.bit<2>(); }
bool Voice::sync() const { return control.bit<1>(); }
bool Voice::gate() const { return control.bit<0>(); }
void Voice::update() {
// Oscillator.
oscillator.previous_phase = oscillator.phase;
if(test()) {
oscillator.phase = 0;
} else {
oscillator.phase += oscillator.pitch;
if(oscillator.did_raise_b19()) {
noise_generator.update(test());
}
}
// ADSR.
// First prescalar, which is a function of the programmer-set rate.
++ adsr.rate_counter;
if(adsr.rate_counter == adsr.rate_counter_target) {
adsr.rate_counter = 0;
// Second prescalar, which approximates an exponential.
static constexpr uint8_t exponential_prescaler[] = {
1, // 0
30, 30, 30, 30, 30, 30, // 16
16, 16, 16, 16, 16, 16, 16, 16, // 714
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, // 1526
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, // 2754
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 5594
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1,
};
static_assert(sizeof(exponential_prescaler) == 256);
static_assert(exponential_prescaler[0] == 1);
static_assert(exponential_prescaler[1] == 30);
static_assert(exponential_prescaler[6] == 30);
static_assert(exponential_prescaler[7] == 16);
static_assert(exponential_prescaler[14] == 16);
static_assert(exponential_prescaler[15] == 8);
static_assert(exponential_prescaler[26] == 8);
static_assert(exponential_prescaler[27] == 4);
static_assert(exponential_prescaler[54] == 4);
static_assert(exponential_prescaler[55] == 2);
static_assert(exponential_prescaler[94] == 2);
static_assert(exponential_prescaler[95] == 1);
static_assert(exponential_prescaler[255] == 1);
if(adsr.phase == ADSR::Phase::Attack) {
++adsr.envelope;
// TODO: what really resets the exponential counter? If anything?
adsr.exponential_counter = 0;
if(adsr.envelope == 0xff) {
adsr.set_phase(ADSR::Phase::DecayAndHold);
}
} else {
++adsr.exponential_counter;
if(adsr.exponential_counter == exponential_prescaler[adsr.envelope]) {
adsr.exponential_counter = 0;
if(adsr.envelope && (adsr.envelope != adsr.sustain || adsr.phase != ADSR::Phase::DecayAndHold)) {
--adsr.envelope;
}
}
}
}
}
void Voice::synchronise(const Voice &prior) {
// Only oscillator work to do here.
if(
sync() &&
prior.oscillator.did_raise_b23()
) {
oscillator.phase = Oscillator::PhaseReload;
}
}
uint16_t Voice::pulse_output() const {
return (
(oscillator.phase ^ 0x8000'0000) < oscillator.pulse_width
) ? 0 : MaxWaveformValue;
}
uint16_t Voice::triangle_output(const Voice &prior) const {
const uint16_t sawtooth = oscillator.sawtooth_output();
const uint16_t xor_mask1 = sawtooth;
const uint16_t xor_mask2 = ring_mod() ? prior.sawtooth() : 0;
const uint16_t xor_mask = ((xor_mask1 ^ xor_mask2) & 0x800) ? 0xfff : 0x000;
return ((sawtooth << 1) ^ xor_mask) & 0xfff;
}
uint16_t Voice::output(const Voice &prior) const {
// TODO: true composite waves.
//
// My current understanding on this: if multiple waveforms are enabled, the pull to zero beats the
// pull to one on any line where the two compete. But the twist is that the lines are not necessarily
// one per bit since they lead to a common ground. Ummm, I think.
//
// Anyway, first pass: logical AND. It's not right. It will temporarily do.
uint16_t output = MaxWaveformValue;
if(pulse()) output &= pulse_output();
if(sawtooth()) output &= oscillator.sawtooth_output();
if(triangle()) output &= triangle_output(prior);
if(noise()) output &= noise_generator.output();
return (output * adsr.envelope) / 255;
}
// MARK: - Wave generation
void SID::set_sample_volume_range(const std::int16_t range) {
range_ = range;
}
bool SID::is_zero_level() const {
return false;
}
template <Outputs::Speaker::Action action>
void SID::apply_samples(const std::size_t number_of_samples, Outputs::Speaker::MonoSample *const target) {
for(std::size_t c = 0; c < number_of_samples; c++) {
// Advance phase.
voices_[0].update();
voices_[1].update();
voices_[2].update();
// Apply hard synchronisations.
voices_[0].synchronise(voices_[2]);
voices_[1].synchronise(voices_[0]);
voices_[2].synchronise(voices_[1]);
// Construct filtered and unfiltered output.
const uint16_t outputs[3] = {
voices_[0].output(voices_[2]),
voices_[1].output(voices_[0]),
voices_[2].output(voices_[1]),
};
const uint16_t direct_sample =
(filter_channels_.bit<0>() ? 0 : outputs[0]) +
(filter_channels_.bit<1>() ? 0 : outputs[1]) +
(filter_channels_.bit<2>() || voice3_disable_ ? 0 : outputs[2]);
const int16_t filtered_sample =
filter_.apply(
(filter_channels_.bit<0>() ? outputs[0] : 0) +
(filter_channels_.bit<1>() ? outputs[1] : 0) +
(filter_channels_.bit<2>() ? outputs[2] : 0)
);
// Sum, apply volume and output.
const auto sample = output_filter_.apply(int16_t(
(
volume_ * (
direct_sample +
filtered_sample
- 227 // DC offset.
)
- 88732
) / 3
));
// Maximum range of above: 15 * (4095 * 3 - 227) = [-3405, 180870]
// So subtracting 88732 will move to the centre of the range, and 3 is the smallest
// integer that avoids clipping.
Outputs::Speaker::apply<action>(
target[c],
Outputs::Speaker::MonoSample((sample * range_) >> 16)
);
}
}
template void SID::apply_samples<Outputs::Speaker::Action::Mix>(
std::size_t, Outputs::Speaker::MonoSample *);
template void SID::apply_samples<Outputs::Speaker::Action::Store>(
std::size_t, Outputs::Speaker::MonoSample *);
template void SID::apply_samples<Outputs::Speaker::Action::Ignore>(
std::size_t, Outputs::Speaker::MonoSample *);

129
Components/SID/SID.hpp Normal file
View File

@@ -0,0 +1,129 @@
//
// SID.hpp
// Clock Signal
//
// Created by Thomas Harte on 07/11/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#pragma once
#include "Numeric/SizedInt.hpp"
#include "Concurrency/AsyncTaskQueue.hpp"
#include "Outputs/Speaker/Implementation/BufferSource.hpp"
#include "SignalProcessing/BiquadFilter.hpp"
namespace MOS::SID {
struct Voice {
static constexpr uint16_t MaxWaveformValue = (1 << 12) - 1;
struct Oscillator {
// Programmer inputs.
uint32_t pitch = 0;
uint32_t pulse_width = 0;
// State.
//
// A real SID has a 24-bit phase counter and does various things when the top bit transitions from 0 to 1.
// This implementation maintains a 32-bit phase counter in which the low byte is unused and the top bit
// is inverted. That saves the cost of any masking and makes the 0 -> 1 transition test actually a 1 -> 0
// transition test, which can be phrased simply as after < before. Sadly overflow of signed integers is
// still undefined behaviour in C++ at the time of writing.
static constexpr uint32_t PhaseReload = 0x8000'0000;
uint32_t phase = PhaseReload;
uint32_t previous_phase = PhaseReload;
void reset_phase();
bool did_raise_b23() const;
bool did_raise_b19() const;
uint16_t sawtooth_output() const;
} oscillator;
struct ADSR {
// Programmer inputs.
Numeric::SizedInt<4> attack;
Numeric::SizedInt<4> decay;
Numeric::SizedInt<4> release;
Numeric::SizedInt<8> sustain;
// State.
enum class Phase {
Attack,
DecayAndHold,
Release,
} phase = Phase::Release;
Numeric::SizedInt<15> rate_counter;
Numeric::SizedInt<15> rate_counter_target;
uint8_t exponential_counter;
uint8_t envelope;
void set_phase(const Phase);
} adsr;
struct NoiseGenerator {
static constexpr uint32_t NoiseReload = 0x7'ffff;
uint32_t noise = NoiseReload;
uint16_t output() const;
void update(const bool test);
} noise_generator;
void set_control(const uint8_t);
void update();
void synchronise(const Voice &prior);
uint16_t output(const Voice &prior) const;
private:
Numeric::SizedInt<8> control;
bool noise() const;
bool pulse() const;
bool sawtooth() const;
bool triangle() const;
bool test() const;
bool ring_mod() const;
bool sync() const;
bool gate() const;
uint16_t pulse_output() const;
uint16_t triangle_output(const Voice &prior) const;
};
class SID: public Outputs::Speaker::BufferSource<SID, false> {
public:
SID(Concurrency::AsyncTaskQueue<false> &audio_queue);
void write(Numeric::SizedInt<5> address, uint8_t value);
uint8_t read(Numeric::SizedInt<5> address);
void set_potentometer_input(int index, uint8_t value);
// Outputs::Speaker::BufferSource.
template <Outputs::Speaker::Action action>
void apply_samples(std::size_t, Outputs::Speaker::MonoSample *);
bool is_zero_level() const;
void set_sample_volume_range(std::int16_t);
private:
Concurrency::AsyncTaskQueue<false> &audio_queue_;
Voice voices_[3];
uint8_t last_write_;
int16_t range_ = 0;
uint8_t volume_ = 0;
SignalProcessing::BiquadFilter filter_;
Numeric::SizedInt<11> filter_cutoff_;
Numeric::SizedInt<4> filter_resonance_;
Numeric::SizedInt<4> filter_channels_;
Numeric::SizedInt<3> filter_mode_;
bool voice3_disable_;
void update_filter();
SignalProcessing::BiquadFilter output_filter_;
uint8_t potentometers_[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,14 +70,19 @@ 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) {
start();
start_impl();
}
}
@@ -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) {
@@ -97,8 +108,15 @@ public:
}
}
/// @returns The number of items currently enqueued.
size_t size() {
const std::lock_guard guard(condition_mutex_);
return actions_.size();
}
/// Causes any enqueued actions that are not yet scheduled to be scheduled.
void perform() {
static_assert(!perform_automatically);
if(actions_.empty()) {
return;
}
@@ -111,7 +129,7 @@ public:
/// The queue cannot be restarted; this is a destructive action.
void stop() {
if(thread_.joinable()) {
should_quit_ = true;
should_quit_.store(true, std::memory_order_relaxed);
enqueue([] {});
if constexpr (!perform_automatically) {
perform();
@@ -124,36 +142,13 @@ public:
///
/// This is not guaranteed safely to restart a stopped queue.
void start() {
thread_ = std::thread{
[this] {
ActionVector actions;
// Continue until told to quit.
while(!should_quit_) {
// Wait for new actions to be signalled, and grab them.
std::unique_lock lock(condition_mutex_);
while(actions_.empty() && !should_quit_) {
condition_.wait(lock);
}
std::swap(actions, actions_);
lock.unlock();
// Update to now (which is possibly a no-op).
TaskQueueStorage<Performer>::update();
// Perform the actions and destroy them.
for(const auto &action: actions) {
action();
}
actions.clear();
}
}
};
static_assert(!start_immediately);
start_impl();
}
/// Schedules any remaining unscheduled work, then blocks synchronously
/// until all scheduled work has been performed.
void flush() {
void lock_flush() {
std::mutex flush_mutex;
std::condition_variable flush_condition;
bool has_run = false;
@@ -172,11 +167,58 @@ public:
flush_condition.wait(lock, [&has_run] { return has_run; });
}
/// Schedules any remaining unscheduled work, then spins
/// until all scheduled work has been performed, placing a memory barrier
/// in between.
void spin_flush() {
std::atomic<bool> has_run = false;
enqueue([&has_run] () {
has_run.store(true, std::memory_order::release);
});
if constexpr (!perform_automatically) {
perform();
}
while(!has_run.load(std::memory_order::acquire));
}
~AsyncTaskQueue() {
stop();
}
private:
void start_impl() {
thread_ = std::thread{
[this] {
ActionVector actions;
// Continue until told to quit.
while(!should_quit_.load(std::memory_order_relaxed)) {
// Wait for new actions to be signalled, and grab them.
std::unique_lock lock(condition_mutex_);
condition_.wait(lock, [&] {
return !actions_.empty() || should_quit_.load(std::memory_order_relaxed);
});
std::swap(actions, actions_);
lock.unlock();
// Update to now (which is possibly a no-op).
TaskQueueStorage<Performer>::update();
// Perform the actions and destroy them.
for(const auto &action: actions) {
action();
}
actions.clear();
}
}
};
}
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

@@ -24,6 +24,7 @@
#include "Components/6522/6522.hpp"
#include "Components/6845/CRTC6845.hpp"
#include "Components/6850/6850.hpp"
#include "Components/SID/SID.hpp"
#include "Components/SAA5050/SAA5050.hpp"
#include "Components/SN76489/SN76489.hpp"
#include "Components/uPD7002/uPD7002.hpp"
@@ -35,6 +36,7 @@
#include "Outputs/Log.hpp"
#include "Outputs/CRT/CRT.hpp"
#include "Outputs/Speaker/Implementation/CompoundSource.hpp"
#include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#include "Concurrency/AsyncTaskQueue.hpp"
@@ -107,22 +109,48 @@ private:
/*!
Combines an SN76489 with an appropriate asynchronous queue and filtering speaker.
*/
// TODO: generalise the below and clean up across the project.
template <bool has_beebsid>
struct Audio {
private:
using CompoundSource = Outputs::Speaker::CompoundSource<TI::SN76489, MOS::SID::SID>;
using Source = std::conditional_t<has_beebsid, CompoundSource, TI::SN76489>;
using Speaker = Outputs::Speaker::PullLowpass<Source>;
Source &speaker_source() {
if constexpr (has_beebsid) {
return compound_;
} else {
return sn76489_;
}
}
public:
Audio() :
sn76489_(TI::SN76489::Personality::SN76489, audio_queue_, 2),
speaker_(sn76489_)
sn76489_(TI::SN76489::Personality::SN76489, audio_queue_, 4),
sid_(audio_queue_),
compound_(sn76489_, sid_),
speaker_(speaker_source())
{
// Combined with the additional divider specified above, implies this chip is clocked at 4Mhz.
speaker_.set_input_rate(2'000'000.0f);
// Combined with the additional divider specified above, implies the SN76489 is clocked at 4Mhz.
speaker_.set_input_rate(1'000'000.0f);
}
~Audio() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
TI::SN76489 *operator ->() {
speaker_.run_for(audio_queue_, time_since_update_.flush<Cycles>());
return &sn76489_;
template <typename TargetT>
TargetT &get() {
post_time();
if constexpr (std::is_same_v<TargetT, TI::SN76489>) {
return sn76489_;
}
if constexpr (std::is_same_v<TargetT, MOS::SID::SID>) {
return sid_;
}
}
void operator +=(const Cycles duration) {
@@ -130,7 +158,7 @@ struct Audio {
}
void flush() {
speaker_.run_for(audio_queue_, time_since_update_.flush<Cycles>());
post_time();
audio_queue_.perform();
}
@@ -138,10 +166,20 @@ struct Audio {
return &speaker_;
}
size_t queue_size() {
return audio_queue_.size();
}
private:
void post_time() {
speaker_.run_for(audio_queue_, time_since_update_.divide(Cycles(2)));
}
Concurrency::AsyncTaskQueue<false> audio_queue_;
TI::SN76489 sn76489_;
Outputs::Speaker::PullLowpass<TI::SN76489> speaker_;
MOS::SID::SID sid_;
CompoundSource compound_;
Outputs::Speaker::PullLowpass<Source> speaker_;
Cycles time_since_update_;
};
@@ -174,19 +212,20 @@ protected:
/*!
Models the system VIA, which connects to the SN76489 and the keyboard.
*/
struct SystemVIAPortHandler;
using SystemVIA = MOS::MOS6522::MOS6522<SystemVIAPortHandler>;
struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
struct Delegate {
virtual void strobe_lightpen() = 0;
};
struct VSyncReceiver {
virtual void set_vsync(bool) = 0;
};
struct SystemVIADelegate {
virtual void strobe_lightpen() = 0;
};
template <typename AudioT>
struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler, public VSyncReceiver {
SystemVIAPortHandler(
Audio &audio,
AudioT &audio,
VideoBaseAddress &video_base,
SystemVIA &via,
Delegate &delegate,
MOS::MOS6522::MOS6522<SystemVIAPortHandler<AudioT>> &via,
SystemVIADelegate &delegate,
const std::vector<std::unique_ptr<Inputs::Joystick>> &joysticks,
const bool run_disk
) :
@@ -221,7 +260,7 @@ struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
// Check for a strobe on the audio output.
if((old_latch^latch_) & old_latch & LatchFlags::WriteToSN76489) {
audio_->write(port_a_output_);
audio_.template get<TI::SN76489>().write(port_a_output_);
}
// Pass on the video wraparound/base.
@@ -335,10 +374,10 @@ private:
uint8_t port_a_output_ = 0;
bool previous_cb2_ = false;
Audio &audio_;
AudioT &audio_;
VideoBaseAddress &video_base_;
SystemVIA &via_;
MOS::MOS6522::MOS6522<SystemVIAPortHandler<AudioT>> &via_;
// MARK: - Keyboard state and helpers.
@@ -369,7 +408,7 @@ private:
}
} ()).to_ulong() & 0xfe; // Discard the first row.
via_.set_control_line_input<MOS::MOS6522::Port::A, MOS::MOS6522::Line::Two>(state);
via_.template set_control_line_input<MOS::MOS6522::Port::A, MOS::MOS6522::Line::Two>(state);
}
static inline const std::string caps_led = "CAPS";
@@ -379,7 +418,11 @@ private:
Activity::Observer *activity_observer_ = nullptr;
const std::vector<std::unique_ptr<Inputs::Joystick>> &joysticks_;
Delegate &delegate_;
SystemVIADelegate &delegate_;
void set_vsync(const bool vsync) override {
via_.template set_control_line_input<MOS::MOS6522::Port::A, MOS::MOS6522::Line::One>(vsync);
}
};
// MARK: - CRTC output.
@@ -389,10 +432,10 @@ private:
*/
class CRTCBusHandler: public VideoBaseAddress {
public:
CRTCBusHandler(const uint8_t *const ram, SystemVIA &system_via) :
CRTCBusHandler(const uint8_t *const ram, VSyncReceiver &vsync_receiver) :
crt_(1024, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red1Green1Blue1),
ram_(ram),
system_via_(system_via)
vsync_receiver_(vsync_receiver)
{}
void set_dynamic_framing(const bool enable) {
@@ -440,7 +483,10 @@ public:
static_assert(!(PixelAllocationUnit % 16));
static_assert(!(PixelAllocationUnit % 12));
system_via_.set_control_line_input<MOS::MOS6522::Port::A, MOS::MOS6522::Line::One>(state.vsync);
if(state.vsync != vsync_) {
vsync_receiver_.set_vsync(state.vsync);
vsync_ = state.vsync;
}
// Count cycles since horizontal sync to insert a colour burst.
// TODO: this is copy/pasted from the CPC. How does the BBC do it?
@@ -675,7 +721,8 @@ private:
}
const uint8_t *const ram_ = nullptr;
SystemVIA &system_via_;
VSyncReceiver &vsync_receiver_;
bool vsync_ = false;
Mullard::SAA5050Serialiser saa5050_serialiser_;
};
@@ -687,38 +734,25 @@ using CRTC = Motorola::CRTC::CRTC6845<
// MARK: - Tube.
template <typename HostT, TubeProcessor> struct Tube;
template <typename HostT, TubeProcessor tube_processor>
struct Tube {
using TubeULA = Acorn::Tube::ULA<HostT>;
TubeULA ula;
Acorn::Tube::Processor<TubeULA, tube_processor> processor;
Tube(HostT &owner) :
ula(owner),
processor(ula) {}
};
template <typename HostT>
struct Tube<HostT, TubeProcessor::None> {
Tube(HostT &) {}
};
template <typename HostT>
struct Tube<HostT, TubeProcessor::WDC65C02> {
using TubeULA = Acorn::Tube::ULA<HostT>;
TubeULA ula;
Acorn::Tube::Tube6502<TubeULA> processor;
Tube(HostT &owner) :
ula(owner),
processor(ula) {}
};
template <typename HostT>
struct Tube<HostT, TubeProcessor::Z80> {
using TubeULA = Acorn::Tube::ULA<HostT>;
TubeULA ula;
Acorn::Tube::TubeZ80<TubeULA> processor;
Tube(HostT &owner) :
ula(owner),
processor(ula) {}
};
// MARK: - ConcreteMachine.
template <TubeProcessor tube_processor, bool has_1770>
template <TubeProcessor tube_processor, bool has_1770, bool has_beebsid>
class ConcreteMachine:
public Activity::Source,
public Configurable::Device,
@@ -731,7 +765,7 @@ class ConcreteMachine:
public MachineTypes::TimedMachine,
public MOS::MOS6522::IRQDelegatePortHandler::Delegate,
public NEC::uPD7002::Delegate,
public SystemVIAPortHandler::Delegate,
public SystemVIADelegate,
public Utility::TypeRecipient<CharacterMapper>,
public WD::WD1770::Delegate
{
@@ -744,7 +778,7 @@ public:
system_via_port_handler_(audio_, crtc_bus_handler_, system_via_, *this, joysticks_, target.should_shift_restart),
user_via_(user_via_port_handler_),
system_via_(system_via_port_handler_),
crtc_bus_handler_(ram_.data(), system_via_),
crtc_bus_handler_(ram_.data(), system_via_port_handler_),
crtc_(crtc_bus_handler_),
acia_(HalfCycles(2'000'000)), // TODO: look up real ACIA clock rate.
adc_(HalfCycles(2'000'000)),
@@ -1003,6 +1037,12 @@ public:
}
break;
}
} else if(has_beebsid && address >= 0xfc20 && address < 0xfc40) {
if constexpr (is_read(operation)) {
value = audio_.template get<MOS::SID::SID>().read(+address);
} else {
audio_.template get<MOS::SID::SID>().write(+address, value);
}
} else {
Logger::error()
.append("Unhandled IO %s at %04x", is_read(operation) ? "read" : "write", address)
@@ -1058,8 +1098,7 @@ private:
return crtc_bus_handler_.get_scaled_scan_status();
}
// MARK: - SystemVIAPortHandler::Delegate.
// MARK: - SystemVIADelegate.
void strobe_lightpen() override {
crtc_.trigger_light_pen();
}
@@ -1142,7 +1181,10 @@ private:
}
void flush_output(const int outputs) final {
if(outputs & Output::Audio) {
// TODO: I think there's an infrastructural bug here on macOS; if the audio output has stalled out,
// the outer wrapper won't ask for an audio flush, which means the queue will never try to start,
// and the audio queue will just fill indefinitely. Could this be the mythical 'leak'?
if(outputs & Output::Audio || audio_.queue_size() > 200) {
audio_.flush();
}
}
@@ -1154,7 +1196,7 @@ private:
// MARK: - uPD7002::Delegate.
void did_change_interrupt_status(NEC::uPD7002 &) override {
system_via_.set_control_line_input<MOS::MOS6522::Port::B, MOS::MOS6522::Line::One>(adc_.interrupt());
system_via_.template set_control_line_input<MOS::MOS6522::Port::B, MOS::MOS6522::Line::One>(adc_.interrupt());
}
// MARK: - MediaTarget.
@@ -1213,9 +1255,9 @@ private:
CPU::MOS6502Mk2::Processor<CPU::MOS6502Mk2::Model::M6502, M6502Traits> m6502_;
UserVIAPortHandler user_via_port_handler_;
SystemVIAPortHandler system_via_port_handler_;
SystemVIAPortHandler<Audio<has_beebsid>> system_via_port_handler_;
UserVIA user_via_;
SystemVIA system_via_;
MOS::MOS6522::MOS6522<SystemVIAPortHandler<Audio<has_beebsid>>> system_via_;
void update_irq_line() {
const bool tube_irq =
@@ -1234,7 +1276,7 @@ private:
);
}
Audio audio_;
Audio<has_beebsid> audio_;
CRTCBusHandler crtc_bus_handler_;
CRTC crtc_;
@@ -1288,12 +1330,21 @@ using namespace BBCMicro;
namespace {
using Target = Analyser::Static::Acorn::BBCMicroTarget;
template <Target::TubeProcessor processor, bool has_1770>
std::unique_ptr<Machine> machine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher) {
if(target.has_beebsid) {
return std::make_unique<BBCMicro::ConcreteMachine<processor, has_1770, true>>(target, rom_fetcher);
} else {
return std::make_unique<BBCMicro::ConcreteMachine<processor, has_1770, false>>(target, rom_fetcher);
}
}
template <Target::TubeProcessor processor>
std::unique_ptr<Machine> machine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher) {
if(target.has_1770dfs || target.has_adfs) {
return std::make_unique<BBCMicro::ConcreteMachine<processor, true>>(target, rom_fetcher);
return machine<processor, true>(target, rom_fetcher);
} else {
return std::make_unique<BBCMicro::ConcreteMachine<processor, false>>(target, rom_fetcher);
return machine<processor, false>(target, rom_fetcher);
}
}
}

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_.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

@@ -8,6 +8,8 @@
#pragma once
#include "TubeProcessor.hpp"
#include "Processors/6502Mk2/6502Mk2.hpp"
#include "Machines/Utility/ROMCatalogue.hpp"
@@ -16,7 +18,7 @@
namespace Acorn::Tube {
template <typename ULAT>
struct Tube6502 {
class Processor<ULAT, TubeProcessor::WDC65C02> {
public:
static constexpr auto ROM = ROM::Name::BBCMicro6502Tube110;
void set_rom(std::vector<uint8_t> source) {
@@ -24,7 +26,7 @@ public:
std::copy(source.begin(), source.end(), rom_);
}
Tube6502(ULAT &ula) : m6502_(*this), ula_(ula) {}
Processor(ULAT &ula) : m6502_(*this), ula_(ula) {}
// By convention, these are cycles relative to the host's 2Mhz bus.
// Multiply by 3/2 to turn that into the tube 6502's usual 3Mhz bus.
@@ -68,7 +70,7 @@ private:
struct M6502Traits {
static constexpr auto uses_ready_line = false;
static constexpr auto pause_precision = CPU::MOS6502Mk2::PausePrecision::AnyCycle;
using BusHandlerT = Tube6502;
using BusHandlerT = Processor;
};
CPU::MOS6502Mk2::Processor<CPU::MOS6502Mk2::Model::WDC65C02, M6502Traits> m6502_;
bool rom_visible_ = true;

View File

@@ -0,0 +1,18 @@
//
// TubeProcessor.hpp
// Clock Signal
//
// Created by Thomas Harte on 07/11/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#pragma once
#include "Analyser/Static/Acorn/Target.hpp"
namespace Acorn::Tube {
using TubeProcessor = Analyser::Static::Acorn::BBCMicroTarget::TubeProcessor;
template <typename ULAT, TubeProcessor> class Processor;
}

View File

@@ -8,6 +8,8 @@
#pragma once
#include "TubeProcessor.hpp"
#include "Processors/Z80/Z80.hpp"
#include "Machines/Utility/ROMCatalogue.hpp"
@@ -16,7 +18,7 @@
namespace Acorn::Tube {
template <typename ULAT>
struct TubeZ80: public CPU::Z80::BusHandler {
class Processor<ULAT, TubeProcessor::Z80>: public CPU::Z80::BusHandler {
public:
static constexpr auto ROM = ROM::Name::BBCMicroZ80Tube122;
void set_rom(std::vector<uint8_t> rom) {
@@ -24,7 +26,7 @@ public:
std::copy(rom.begin(), rom.end(), std::begin(rom_));
}
TubeZ80(ULAT &ula) : z80_(*this), ula_(ula) {}
Processor(ULAT &ula) : z80_(*this), ula_(ula) {}
void run_for(const Cycles cycles) {
// Map from 2Mhz to 6Mhz.
@@ -84,7 +86,7 @@ public:
private:
CPU::Z80::Processor<TubeZ80, false, false> z80_;
CPU::Z80::Processor<Processor, false, false> z80_;
bool rom_visible_ = true;
uint8_t rom_[4096];

View File

@@ -127,7 +127,7 @@ public:
}
~AYDeferrer() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
/// Adds @c half_cycles half cycles to the amount of time that has passed.

View File

@@ -678,7 +678,7 @@ public:
}
~ConcreteMachine() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {

View File

@@ -210,7 +210,7 @@ public:
}
~ConcreteMachine() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
void run_for(const Cycles cycles) override {

View File

@@ -172,7 +172,7 @@ public:
}
~ConcreteMachine() {
audio_.queue.flush();
audio_.queue.lock_flush();
}
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {

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_.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

@@ -144,7 +144,7 @@ public:
}
~ConcreteMachine() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
// MARK: CRTMachine::Machine

View File

@@ -168,7 +168,7 @@ public:
}
~ConcreteMachine() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {

View File

@@ -17,7 +17,10 @@
using namespace Commodore::C1540;
namespace {
ROM::Name rom_name(Personality personality) {
// MARK: - Construction, including ROM requests.
ROM::Name rom_name(const Personality personality) {
switch(personality) {
default:
case Personality::C1540: return ROM::Name::Commodore1540;
@@ -26,11 +29,11 @@ ROM::Name rom_name(Personality personality) {
}
}
ROM::Request Machine::rom_request(Personality personality) {
ROM::Request Machine::rom_request(const Personality personality) {
return ROM::Request(rom_name(personality));
}
MachineBase::MachineBase(Personality personality, const ROM::Map &roms) :
MachineBase::MachineBase(const Personality personality, const ROM::Map &roms) :
Storage::Disk::Controller(1000000),
m6502_(*this),
drive_VIA_(drive_VIA_port_handler_),
@@ -58,14 +61,13 @@ MachineBase::MachineBase(Personality personality, const ROM::Map &roms) :
std::memcpy(rom_, rom->second.data(), std::min(sizeof(rom_), rom->second.size()));
}
Machine::Machine(Personality personality, const ROM::Map &roms) :
Machine::Machine(const Personality personality, const ROM::Map &roms) :
MachineBase(personality, roms) {}
void Machine::set_serial_bus(Commodore::Serial::Bus &serial_bus) {
Commodore::Serial::attach(serial_port_, serial_bus);
}
// MARK: - 6502 bus.
Cycles MachineBase::perform_bus_operation(CPU::MOS6502::BusOperation operation, uint16_t address, uint8_t *value) {
template <CPU::MOS6502Mk2::BusOperation operation, typename AddressT>
Cycles MachineBase::perform(const AddressT address, CPU::MOS6502Mk2::data_t<operation> value) {
/*
Memory map (given that I'm unsure yet on any potential mirroring):
@@ -75,24 +77,28 @@ Cycles MachineBase::perform_bus_operation(CPU::MOS6502::BusOperation operation,
0xc000-0xffff ROM
*/
if(address < 0x800) {
if(is_read(operation))
*value = ram_[address];
if constexpr (is_read(operation))
value = ram_[address];
else
ram_[address] = *value;
ram_[address] = value;
} else if(address >= 0xc000) {
if(is_read(operation)) {
*value = rom_[address & 0x3fff];
if constexpr (is_read(operation)) {
value = rom_[address & 0x3fff];
}
} else if(address >= 0x1800 && address <= 0x180f) {
if(is_read(operation))
*value = serial_port_VIA_.read(address);
if constexpr (is_read(operation))
value = serial_port_VIA_.read(address);
else
serial_port_VIA_.write(address, *value);
serial_port_VIA_.write(address, value);
} else if(address >= 0x1c00 && address <= 0x1c0f) {
if(is_read(operation))
*value = drive_VIA_.read(address);
if constexpr (is_read(operation))
value = drive_VIA_.read(address);
else
drive_VIA_.write(address, *value);
drive_VIA_.write(address, value);
} else {
if constexpr (is_read(operation)) {
value = 0xff;
}
}
serial_port_VIA_.run_for(Cycles(1));
@@ -101,34 +107,42 @@ Cycles MachineBase::perform_bus_operation(CPU::MOS6502::BusOperation operation,
return Cycles(1);
}
void Machine::set_disk(std::shared_ptr<Storage::Disk::Disk> disk) {
get_drive().set_disk(disk);
}
void Machine::run_for(const Cycles cycles) {
m6502_.run_for(cycles);
const bool drive_motor = drive_VIA_port_handler_.get_motor_enabled();
const bool drive_motor = drive_VIA_port_handler_.motor_enabled();
get_drive().set_motor_on(drive_motor);
if(drive_motor)
if(drive_motor) {
Storage::Disk::Controller::run_for(cycles);
}
}
void MachineBase::set_activity_observer(Activity::Observer *observer) {
// MARK: - External attachments.
void Machine::set_serial_bus(Commodore::Serial::Bus &serial_bus) {
Commodore::Serial::attach(serial_port_, serial_bus);
}
void Machine::set_disk(std::shared_ptr<Storage::Disk::Disk> disk) {
get_drive().set_disk(disk);
drive_VIA_port_handler_.set_is_read_only(disk->is_read_only());
}
void MachineBase::set_activity_observer(Activity::Observer *const observer) {
drive_VIA_.bus_handler().set_activity_observer(observer);
get_drive().set_activity_observer(observer, "Drive", false);
}
// MARK: - 6522 delegate
// MARK: - 6522 delegate.
void MachineBase::mos6522_did_change_interrupt_status(void *) {
// both VIAs are connected to the IRQ line
m6502_.set_irq_line(serial_port_VIA_.get_interrupt_line() || drive_VIA_.get_interrupt_line());
m6502_.set<CPU::MOS6502Mk2::Line::IRQ>(serial_port_VIA_.get_interrupt_line() || drive_VIA_.get_interrupt_line());
}
// MARK: - Disk drive
// MARK: - Disk drive.
void MachineBase::process_input_bit(int value) {
void MachineBase::process_input_bit(const int value) {
shift_register_ = (shift_register_ << 1) | value;
if((shift_register_ & 0x3ff) == 0x3ff) {
drive_VIA_port_handler_.set_sync_detected(true);
@@ -140,11 +154,11 @@ void MachineBase::process_input_bit(int value) {
if(bit_window_offset_ == 8) {
drive_VIA_port_handler_.set_data_input(uint8_t(shift_register_));
bit_window_offset_ = 0;
if(drive_VIA_port_handler_.get_should_set_overflow()) {
m6502_.set_overflow_line(true);
if(drive_VIA_port_handler_.should_set_overflow()) {
m6502_.set<CPU::MOS6502Mk2::Line::Overflow>(true);
}
}
else m6502_.set_overflow_line(false);
else m6502_.set<CPU::MOS6502Mk2::Line::Overflow>(false);
}
// the 1540 does not recognise index holes
@@ -164,7 +178,9 @@ void MachineBase::drive_via_did_set_data_density(DriveVIA &, const int density)
template <MOS::MOS6522::Port port>
uint8_t SerialPortVIA::get_port_input() const {
if(port) return port_b_;
if(port) {
return port_b_;
}
return 0xff;
}
@@ -203,13 +219,15 @@ void SerialPortVIA::set_serial_port(Commodore::Serial::Port &port) {
void SerialPortVIA::update_data_line() {
// "ATN (Attention) is an input on pin 3 of P2 and P3 that is sensed at PB7 and CA1 of UC3 after being inverted by UA1"
serial_port_->set_output(::Commodore::Serial::Line::Data,
Serial::LineLevel(!data_level_output_ && (attention_level_input_ != attention_acknowledge_level_)));
serial_port_->set_output(
::Commodore::Serial::Line::Data,
Serial::LineLevel(!data_level_output_ && (attention_level_input_ != attention_acknowledge_level_))
);
}
// MARK: - DriveVIA
void DriveVIA::set_delegate(Delegate *delegate) {
void DriveVIA::set_delegate(Delegate *const delegate) {
delegate_ = delegate;
}
@@ -220,18 +238,22 @@ uint8_t DriveVIA::get_port_input() const {
}
void DriveVIA::set_sync_detected(const bool sync_detected) {
port_b_ = (port_b_ & 0x7f) | (sync_detected ? 0x00 : 0x80);
port_b_ = (port_b_ & ~0x80) | (sync_detected ? 0x00 : 0x80);
}
void DriveVIA::set_data_input(uint8_t value) {
void DriveVIA::set_is_read_only(const bool is_read_only) {
port_b_ = (port_b_ & ~0x10) | (is_read_only ? 0x00 : 0x10);
}
void DriveVIA::set_data_input(const uint8_t value) {
port_a_ = value;
}
bool DriveVIA::get_should_set_overflow() {
bool DriveVIA::should_set_overflow() {
return should_set_overflow_;
}
bool DriveVIA::get_motor_enabled() {
bool DriveVIA::motor_enabled() {
return drive_motor_;
}
@@ -240,36 +262,48 @@ void DriveVIA::set_control_line_output(const bool value) {
if(port == MOS::MOS6522::Port::A && line == MOS::MOS6522::Line::Two) {
should_set_overflow_ = value;
}
}
template <MOS::MOS6522::Port port>
void DriveVIA::set_port_output(const uint8_t value, uint8_t) {
if(port) {
if(previous_port_b_output_ != value) {
// Record drive motor state.
drive_motor_ = value&4;
// Check for a head step.
const int step_difference = ((value&3) - (previous_port_b_output_&3))&3;
if(step_difference) {
if(delegate_) delegate_->drive_via_did_step_head(*this, (step_difference == 1) ? 1 : -1);
}
// Check for a change in density.
const int density_difference = (previous_port_b_output_^value) & (3 << 5);
if(density_difference && delegate_) {
delegate_->drive_via_did_set_data_density(*this, (value >> 5)&3);
}
// Post the LED status.
if(observer_) observer_->set_led_status("Drive", value&8);
previous_port_b_output_ = value;
if(port == MOS::MOS6522::Port::B && line == MOS::MOS6522::Line::Two) {
// TODO: 0 = write, 1 = read.
if(!value) {
printf("NOT IMPLEMENTED: write mode\n");
}
}
}
void DriveVIA::set_activity_observer(Activity::Observer *observer) {
template <>
void DriveVIA::set_port_output<MOS::MOS6522::Port::B>(const uint8_t value, uint8_t) {
if(previous_port_b_output_ != value) {
// Record drive motor state.
drive_motor_ = value&4;
// Check for a head step.
const int step_difference = ((value&3) - (previous_port_b_output_&3))&3;
if(step_difference && delegate_) {
delegate_->drive_via_did_step_head(*this, (step_difference == 1) ? 1 : -1);
}
// Check for a change in density.
const int density_difference = (previous_port_b_output_^value) & (3 << 5);
if(density_difference && delegate_) {
delegate_->drive_via_did_set_data_density(*this, (value >> 5)&3);
}
// Post the LED status.
if(observer_) {
observer_->set_led_status("Drive", value&8);
}
previous_port_b_output_ = value;
}
}
template <>
void DriveVIA::set_port_output<MOS::MOS6522::Port::A>(const uint8_t value, uint8_t) {
printf("TODO: output is %02x\n", value);
}
void DriveVIA::set_activity_observer(Activity::Observer *const observer) {
observer_ = observer;
if(observer) {
observer->register_led("Drive");
@@ -277,9 +311,9 @@ void DriveVIA::set_activity_observer(Activity::Observer *observer) {
}
}
// MARK: - SerialPort
// MARK: - SerialPort.
void SerialPort::set_input(Serial::Line line, Serial::LineLevel level) {
void SerialPort::set_input(const Serial::Line line, const Serial::LineLevel level) {
serial_port_via_->set_serial_line_state(line, bool(level), *via_);
}

View File

@@ -8,7 +8,7 @@
#pragma once
#include "Processors/6502/6502.hpp"
#include "Processors/6502Mk2/6502Mk2.hpp"
#include "Components/6522/6522.hpp"
#include "Machines/Commodore/SerialBus.hpp"
@@ -65,15 +65,17 @@ private:
It is wired up such that Port B contains:
Bits 0/1: head step direction
Bit 2: motor control
Bit 3: LED control (TODO)
Bit 3: LED control
Bit 4: write protect photocell status (TODO)
Bits 5/6: read/write density
Bit 7: 0 if sync marks are currently being detected, 1 otherwise.
... and Port A contains the byte most recently read from the disk or the byte next to write to the disk, depending on data direction.
It is implied that CA2 might be used to set processor overflow, CA1 a strobe for data input, and one of the CBs being definitive on
whether the disk head is being told to read or write, but it's unclear and I've yet to investigate. So, TODO.
Elsewhere:
* CA2 might is used to set processor overflow;
* CA1 a strobe for data input; and
* CB2 indicates read/write mode; 1 = read, 0 = write.
*/
class DriveVIA: public MOS::MOS6522::IRQDelegatePortHandler {
public:
@@ -88,8 +90,9 @@ public:
void set_sync_detected(bool);
void set_data_input(uint8_t);
bool get_should_set_overflow();
bool get_motor_enabled();
void set_is_read_only(bool);
bool should_set_overflow();
bool motor_enabled();
template <MOS::MOS6522::Port, MOS::MOS6522::Line>
void set_control_line_output(bool value);
@@ -122,7 +125,6 @@ private:
};
class MachineBase:
public CPU::MOS6502::BusHandler,
public MOS::MOS6522::IRQDelegatePortHandler::Delegate,
public DriveVIA::Delegate,
public Storage::Disk::Controller {
@@ -134,7 +136,8 @@ public:
void set_activity_observer(Activity::Observer *);
// to satisfy CPU::MOS6502::Processor
Cycles perform_bus_operation(CPU::MOS6502::BusOperation, uint16_t address, uint8_t *value);
template <CPU::MOS6502Mk2::BusOperation operation, typename AddressT>
Cycles perform(const AddressT, CPU::MOS6502Mk2::data_t<operation>);
protected:
// to satisfy MOS::MOS6522::Delegate
@@ -144,7 +147,12 @@ protected:
void drive_via_did_step_head(DriveVIA &, int direction) override;
void drive_via_did_set_data_density(DriveVIA &, int density) override;
CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, MachineBase, false> m6502_;
struct M6502Traits {
static constexpr auto uses_ready_line = false;
static constexpr auto pause_precision = CPU::MOS6502Mk2::PausePrecision::AnyCycle;
using BusHandlerT = MachineBase;
};
CPU::MOS6502Mk2::Processor<CPU::MOS6502Mk2::Model::M6502, M6502Traits> m6502_;
uint8_t ram_[0x800];
uint8_t rom_[0x4000];

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

@@ -16,7 +16,7 @@
#include "Machines/MachineTypes.hpp"
#include "Machines/Utility/MemoryFuzzer.hpp"
#include "Processors/6502/6502.hpp"
#include "Processors/6502Mk2/6502Mk2.hpp"
#include "Analyser/Static/Commodore/Target.hpp"
#include "Outputs/Log.hpp"
#include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
@@ -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,20 +234,17 @@ public:
}
~ConcreteMachine() {
audio_queue_.flush();
audio_.stop();
}
// HACK. NOCOMMIT.
// int pulse_num_ = 0;
Cycles perform_bus_operation(
const CPU::MOS6502::BusOperation operation,
const uint16_t address,
uint8_t *const value
) {
template <CPU::MOS6502Mk2::BusOperation operation, typename AddressT>
Cycles perform(const AddressT address, CPU::MOS6502Mk2::data_t<operation> value) {
// Determine from the TED video subsystem the length of this clock cycle as perceived by the 6502,
// relative to the master clock.
const auto length = video_.cycle_length(operation == CPU::MOS6502::BusOperation::Ready);
const auto length = video_.cycle_length(operation == CPU::MOS6502Mk2::BusOperation::Ready);
// Update other subsystems.
advance_timers_and_tape(length);
@@ -261,11 +256,10 @@ public:
c1541_->run_for(c1541_cycles_.divide(media_divider_));
}
time_since_audio_update_ += length;
audio_ += length;
}
if(operation == CPU::MOS6502::BusOperation::Ready) {
if(operation == CPU::MOS6502Mk2::BusOperation::Ready) {
return length;
}
@@ -282,17 +276,17 @@ public:
// b1 = serial clock out and cassette write;
// b0 = serial data out.
if(is_read(operation)) {
if constexpr (is_read(operation)) {
if(!address) {
*value = io_direction_;
value = io_direction_;
} else {
*value = io_input();
value = io_input();
}
} else {
if(!address) {
io_direction_ = *value;
io_direction_ = value;
} else {
io_output_ = *value;
io_output_ = value;
}
const auto output = io_output_ | ~io_direction_;
@@ -323,36 +317,36 @@ public:
// );
// }
if(
use_fast_tape_hack_ &&
operation == CPU::MOS6502Esque::BusOperation::ReadOpcode &&
(
(use_hle && address == 0xe5fd) ||
address == 0xe68b ||
address == 0xe68d
)
) {
// ++pulse_num_;
if(use_hle) {
read_dipole();
}
if constexpr (is_read(operation)) {
if(
use_fast_tape_hack_ &&
operation == CPU::MOS6502Mk2::BusOperation::ReadOpcode &&
(
(use_hle && address == 0xe5fd) ||
address == 0xe68b ||
address == 0xe68d
)
) {
// ++pulse_num_;
if(use_hle) {
read_dipole();
}
// using Flag = CPU::MOS6502::Flag;
// using Register = CPU::MOS6502::Register;
// const auto flags = m6502_.value_of(Register::Flags);
// printf("to %lld: %c%c%c\n",
// tape_player_->serialiser()->offset(),
// flags & Flag::Sign ? 'n' : '-',
// flags & Flag::Overflow ? 'v' : '-',
// flags & Flag::Carry ? 'c' : '-'
// );
*value = 0x60;
} else {
if(is_read(operation)) {
*value = map_.read(address);
// using Flag = CPU::MOS6502::Flag;
// using Register = CPU::MOS6502::Register;
// const auto flags = m6502_.value_of(Register::Flags);
// printf("to %lld: %c%c%c\n",
// tape_player_->serialiser()->offset(),
// flags & Flag::Sign ? 'n' : '-',
// flags & Flag::Overflow ? 'v' : '-',
// flags & Flag::Carry ? 'c' : '-'
// );
value = 0x60;
} else {
map_.write(address) = *value;
value = map_.read(address);
}
} else {
map_.write(address) = value;
}
@@ -393,12 +387,12 @@ public:
// ram_[0x90] = 0;
// ram_[0x93] = 0;
//
// *value = 0x0c; // NOP abs.
// value = 0x0c; // NOP abs.
// }
// }
} else if(address < 0xff00) {
// Miscellaneous hardware. All TODO.
if(is_read(operation)) {
if constexpr (is_read(operation)) {
switch(address & 0xfff0) {
case 0xfd10:
// 6529 parallel port, about which I know only what I've found in kernel ROM disassemblies.
@@ -406,7 +400,7 @@ public:
// If play button is not currently pressed and this read is immediately followed by
// an AND 4, press it. The kernel will deal with motor control subsequently.
if(!play_button_) {
const uint16_t pc = m6502_.value_of(CPU::MOS6502::Register::ProgramCounter);
const uint16_t pc = m6502_.registers().pc.full;
const uint8_t next[] = {
map_.read(pc+0),
map_.read(pc+1),
@@ -422,22 +416,23 @@ public:
}
}
*value = 0xff ^ (play_button_ ? 0x4 :0x0);
value = 0xff ^ (play_button_ ? 0x4 :0x0);
break;
case 0xfdd0:
case 0xfdf0:
*value = uint8_t(address >> 8);
value = uint8_t(address >> 8);
break;
default:
value = 0xff;
Logger::info().append("TODO: read @ %04x", address);
break;
}
} else {
switch(address & 0xfff0) {
case 0xfd30:
keyboard_mask_ = *value;
keyboard_mask_ = value;
break;
case 0xfdd0: {
@@ -447,28 +442,28 @@ public:
} break;
default:
Logger::info().append("TODO: write of %02x @ %04x", *value, address);
Logger::info().append("TODO: write of %02x @ %04x", value, address);
break;
}
}
} else {
const auto pc = m6502_.value_of(CPU::MOS6502::Register::ProgramCounter);
const auto pc = m6502_.registers().pc.full;
const bool is_from_rom =
(rom_is_paged_ && pc >= 0x8000) ||
(pc >= 0x400 && pc < 0x500) ||
(pc >= 0x700 && pc < 0x800);
bool is_hit = true;
if(is_read(operation)) {
if constexpr (is_read(operation)) {
switch(address) {
case 0xff00: *value = timers_.read<0>(); break;
case 0xff01: *value = timers_.read<1>(); break;
case 0xff02: *value = timers_.read<2>(); break;
case 0xff03: *value = timers_.read<3>(); break;
case 0xff04: *value = timers_.read<4>(); break;
case 0xff05: *value = timers_.read<5>(); break;
case 0xff06: *value = video_.read<0xff06>(); break;
case 0xff07: *value = video_.read<0xff07>(); break;
case 0xff00: value = timers_.read<0>(); break;
case 0xff01: value = timers_.read<1>(); break;
case 0xff02: value = timers_.read<2>(); break;
case 0xff03: value = timers_.read<3>(); break;
case 0xff04: value = timers_.read<4>(); break;
case 0xff05: value = timers_.read<5>(); break;
case 0xff06: value = video_.read<0xff06>(); break;
case 0xff07: value = video_.read<0xff07>(); break;
case 0xff08: {
const uint8_t keyboard_input =
~(
@@ -487,127 +482,122 @@ public:
((joystick_mask_ & 0x02) ? 0xff : (joystick(0).mask() | 0x40)) &
((joystick_mask_ & 0x04) ? 0xff : (joystick(1).mask() | 0x80));
*value = keyboard_input & joystick_mask;
value = keyboard_input & joystick_mask;
} break;
case 0xff09: *value = interrupts_.status(); break;
case 0xff09: value = interrupts_.status(); break;
case 0xff0a:
*value = interrupts_.mask() | video_.read<0xff0a>() | 0xa0;
value = interrupts_.mask() | video_.read<0xff0a>() | 0xa0;
break;
case 0xff0b: *value = video_.read<0xff0b>(); break;
case 0xff0c: *value = video_.read<0xff0c>(); break;
case 0xff0d: *value = video_.read<0xff0d>(); break;
case 0xff0e: *value = ff0e_; break;
case 0xff0f: *value = ff0f_; break;
case 0xff10: *value = ff10_ | 0xfc; break;
case 0xff11: *value = ff11_; break;
case 0xff12: *value = ff12_ | 0xc0; break;
case 0xff13: *value = ff13_ | (rom_is_paged_ ? 1 : 0); break;
case 0xff14: *value = video_.read<0xff14>(); break;
case 0xff15: *value = video_.read<0xff15>(); break;
case 0xff16: *value = video_.read<0xff16>(); break;
case 0xff17: *value = video_.read<0xff17>(); break;
case 0xff18: *value = video_.read<0xff18>(); break;
case 0xff19: *value = video_.read<0xff19>(); break;
case 0xff1a: *value = video_.read<0xff1a>(); break;
case 0xff1b: *value = video_.read<0xff1b>(); break;
case 0xff1c: *value = video_.read<0xff1c>(); break;
case 0xff1d: *value = video_.read<0xff1d>(); break;
case 0xff1e: *value = video_.read<0xff1e>(); break;
case 0xff1f: *value = video_.read<0xff1f>(); break;
case 0xff0b: value = video_.read<0xff0b>(); break;
case 0xff0c: value = video_.read<0xff0c>(); break;
case 0xff0d: value = video_.read<0xff0d>(); break;
case 0xff0e: value = ff0e_; break;
case 0xff0f: value = ff0f_; break;
case 0xff10: value = ff10_ | 0xfc; break;
case 0xff11: value = ff11_; break;
case 0xff12: value = ff12_ | 0xc0; break;
case 0xff13: value = ff13_ | (rom_is_paged_ ? 1 : 0); break;
case 0xff14: value = video_.read<0xff14>(); break;
case 0xff15: value = video_.read<0xff15>(); break;
case 0xff16: value = video_.read<0xff16>(); break;
case 0xff17: value = video_.read<0xff17>(); break;
case 0xff18: value = video_.read<0xff18>(); break;
case 0xff19: value = video_.read<0xff19>(); break;
case 0xff1a: value = video_.read<0xff1a>(); break;
case 0xff1b: value = video_.read<0xff1b>(); break;
case 0xff1c: value = video_.read<0xff1c>(); break;
case 0xff1d: value = video_.read<0xff1d>(); break;
case 0xff1e: value = video_.read<0xff1e>(); break;
case 0xff1f: value = video_.read<0xff1f>(); break;
case 0xff3e: *value = 0; break;
case 0xff3f: *value = 0; break;
case 0xff3e: value = 0; break;
case 0xff3f: value = 0; break;
default:
Logger::info().append("TODO: TED read at %04x", address);
value = 0xff;
is_hit = false;
}
} else {
switch(address) {
case 0xff00: timers_.write<0>(*value); break;
case 0xff01: timers_.write<1>(*value); break;
case 0xff02: timers_.write<2>(*value); break;
case 0xff03: timers_.write<3>(*value); break;
case 0xff04: timers_.write<4>(*value); break;
case 0xff05: timers_.write<5>(*value); break;
case 0xff06: video_.write<0xff06>(*value); break;
case 0xff00: timers_.write<0>(value); break;
case 0xff01: timers_.write<1>(value); break;
case 0xff02: timers_.write<2>(value); break;
case 0xff03: timers_.write<3>(value); break;
case 0xff04: timers_.write<4>(value); break;
case 0xff05: timers_.write<5>(value); break;
case 0xff06: video_.write<0xff06>(value); break;
case 0xff07:
video_.write<0xff07>(*value);
update_audio();
audio_.set_divider(*value);
video_.write<0xff07>(value);
audio_->set_divider(value);
break;
case 0xff08:
// Observation here: the kernel posts a 0 to this
// address upon completing each keyboard scan cycle,
// once per frame.
if(typer_ && !*value) {
if(typer_ && !value) {
if(!typer_->type_next_character()) {
clear_all_keys();
typer_.reset();
}
}
joystick_mask_ = *value;
joystick_mask_ = value;
break;
case 0xff09:
interrupts_.set_status(*value);
interrupts_.set_status(value);
break;
case 0xff0a:
interrupts_.set_mask(*value);
video_.write<0xff0a>(*value);
interrupts_.set_mask(value);
video_.write<0xff0a>(value);
break;
case 0xff0b: video_.write<0xff0b>(*value); break;
case 0xff0c: video_.write<0xff0c>(*value); break;
case 0xff0d: video_.write<0xff0d>(*value); break;
case 0xff0b: video_.write<0xff0b>(value); break;
case 0xff0c: video_.write<0xff0c>(value); break;
case 0xff0d: video_.write<0xff0d>(value); break;
case 0xff0e:
ff0e_ = *value;
update_audio();
audio_.set_frequency_low<0>(*value);
ff0e_ = value;
audio_->set_frequency_low<0>(value);
break;
case 0xff0f:
ff0f_ = *value;
update_audio();
audio_.set_frequency_low<1>(*value);
ff0f_ = value;
audio_->set_frequency_low<1>(value);
break;
case 0xff10:
ff10_ = *value;
update_audio();
audio_.set_frequency_high<1>(*value);
ff10_ = value;
audio_->set_frequency_high<1>(value);
break;
case 0xff11:
ff11_ = *value;
update_audio();
audio_.set_control(*value);
ff11_ = value;
audio_->set_control(value);
break;
case 0xff12:
ff12_ = *value & 0x3f;
video_.write<0xff12>(*value);
ff12_ = value & 0x3f;
video_.write<0xff12>(value);
if((*value & 4)) {
if((value & 4)) {
page_video_rom();
} else {
page_video_ram();
}
update_audio();
audio_.set_frequency_high<0>(*value);
audio_->set_frequency_high<0>(value);
break;
case 0xff13:
ff13_ = *value & 0xfe;
video_.write<0xff13>(*value);
ff13_ = value & 0xfe;
video_.write<0xff13>(value);
break;
case 0xff14: video_.write<0xff14>(*value); break;
case 0xff15: video_.write<0xff15>(*value); break;
case 0xff16: video_.write<0xff16>(*value); break;
case 0xff17: video_.write<0xff17>(*value); break;
case 0xff18: video_.write<0xff18>(*value); break;
case 0xff19: video_.write<0xff19>(*value); break;
case 0xff1a: video_.write<0xff1a>(*value); break;
case 0xff1b: video_.write<0xff1b>(*value); break;
case 0xff1c: video_.write<0xff1c>(*value); break;
case 0xff1d: video_.write<0xff1d>(*value); break;
case 0xff1e: video_.write<0xff1e>(*value); break;
case 0xff1f: video_.write<0xff1f>(*value); break;
case 0xff14: video_.write<0xff14>(value); break;
case 0xff15: video_.write<0xff15>(value); break;
case 0xff16: video_.write<0xff16>(value); break;
case 0xff17: video_.write<0xff17>(value); break;
case 0xff18: video_.write<0xff18>(value); break;
case 0xff19: video_.write<0xff19>(value); break;
case 0xff1a: video_.write<0xff1a>(value); break;
case 0xff1b: video_.write<0xff1b>(value); break;
case 0xff1c: video_.write<0xff1c>(value); break;
case 0xff1d: video_.write<0xff1d>(value); break;
case 0xff1e: video_.write<0xff1e>(value); break;
case 0xff1f: video_.write<0xff1f>(value); break;
case 0xff3e: page_cpu_rom(); break;
case 0xff3f: page_cpu_ram(); break;
@@ -626,22 +616,26 @@ public:
}
private:
using Processor = CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, ConcreteMachine, true>;
Processor m6502_;
struct M6502Traits {
static constexpr auto uses_ready_line = true;
static constexpr auto pause_precision = CPU::MOS6502Mk2::PausePrecision::BetweenInstructions;
using BusHandlerT = ConcreteMachine;
};
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 {
if(c1541_) c1541_->set_activity_observer(observer);
}
void set_irq_line(bool active) override {
m6502_.set_irq_line(active);
void set_irq_line(const bool active) override {
m6502_.template set<CPU::MOS6502Mk2::Line::IRQ>(active);
}
void set_ready_line(bool active) override {
m6502_.set_ready_line(active);
void set_ready_line(const bool active) override {
m6502_.template set<CPU::MOS6502Mk2::Line::Ready>(active);
}
void page_video_rom() {
@@ -684,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();
}
}
@@ -720,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 {
@@ -788,26 +771,19 @@ private:
// TODO: substantially simplify the below; at the minute it's a
// literal transcription of the original as a simple first step.
void read_dipole() {
using Register = CPU::MOS6502::Register;
using Flag = CPU::MOS6502::Flag;
using Flag = CPU::MOS6502Mk2::Flag;
//
// Get registers now and ensure they'll be written back at function exit.
//
CPU::MOS6502Esque::LazyFlags flags(uint8_t(m6502_.value_of(Register::Flags)));
uint8_t x, y, a;
uint8_t s = uint8_t(m6502_.value_of(Register::StackPointer));
auto registers = m6502_.registers();
struct ScopeGuard {
ScopeGuard(std::function<void(void)> at_exit) : at_exit_(at_exit) {}
~ScopeGuard() { at_exit_(); }
private:
std::function<void(void)> at_exit_;
} registers([&] {
m6502_.set_value_of(Register::Flags, flags.get());
m6502_.set_value_of(Register::A, a);
m6502_.set_value_of(Register::X, x);
m6502_.set_value_of(Register::Y, y);
m6502_.set_value_of(Register::StackPointer, s);
} store_registers([&] {
m6502_.set_registers(registers);
});
//
@@ -822,38 +798,38 @@ private:
// 6502 pseudo-ops.
//
const auto ldabs = [&] (uint8_t &target, const uint16_t address) {
flags.set_nz(target = map_.read(address));
registers.flags.set_per<Flag::NegativeZero>(target = map_.read(address));
};
const auto ldimm = [&] (uint8_t &target, const uint8_t value) {
flags.set_nz(target = value);
registers.flags.set_per<Flag::NegativeZero>(target = value);
};
const auto pha = [&] () {
map_.write(0x100 + s) = a;
--s;
map_.write(0x100 + registers.s) = registers.a;
--registers.s;
};
const auto pla = [&] () {
++s;
a = map_.read(0x100 + s);
++registers.s;
registers.a = map_.read(0x100 + registers.s);
};
const auto bit = [&] (const uint8_t value) {
flags.zero_result = a & value;
flags.negative_result = value;
flags.overflow = value & CPU::MOS6502Esque::Flag::Overflow;
registers.flags.set_per<Flag::Zero>(registers.a & value);
registers.flags.set_per<Flag::Negative>(value);
registers.flags.set_per<Flag::Overflow>(value);
};
const auto cmp = [&] (const uint8_t value) {
const uint16_t temp16 = a - value;
flags.set_nz(uint8_t(temp16));
flags.carry = ((~temp16) >> 8)&1;
const uint16_t temp16 = registers.a - value;
registers.flags.set_per<Flag::NegativeZero>(uint8_t(temp16));
registers.flags.set_per<Flag::Carry>(((~temp16) >> 8)&1);
};
const auto andimm = [&] (const uint8_t value) {
a &= value;
flags.set_nz(a);
registers.a &= value;
registers.flags.set_per<Flag::NegativeZero>(registers.a);
};
const auto ne = [&]() -> bool {
return flags.zero_result;
return !registers.flags.get<Flag::Zero>();
};
const auto eq = [&]() -> bool {
return !flags.zero_result;
return registers.flags.get<Flag::Zero>();
};
//
@@ -862,7 +838,7 @@ private:
const auto dipok = [&] {
// clc ; everything's fine
// rts
flags.carry = 0;
registers.flags.set_per<Flag::Carry>(0);
};
const auto rshort = [&] {
// bit tshrtd ; got a short
@@ -878,7 +854,7 @@ private:
const auto rderr1 = [&] {
// sec ; i'm confused
// rts
flags.carry = Flag::Carry;
registers.flags.set_per<Flag::Carry>(Flag::Carry);
};
//
@@ -891,8 +867,8 @@ private:
//rddipl
// ldx dsamp1 ; setup x,y with 1st sample point
// ldy dsamp1+1
ldabs(x, dsamp1);
ldabs(y, dsamp1 + 1);
ldabs(registers.x, dsamp1);
ldabs(registers.y, dsamp1 + 1);
advance_cycles(8);
//badeg1
@@ -901,9 +877,9 @@ private:
// pha
// lda dsamp2
// pha
ldabs(a, dsamp2 + 1);
ldabs(registers.a, dsamp2 + 1);
pha();
ldabs(a, dsamp2);
ldabs(registers.a, dsamp2);
pha();
advance_cycles(14);
@@ -911,7 +887,7 @@ private:
//rwtl ; wait till rd line is high
// bit port [= $0001]
// beq rwtl ; !ls!
ldimm(a, 0x10);
ldimm(registers.a, 0x10);
advance_cycles(2);
do {
bit(io_input());
@@ -933,8 +909,8 @@ private:
// stx timr2l
// sty timr2h
timers_.write<2>(x);
timers_.write<3>(y);
timers_.write<2>(registers.x);
timers_.write<3>(registers.y);
advance_cycles(8);
@@ -945,9 +921,9 @@ private:
// pla
// sta timr3h ;go! ...tb
pla();
timers_.write<4>(a);
timers_.write<4>(registers.a);
pla();
timers_.write<5>(a);
timers_.write<5>(registers.a);
advance_cycles(14);
@@ -955,8 +931,8 @@ private:
//
// lda #$50 ; clr ta,tb
// sta tedirq
ldimm(a, 0x50);
interrupts_.set_status(a);
ldimm(registers.a, 0x50);
interrupts_.set_status(registers.a);
advance_cycles(6);
@@ -969,7 +945,7 @@ private:
// and #$10 ; a look at that edge again
// bne badeg1 ; woa! got a bad edge trigger !ls!
do {
ldimm(a, io_input());
ldimm(registers.a, io_input());
cmp(io_input());
if(advance_cycles(9)) {
return;
@@ -992,7 +968,7 @@ private:
// lda #$10
//wata ; wait for ta to timeout
ldimm(a, 0x10);
ldimm(registers.a, 0x10);
advance_cycles(3);
do {
// bit port ; kuldge, kludge, kludge !!! <<><>>
@@ -1020,7 +996,7 @@ private:
do {
// lda port
// cmp port
ldimm(a, io_input());
ldimm(registers.a, io_input());
cmp(io_input());
if(advance_cycles(9)) {
@@ -1048,7 +1024,7 @@ private:
//
//; wait for tb to timeout
//; now do the dipole sample #2
ldimm(a, 0x40);
ldimm(registers.a, 0x40);
advance_cycles(3);
do {
bit(interrupts_.status());
@@ -1063,7 +1039,7 @@ private:
// cmp port
// bne casdb3
do {
ldimm(a, io_input());
ldimm(registers.a, io_input());
cmp(io_input());
if(advance_cycles(9)) {
return;
@@ -1084,10 +1060,10 @@ private:
// sta timr2l
// lda zcell+1
// sta timr2h
ldabs(a, zcell);
timers_.write<2>(a);
ldabs(a, zcell + 1);
timers_.write<3>(y);
ldabs(registers.a, zcell);
timers_.write<2>(registers.a);
ldabs(registers.a, zcell + 1);
timers_.write<3>(registers.y);
advance_cycles(16);
@@ -1096,9 +1072,9 @@ private:
// lda #$10
// sta tedirq ; verify +180 half of word dipole
// lda #$10
ldimm(a, 0x10);
interrupts_.set_status(a);
ldimm(a, 0x10);
ldimm(registers.a, 0x10);
interrupts_.set_status(registers.a);
ldimm(registers.a, 0x10);
advance_cycles(8);
//wata2
@@ -1116,7 +1092,7 @@ private:
// cmp port
// bne casdb4
do {
ldimm(a, io_input());
ldimm(registers.a, io_input());
cmp(io_input());
if(advance_cycles(9)) {
return;

View File

@@ -13,7 +13,7 @@
#include "Activity/Source.hpp"
#include "Machines/MachineTypes.hpp"
#include "Processors/6502/6502.hpp"
#include "Processors/6502Mk2/6502Mk2.hpp"
#include "Components/6560/6560.hpp"
#include "Components/6522/6522.hpp"
@@ -72,26 +72,30 @@ public:
// Port A provides information about the presence or absence of a tape, and parts of
// the joystick and serial port state, both of which have been statefully collected
// into port_a_.
if(!port) {
if constexpr (port == MOS::MOS6522::Port::A) {
return port_a_ | (tape_->has_tape() ? 0x00 : 0x40);
}
return 0xff;
}
/// Receives announcements of control line output change from the 6522.
template <MOS::MOS6522::Port port, MOS::MOS6522::Line line> void set_control_line_output(const bool value) {
// The CA2 output is used to control the tape motor.
if(port == MOS::MOS6522::Port::A && line == MOS::MOS6522::Line::Two) {
template <MOS::MOS6522::Port port, MOS::MOS6522::Line line>
void set_control_line_output(const bool value) {
// CA2: control the tape motor.
if constexpr (port == MOS::MOS6522::Port::A && line == MOS::MOS6522::Line::Two) {
tape_->set_motor_control(!value);
}
}
/// Receives announcements of changes in the serial bus connected to the serial port and propagates them into Port A.
void set_serial_line_state(Commodore::Serial::Line line, const bool value) {
void set_serial_line_state(const Commodore::Serial::Line line, const bool value) {
const auto set = [&](const uint8_t bit) {
port_a_ = (port_a_ & ~bit) | (value ? bit : 0x00);
};
switch(line) {
default: break;
case ::Commodore::Serial::Line::Data: port_a_ = (port_a_ & ~0x02) | (value ? 0x02 : 0x00); break;
case ::Commodore::Serial::Line::Clock: port_a_ = (port_a_ & ~0x01) | (value ? 0x01 : 0x00); break;
case ::Commodore::Serial::Line::Data: set(0x02); break;
case ::Commodore::Serial::Line::Clock: set(0x01); break;
}
}
@@ -146,7 +150,7 @@ public:
/// Sets all keys as unpressed.
void clear_all_keys() {
memset(columns_, 0xff, sizeof(columns_));
std::fill(std::begin(columns_), std::end(columns_), 0xff);
}
/// Called by the 6522 to get input. Reads the keyboard on Port A, returns a small amount of joystick state on Port B.
@@ -230,8 +234,6 @@ struct Vic6560BusHandler {
// It is assumed that these pointers have been filled in by the machine.
const uint8_t *video_memory_map[16]{}; // Segments video memory into 1kb portions.
const uint8_t *colour_memory{}; // Colour memory must be contiguous.
// TODO: make the above const.
};
/*!
@@ -280,7 +282,6 @@ class ConcreteMachine:
public MachineTypes::MappedKeyboardMachine,
public MachineTypes::JoystickMachine,
public Configurable::Device,
public CPU::MOS6502::BusHandler,
public MOS::MOS6522::IRQDelegatePortHandler::Delegate,
public Utility::TypeRecipient<CharacterMapper>,
public Storage::Tape::BinaryTapePlayer::Delegate,
@@ -455,7 +456,7 @@ public:
}
void set_key_state(const uint16_t key, const bool is_pressed) final {
if(key < 0xfff0) {
if(key < KeyUp) {
keyboard_via_port_handler_.set_key_state(key, is_pressed);
} else {
switch(key) {
@@ -481,28 +482,27 @@ public:
void clear_all_keys() final {
keyboard_via_port_handler_.clear_all_keys();
set_key_state(KeyRestore, false);
}
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
return joysticks_;
}
// to satisfy CPU::MOS6502::Processor
forceinline Cycles perform_bus_operation(
const CPU::MOS6502::BusOperation operation,
const uint16_t address,
uint8_t *const value
) {
template <CPU::MOS6502Mk2::BusOperation operation, typename AddressT>
Cycles perform(const AddressT address, CPU::MOS6502Mk2::data_t<operation> value) {
// Tun the phase-1 part of this cycle, in which the VIC accesses memory.
cycles_since_mos6560_update_++;
// Run the phase-2 part of the cycle, which is whatever the 6502 said it should be.
const bool is_from_rom = m6502_.value_of(CPU::MOS6502::Register::ProgramCounter) > 0x8000;
if(is_read(operation)) {
const auto is_from_rom = [&]() {
return m6502_.registers().pc.full > 0x8000;
};
if constexpr (is_read(operation)) {
const auto page = processor_read_memory_map_[address >> 10];
uint8_t result;
if(!page) {
if(!is_from_rom) confidence_.add_miss();
if(!is_from_rom()) confidence_.add_miss();
result = 0xff;
} else {
result = processor_read_memory_map_[address >> 10][address & 0x3ff];
@@ -515,7 +515,7 @@ public:
if(address & 0x10) result &= user_port_via_.read(address);
if(address & 0x20) result &= keyboard_via_.read(address);
if(!is_from_rom) {
if(!is_from_rom()) {
if((address & 0x100) && !(address & 0x30)) {
confidence_.add_miss();
} else {
@@ -523,10 +523,10 @@ public:
}
}
}
*value = result;
value = result;
// Consider applying the fast tape hack.
if(use_fast_tape_hack_ && operation == CPU::MOS6502::BusOperation::ReadOpcode) {
if(use_fast_tape_hack_ && operation == CPU::MOS6502Mk2::BusOperation::ReadOpcode) {
if(address == 0xf7b2) {
// Address 0xf7b2 contains a JSR to 0xf8c0 ('RDTPBLKS') that will fill the tape buffer with the
// next header. Skip that via a three-byte NOP and fill in the next header programmatically.
@@ -551,10 +551,10 @@ public:
ram_[0x90] = 0;
ram_[0x93] = 0;
*value = 0x0c; // i.e. NOP abs, to swallow the entire JSR
value = 0x0c; // i.e. NOP abs, to swallow the entire JSR
} else if(address == 0xf90b) {
const auto x = uint8_t(m6502_.value_of(CPU::MOS6502::Register::X));
if(x == 0xe) {
auto registers = m6502_.registers();
if(registers.x == 0xe) {
Storage::Tape::Commodore::Parser parser(TargetPlatform::Vic20);
const auto tape_position = tape_->serialiser()->offset();
const std::unique_ptr<Storage::Tape::Commodore::Data> data = parser.get_next_data(*tape_->serialiser());
@@ -576,14 +576,13 @@ public:
// set tape status, carry and flag
ram_[0x90] |= 0x40;
uint8_t flags = uint8_t(m6502_.value_of(CPU::MOS6502::Register::Flags));
flags &= ~uint8_t((CPU::MOS6502::Flag::Carry | CPU::MOS6502::Flag::Interrupt));
m6502_.set_value_of(CPU::MOS6502::Register::Flags, flags);
registers.flags.set_per<CPU::MOS6502Mk2::Flag::Carry>(0);
registers.flags.set_per<CPU::MOS6502Mk2::Flag::Interrupt>(0);
// to ensure that execution proceeds to 0xfccf, pretend a NOP was here and
// ensure that the PC leaps to 0xfccf
m6502_.set_value_of(CPU::MOS6502::Register::ProgramCounter, 0xfccf);
*value = 0xea; // i.e. NOP implied
registers.pc.full = 0xfccf;
value = 0xea; // i.e. NOP implied
hold_tape_ = true;
Logger::info().append("Found data");
} else {
@@ -592,27 +591,28 @@ public:
Logger::info().append("Didn't find data");
}
}
m6502_.set_registers(registers);
}
}
} else {
uint8_t *const ram = processor_write_memory_map_[address >> 10];
if(ram) {
update_video();
ram[address & 0x3ff] = *value;
ram[address & 0x3ff] = value;
}
// Anything between 0x9000 and 0x9400 is the IO area.
if((address&0xfc00) == 0x9000) {
// The VIC is selected by bit 8 = 0
if(!(address&0x100)) {
update_video();
mos6560_.write(address, *value);
mos6560_.write(address, value);
}
// The first VIA is selected by bit 4 = 1.
if(address & 0x10) user_port_via_.write(address, *value);
if(address & 0x10) user_port_via_.write(address, value);
// The second VIA is selected by bit 5 = 1.
if(address & 0x20) keyboard_via_.write(address, *value);
if(address & 0x20) keyboard_via_.write(address, value);
if(!is_from_rom) {
if(!is_from_rom()) {
if((address & 0x100) && !(address & 0x30)) {
confidence_.add_miss();
} else {
@@ -620,13 +620,13 @@ public:
}
}
} else if(!ram) {
if(!is_from_rom) confidence_.add_miss();
if(!is_from_rom()) confidence_.add_miss();
}
}
user_port_via_.run_for(Cycles(1));
keyboard_via_.run_for(Cycles(1));
if(typer_ && address == 0xeb1e && operation == CPU::MOS6502::BusOperation::ReadOpcode) {
if(typer_ && address == 0xeb1e && operation == CPU::MOS6502Mk2::BusOperation::ReadOpcode) {
if(!typer_->type_next_character()) {
clear_all_keys();
typer_.reset();
@@ -672,8 +672,8 @@ public:
}
void mos6522_did_change_interrupt_status(void *) final {
m6502_.set_nmi_line(user_port_via_.get_interrupt_line());
m6502_.set_irq_line(keyboard_via_.get_interrupt_line());
m6502_.template set<CPU::MOS6502Mk2::Line::NMI>(user_port_via_.get_interrupt_line());
m6502_.template set<CPU::MOS6502Mk2::Line::IRQ>(keyboard_via_.get_interrupt_line());
}
void type_string(const std::string &string) final {
@@ -718,10 +718,16 @@ public:
}
private:
struct M6502Traits {
static constexpr auto uses_ready_line = false;
static constexpr auto pause_precision = CPU::MOS6502Mk2::PausePrecision::BetweenInstructions;
using BusHandlerT = ConcreteMachine;
};
CPU::MOS6502Mk2::Processor<CPU::MOS6502Mk2::Model::M6502, M6502Traits> m6502_;
void update_video() {
mos6560_.run_for(cycles_since_mos6560_update_.flush<Cycles>());
}
CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, ConcreteMachine, false> m6502_;
std::vector<uint8_t> character_rom_;
std::vector<uint8_t> basic_rom_;
@@ -745,12 +751,22 @@ private:
++address;
}
}
void write_to_map(const uint8_t **const map, const uint8_t *area, uint16_t address, size_t length) {
void write_to_map(
const uint8_t **const map,
const uint8_t *const area,
const uint16_t address,
const size_t length
) {
write_to_map([&](const uint16_t address, const size_t offset) {
map[address] = &area[offset];
}, address, length);
}
void write_to_map(uint8_t **const map, uint8_t *area, uint16_t address, size_t length) {
void write_to_map(
uint8_t **const map,
uint8_t *const area,
const uint16_t address,
const size_t length
) {
write_to_map([&](const uint16_t address, const size_t offset) {
map[address] = &area[offset];
}, address, length);

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_.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

@@ -360,7 +360,7 @@ public:
}
~ConcreteMachine() {
speaker_.audio_queue.flush();
speaker_.audio_queue.lock_flush();
}
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {

View File

@@ -178,7 +178,7 @@ public:
}
~ConcreteMachine() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
ChangeEffect effect_for_file_did_change(const std::string &) const final {

View File

@@ -406,7 +406,7 @@ public:
}
~ConcreteMachine() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
void set_key_state(uint16_t key, bool is_pressed) final {

View File

@@ -817,7 +817,7 @@ public:
}
~ConcreteMachine() {
speaker_.queue.flush();
speaker_.queue.lock_flush();
}
// MARK: - TimedMachine.

View File

@@ -114,7 +114,7 @@ public:
}
~ConcreteMachine() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {

View File

@@ -213,7 +213,7 @@ public:
}
~ConcreteMachine() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
static constexpr unsigned int clock_rate() {

View File

@@ -24,8 +24,9 @@ struct SizedInt {
constexpr SizedInt(const IntT start_value) noexcept : counter_(start_value & Mask) {}
SizedInt() = default;
template <int begin = 0>
IntT get() const {
return counter_;
return counter_ >> begin;
}
SizedInt operator +(const SizedInt offset) const { return SizedInt<bits>(counter_ + offset.counter_); }

View File

@@ -753,6 +753,10 @@
4B92E26B234AE35100CD6D1B /* MFP68901.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B92E268234AE35000CD6D1B /* MFP68901.cpp */; };
4B92EACA1B7C112B00246143 /* 6502TimingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B92EAC91B7C112B00246143 /* 6502TimingTests.swift */; };
4B9378E422A199C600973513 /* Audio.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B9378E222A199C600973513 /* Audio.cpp */; };
4B96DECC2EBEE7D100505298 /* SID.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B96DECA2EBEE7D100505298 /* SID.cpp */; };
4B96DECD2EBEE7D100505298 /* SID.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B96DECA2EBEE7D100505298 /* SID.cpp */; };
4B96DECE2EBEE7D100505298 /* SID.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B96DECA2EBEE7D100505298 /* SID.cpp */; };
4B96DED32EC3ECDA00505298 /* SIDTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B96DED22EC3ECDA00505298 /* SIDTests.mm */; };
4B96F7CE263E33B10092AEE1 /* DSK.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B96F7CC263E33B10092AEE1 /* DSK.cpp */; };
4B96F7CF263E33B10092AEE1 /* DSK.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B96F7CC263E33B10092AEE1 /* DSK.cpp */; };
4B98A05E1FFAD3F600ADF63B /* CSROMFetcher.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B98A05D1FFAD3F600ADF63B /* CSROMFetcher.mm */; };
@@ -1951,6 +1955,11 @@
4B9378E322A199C600973513 /* Audio.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Audio.hpp; sourceTree = "<group>"; };
4B95FA9C1F11893B0008E395 /* ZX8081Controller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZX8081Controller.swift; sourceTree = "<group>"; };
4B961408222760E0001A7BF2 /* Screenshot.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Screenshot.hpp; sourceTree = "<group>"; };
4B96DEC32EBEA88C00505298 /* TubeProcessor.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = TubeProcessor.hpp; sourceTree = "<group>"; };
4B96DEC92EBEE7D100505298 /* SID.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = SID.hpp; sourceTree = "<group>"; };
4B96DECA2EBEE7D100505298 /* SID.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = SID.cpp; sourceTree = "<group>"; };
4B96DED22EC3ECDA00505298 /* SIDTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SIDTests.mm; sourceTree = "<group>"; };
4B96DED42EC53BC300505298 /* BiquadFilter.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = BiquadFilter.hpp; sourceTree = "<group>"; };
4B96F7CB263E30B00092AEE1 /* RawSectorDump.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = RawSectorDump.hpp; sourceTree = "<group>"; };
4B96F7CC263E33B10092AEE1 /* DSK.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = DSK.cpp; sourceTree = "<group>"; };
4B96F7CD263E33B10092AEE1 /* DSK.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = DSK.hpp; sourceTree = "<group>"; };
@@ -2514,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>"; };
@@ -3016,6 +3026,7 @@
4BC76E671C98E31700E6EF73 /* FIRFilter.cpp */,
4BC76E681C98E31700E6EF73 /* FIRFilter.hpp */,
4B24095A1C45DF85004DA684 /* Stepper.hpp */,
4B96DED42EC53BC300505298 /* BiquadFilter.hpp */,
);
name = SignalProcessing;
path = ../../SignalProcessing;
@@ -3365,6 +3376,7 @@
4B4195F52EB92630001C966D /* Tube6502.hpp */,
4B4195F62EBAB06C001C966D /* TubeZ80.hpp */,
4B4195F42EB8F061001C966D /* ULA.hpp */,
4B96DEC32EBEA88C00505298 /* TubeProcessor.hpp */,
);
path = Tube;
sourceTree = "<group>";
@@ -4352,6 +4364,15 @@
path = 68901;
sourceTree = "<group>";
};
4B96DECB2EBEE7D100505298 /* SID */ = {
isa = PBXGroup;
children = (
4B96DEC92EBEE7D100505298 /* SID.hpp */,
4B96DECA2EBEE7D100505298 /* SID.cpp */,
);
path = SID;
sourceTree = "<group>";
};
4B9F11C72272375400701480 /* QL Startup */ = {
isa = PBXGroup;
children = (
@@ -4870,6 +4891,7 @@
4BD4A8CF1E077FD20020D856 /* PCMTrackTests.mm */,
4B3F76B825A1635300178AEC /* PowerPCDecoderTests.mm */,
4BE76CF822641ED300ACD6FA /* QLTests.mm */,
4B96DED22EC3ECDA00505298 /* SIDTests.mm */,
4B8DD3672633B2D400B3C866 /* SpectrumVideoContentionTests.mm */,
4B2AF8681E513FC20027EE29 /* TIATests.mm */,
4B1D08051E0F7A1100763741 /* TimeTests.mm */,
@@ -5166,6 +5188,7 @@
4BF0BC6E2973318E00CCA2B5 /* RP5C01 */,
4B8855A42E84D51B00E251DD /* SAA5050 */,
4B0ACBFF237756EC008902D0 /* Serial */,
4B96DECB2EBEE7D100505298 /* SID */,
4BB0A6582044FD3000FB3688 /* SN76489 */,
4B47F3B42E7B9A14005D4DEC /* uPD7002 */,
);
@@ -5290,6 +5313,7 @@
4BD060A41FE49D3C006E14BE /* Speaker */ = {
isa = PBXGroup;
children = (
4BEF9CA92EC8294E00DDD0F6 /* SpeakerQueue.hpp */,
4BD060A51FE49D3C006E14BE /* Speaker.hpp */,
4B8EF6051FE5AF830076CCDD /* Implementation */,
);
@@ -6161,6 +6185,7 @@
4B894521201967B4007DE474 /* StaticAnalyser.cpp in Sources */,
4B47F3B62E7BAB94005D4DEC /* uPD7002.cpp in Sources */,
4B8318B522D3E548006DB630 /* Macintosh.cpp in Sources */,
4B96DECD2EBEE7D100505298 /* SID.cpp in Sources */,
4B8DF4FA254E36AE00F3433C /* Video.cpp in Sources */,
4B7BA03123C2B19C00B98D9E /* Jasmin.cpp in Sources */,
4B7F188F2154825E00388727 /* MasterSystem.cpp in Sources */,
@@ -6555,6 +6580,7 @@
4BC080D026A257A200D03FD8 /* StaticAnalyser.cpp in Sources */,
4B4DC8211D2C2425003C5BF8 /* Vic20.cpp in Sources */,
4B71368E1F788112008B8ED9 /* Parser.cpp in Sources */,
4B96DECC2EBEE7D100505298 /* SID.cpp in Sources */,
4B12C0ED1FCFA98D005BFD93 /* Keyboard.cpp in Sources */,
4BA0F68E1EEA0E8400E9489E /* ZX8081.cpp in Sources */,
429B13602B1F7BDA006BB4CB /* StaticAnalyser.cpp in Sources */,
@@ -6681,6 +6707,7 @@
4B06AAF82C6460760034D014 /* IntelligentKeyboard.cpp in Sources */,
4B7752B628217EE70073E2C5 /* DSK.cpp in Sources */,
4B06AAD12C645F130034D014 /* 1770.cpp in Sources */,
4B96DECE2EBEE7D100505298 /* SID.cpp in Sources */,
4B778F2523A5EDF40000D260 /* Encoder.cpp in Sources */,
4B778F4223A5F1A70000D260 /* MemoryFuzzer.cpp in Sources */,
4B778F0123A5EBA00000D260 /* MacintoshIMG.cpp in Sources */,
@@ -6776,6 +6803,7 @@
4B778F4623A5F1D80000D260 /* StaticAnalyser.cpp in Sources */,
4B778F1323A5EC890000D260 /* Z80Base.cpp in Sources */,
4B778F2923A5EF030000D260 /* CommodoreROM.cpp in Sources */,
4B96DED32EC3ECDA00505298 /* SIDTests.mm in Sources */,
4B06AADC2C645F720034D014 /* BD500.cpp in Sources */,
4B778F4823A5F1E70000D260 /* StaticAnalyser.cpp in Sources */,
4B92EACA1B7C112B00246143 /* 6502TimingTests.swift in Sources */,

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

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24128" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24412" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24128"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24412"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>

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

@@ -76,6 +76,7 @@ SOURCES += \
$$SRC/Components/KonamiSCC/*.cpp \
$$SRC/Components/OPx/*.cpp \
$$SRC/Components/RP5C01/*.cpp \
$$SRC/Components/SID/*.cpp \
$$SRC/Components/SAA5050/*.cpp \
$$SRC/Components/Serial/*.cpp \
$$SRC/Components/SN76489/*.cpp \
@@ -213,6 +214,7 @@ HEADERS += \
$$SRC/Components/OPx/*.hpp \
$$SRC/Components/OPx/Implementation/*.hpp \
$$SRC/Components/RP5C01/*.hpp \
$$SRC/Components/SID/*.hpp \
$$SRC/Components/SAA5050/*.hpp \
$$SRC/Components/Serial/*.hpp \
$$SRC/Components/SN76489/*.hpp \

View File

@@ -61,6 +61,7 @@ SOURCES += glob.glob('../../Components/KonamiSCC/*.cpp')
SOURCES += glob.glob('../../Components/OPx/*.cpp')
SOURCES += glob.glob('../../Components/RP5C01/*.cpp')
SOURCES += glob.glob('../../Components/SAA5050/*.cpp')
SOURCES += glob.glob('../../Components/SID/*.cpp')
SOURCES += glob.glob('../../Components/SN76489/*.cpp')
SOURCES += glob.glob('../../Components/Serial/*.cpp')
SOURCES += glob.glob('../../Components/uPD7002/*.cpp')

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_));
}
};
}

View File

@@ -66,6 +66,7 @@ enum class Line {
PowerOn,
Overflow,
NMI,
Ready,
};
// MARK: - Address bus.
@@ -185,14 +186,24 @@ public:
(inputs_.interrupt_requests & Inputs::InterruptRequest::PowerOn);
break;
// Level triggered.
// Level triggered interrupts.
case Line::Reset: level_sample(Inputs::InterruptRequest::Reset); break;
case Line::IRQ: level_sample(Inputs::InterruptRequest::IRQ); break;
// Edge triggered.
case Line::Overflow: edge_sample(Inputs::InterruptRequest::Reset, inputs_.overflow); break;
// Edge triggered interrupts.
case Line::NMI: edge_sample(Inputs::InterruptRequest::NMI, inputs_.nmi); break;
// Leval-capturing state.
case Line::Ready: inputs_.ready = value; break;
// Edge-triggered state.
case Line::Overflow:
if(!inputs_.overflow && value) {
registers_.flags.set_per<Flag::Overflow>(Flag::Overflow);
}
inputs_.overflow = value;
break;
default:
__builtin_unreachable();
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 554 KiB

View File

@@ -0,0 +1,214 @@
//
// BiquadFilter.hpp
// Clock Signal
//
// Created by Thomas Harte on 12/11/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#pragma once
#include <cassert>
#include <cmath>
#include <numbers>
namespace SignalProcessing {
/*!
A biquad[ratic] filter approximates the real analogue thing in taking a 1d PCM signal and applying a
filter to it as a function of the current input plus the two most-recent inputs plus the two most-recent outputs.
So both IIR and three-tap FIR filters are degenerate cases of the biquad.
It is used quite often in real designs, hence an implementation of this filter specifically.
... and the below is largely textbook; I can't claim any great knowledge. I am especially indebted to
the W3C Group's audio EQ cookbook at https://www.w3.org/TR/audio-eq-cookbook/ .
*/
class BiquadFilter {
public:
enum class Type {
LowPass,
HighPass,
BandPass,
Notch,
AllPass,
Peaking,
LowShelf,
HighShelf
};
// Default construction: a filter that produces _nothing_.
BiquadFilter() {}
BiquadFilter(
const Type type,
const float sample_rate,
const float frequency,
const float resonance = 0.707f,
const float gain = 8,
const bool normalise = true
) {
configure(type, sample_rate, frequency, resonance, gain, normalise);
}
void configure(
const Type type,
const float sample_rate,
const float frequency,
const float resonance = 0.707f,
const float gain = 8,
const bool normalise = true
) {
const float w0 = 2.0f * std::numbers::pi_v<float> * frequency / sample_rate;
const float alpha = std::sin(w0) / (2.0f * resonance);
const float cos_w0 = std::cos(w0);
float coefficients[5];
float magnitude = 1.0f;
switch(type) {
case Type::LowPass:
coefficients[0] = (1.0f - cos_w0) / 2.0f;
coefficients[1] = 1.0f - cos_w0;
coefficients[2] = (1.0f - cos_w0) / 2.0f;
magnitude = 1.0f + alpha;
coefficients[3] = -2.0f * cos_w0;
coefficients[4] = 1.0f - alpha;
break;
case Type::HighPass:
coefficients[0] = (1.0f - cos_w0) / 2.0f;
coefficients[1] = -(1.0f + cos_w0);
coefficients[2] = (1.0f - cos_w0) / 2.0f;
magnitude = 1.0f + alpha;
coefficients[3] = -2.0f * cos_w0;
coefficients[4] = 1.0f - alpha;
break;
case Type::BandPass:
coefficients[0] = alpha;
coefficients[1] = 0.0f;
coefficients[2] = -alpha;
magnitude = 1.0f + alpha;
coefficients[3] = -2.0f * cos_w0;
coefficients[0] = 1.0f - alpha;
break;
case Type::Notch:
coefficients[0] = 1.0f;
coefficients[1] = -2.0f * cos_w0;
coefficients[2] = 1.0f;
magnitude = 1.0f + alpha;
coefficients[3] = -2.0f * cos_w0;
coefficients[4] = 1.0f - alpha;
break;
case Type::AllPass:
coefficients[0] = 1.0f - alpha;
coefficients[1] = -2.0f * cos_w0;
coefficients[2] = 1.0f + alpha;
magnitude = 1.0f + alpha;
coefficients[3] = -2.0f * cos_w0;
coefficients[4] = 1.0f - alpha;
break;
case Type::Peaking: {
const float a = std::pow(10.0f, gain / 40.0f);
coefficients[0] = 1.0f + (alpha * a);
coefficients[1] = -2.0f * cos_w0;
coefficients[2] = 1.0f - (alpha * a);
magnitude = 1.0f + (alpha / a);
coefficients[3] = -2.0f * cos_w0;
coefficients[4] = 1.0f - (alpha / a);
} break;
case Type::LowShelf: {
const float a_ls = std::pow(10.0f, gain / 40.0f);
const float sqrt_a = std::sqrt(a_ls);
const float alpha_ls =
std::sin(w0) / 2.0f * std::sqrt((a_ls + 1.0f / a_ls) * (1.0f / resonance - 1.0f) + 2.0f);
coefficients[0] = a_ls * ((a_ls + 1.0f) - (a_ls - 1.0f) * cos_w0 + 2.0f * sqrt_a * alpha_ls);
coefficients[1] = 2.0f * a_ls * ((a_ls - 1.0f) - (a_ls + 1.0f) * cos_w0);
coefficients[2] = a_ls * ((a_ls + 1.0f) - (a_ls - 1.0f) * cos_w0 - 2.0f * sqrt_a * alpha_ls);
magnitude = (a_ls + 1.0f) + (a_ls - 1.0f) * cos_w0 + 2.0f * sqrt_a * alpha_ls;
coefficients[3] = -2.0f * ((a_ls - 1) + (a_ls + 1) * cos_w0);
coefficients[4] = (a_ls + 1.0f) + (a_ls - 1.0f) * cos_w0 - 2.0f * sqrt_a * alpha_ls;
} break;
case Type::HighShelf: {
const float a_hs = std::pow(10.0f, gain / 40.0f);
const float sqrt_a_hs = std::sqrt(a_hs);
const float alpha_hs =
std::sin(w0) / 2.0f * std::sqrt((a_hs + 1.0f / a_hs) * (1.0f / resonance - 1.0f) + 2.0f);
coefficients[0] = a_hs * ((a_hs + 1.0f) + (a_hs - 1.0f) * cos_w0 + 2.0f * sqrt_a_hs * alpha_hs);
coefficients[1] = -2.0f * a_hs * ((a_hs - 1.0f) + (a_hs + 1.0f) * cos_w0);
coefficients[2] = a_hs * ((a_hs + 1.0f) + (a_hs - 1.0f) * cos_w0 - 2.0f * sqrt_a_hs * alpha_hs);
magnitude = (a_hs + 1.0f) - (a_hs - 1.0f) * cos_w0 + 2.0f * sqrt_a_hs * alpha_hs;
coefficients[3] = 2.0f * ((a_hs - 1.0f) - (a_hs + 1.0f) * cos_w0);
coefficients[4] = (a_hs + 1.0f) - (a_hs - 1.0f) * cos_w0 - 2.0f * sqrt_a_hs * alpha_hs;
} break;
}
if(normalise) {
for(int c = 0; c < 5; c++) {
coefficients[c] /= magnitude;
}
}
for(int c = 0; c < 5; c++) {
#ifdef FIXED
coefficients_[c] = FixedType(coefficients[c] * FixedMultiplier);
#else
coefficients_[c] = coefficients[c];
#endif
}
}
int16_t apply(const int16_t input) {
#ifdef FIXED
const auto applied =
coefficients_[0] * input +
coefficients_[1] * inputs_[0] +
coefficients_[2] * inputs_[1] -
coefficients_[3] * outputs_[0] -
coefficients_[4] * outputs_[1];
const auto output = int16_t(applied >> FixedShift);
#else
const float output =
coefficients_[0] * float(input) +
coefficients_[1] * inputs_[0] +
coefficients_[2] * inputs_[1] -
coefficients_[3] * outputs_[0] -
coefficients_[4] * outputs_[1];
#endif
inputs_[1] = inputs_[0];
inputs_[0] = input;
outputs_[1] = outputs_[0];
outputs_[0] = output;
return int16_t(output);
}
private:
#ifdef FIXED
int16_t inputs_[2]{};
int16_t outputs_[2]{};
using FixedType = int64_t;
static constexpr int FixedShift = 48;
static constexpr auto FixedMultiplier = static_cast<float>(int64_t(1) << FixedShift);
FixedType coefficients_[5]{};
#else
float inputs_[2]{};
float outputs_[2]{};
float coefficients_[5]{};
#endif
// Coefficients indices versus common textbook terms:
// 0 = b0; 1 = b1; 2 = b2; 3 = a1; 4 = a2
};
}

View File

@@ -75,7 +75,7 @@ Track *DiskImageHolder<T>::track_at_position(Track::Address address) const {
template <typename T>
DiskImageHolder<T>::~DiskImageHolder() {
if(update_queue_) update_queue_->flush();
if(update_queue_) update_queue_->lock_flush();
}
template <typename T>

View File

@@ -59,6 +59,7 @@ set(CLK_SOURCES
Components/OPx/OPLL.cpp
Components/RP5C01/RP5C01.cpp
Components/SAA5050/SAA5050.cpp
Components/SID/SID.cpp
Components/SN76489/SN76489.cpp
Components/Serial/Line.cpp
Components/uPD7002/uPD7002.cpp