1
0
mirror of https://github.com/TomHarte/CLK.git synced 2026-04-26 03:29:40 +00:00
Files
CLK/Components/SID/SID.hpp
T
2025-11-11 20:53:54 -05:00

294 lines
9.1 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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"
namespace MOS::SID {
struct Voice {
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() {
phase = PhaseReload;
}
static constexpr uint32_t NoiseReload = 0x7'ffff;
uint32_t noise = NoiseReload;
bool did_raise_b23() const {
return previous_phase > phase;
}
bool did_raise_b19() const {
static constexpr int NoiseBit = 1 << (19 + 8);
return (previous_phase ^ phase) & phase & NoiseBit;
}
uint16_t sawtooth_output() const {
return (phase >> 20) ^ 0x800;
}
uint16_t noise_output() const {
// Uses bits: 20, 18, 14, 11, 9, 5, 2 and 0, plus four more zero bits.
return
((noise >> 8) & 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
}
void update_noise(const bool test) {
noise =
(noise << 1) |
(((noise >> 17) ^ ((noise >> 22) | test)) & 1);
}
} 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 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;
}
}
} adsr;
Numeric::SizedInt<8> control;
void 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 noise() const { return control.bit<7>(); }
bool pulse() const { return control.bit<6>(); }
bool sawtooth() const { return control.bit<5>(); }
bool triangle() const { return control.bit<4>(); }
bool test() const { return control.bit<3>(); }
bool ring_mod() const { return control.bit<2>(); }
bool sync() const { return control.bit<1>(); }
bool gate() const { return control.bit<0>(); }
void update() {
// Oscillator.
oscillator.previous_phase = oscillator.phase;
if(test()) {
oscillator.phase = 0;
} else {
oscillator.phase += oscillator.pitch;
if(oscillator.did_raise_b19()) {
oscillator.update_noise(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;
} else {
++adsr.exponential_counter;
if(adsr.exponential_counter == exponential_prescaler[adsr.envelope]) {
adsr.exponential_counter = 0;
switch(adsr.phase) {
default: __builtin_unreachable();
case ADSR::Phase::DecayAndHold:
if(adsr.envelope == adsr.sustain) {
break;
}
--adsr.envelope;
break;
case ADSR::Phase::Release:
if(adsr.envelope) {
--adsr.envelope;
}
break;
}
}
}
}
}
void synchronise(const Voice &prior) {
// Only oscillator work to do here.
if(
sync() &&
prior.oscillator.did_raise_b23()
) {
oscillator.phase = Oscillator::PhaseReload;
}
}
static constexpr uint16_t MaxWaveformValue = (1 << 12) - 1;
uint16_t sawtooth_output() const {
return oscillator.sawtooth_output();
}
uint16_t pulse_output() const {
// TODO: find a better test than this.
return (
(oscillator.phase ^ 0x8000'0000) <
(oscillator.pulse_width ^ 0x8000'0000))
? MaxWaveformValue : 0;
}
uint16_t 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 noise_output() const {
return oscillator.noise_output();
}
uint16_t 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 &= sawtooth_output();
if(triangle()) output &= triangle_output(prior);
if(noise()) output &= noise_output();
return (output * adsr.envelope) / 255;
}
};
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);
// Outputs::Speaker::BufferSource.
template <Outputs::Speaker::Action action>
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target);
bool is_zero_level() const;
void set_sample_volume_range(std::int16_t);
private:
Concurrency::AsyncTaskQueue<false> &audio_queue_;
Voice voices_[3];
// TODO: an emulator thread copy of voices needs to be kept too, to do the digital stuff, as
// the current output of voice 3 can be read. Probably best if the audio thread posts its most
// recent copy atomically and the emulator thread just catches up from whatever it has? I don't
// think that spinning on voice 3 is common.
uint8_t last_write_;
int16_t range_ = 0;
};
}