mirror of
https://github.com/TomHarte/CLK.git
synced 2024-12-01 11:49:58 +00:00
Makes a first attempt at implementing the SN76489.
This commit is contained in:
parent
0ad2676640
commit
d4df101ab6
@ -108,6 +108,7 @@ void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
evaluate_output_volume();
|
evaluate_output_volume();
|
||||||
|
printf("%d ", output_volume_);
|
||||||
|
|
||||||
for(int ic = 0; ic < 8 && c < number_of_samples; ic++) {
|
for(int ic = 0; ic < 8 && c < number_of_samples; ic++) {
|
||||||
target[c] = output_volume_;
|
target[c] = output_volume_;
|
||||||
|
@ -8,9 +8,128 @@
|
|||||||
|
|
||||||
#include "SN76489.hpp"
|
#include "SN76489.hpp"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
using namespace TI;
|
using namespace TI;
|
||||||
|
|
||||||
SN76489::SN76489(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) {}
|
SN76489::SN76489(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) {
|
||||||
|
// Build a volume table.
|
||||||
void SN76489::write(uint8_t value) {
|
double multiplier = pow(10.0, -0.1);
|
||||||
|
double volume = 8191.0f;
|
||||||
|
for(int c = 0; c < 16; ++c) {
|
||||||
|
volumes_[c] = (int)round(volume);
|
||||||
|
volume *= multiplier;
|
||||||
|
}
|
||||||
|
volumes_[15] = 0;
|
||||||
|
evaluate_output_volume();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SN76489::set_register(uint8_t value) {
|
||||||
|
task_queue_.defer([value, this] () {
|
||||||
|
if(value & 0x80) {
|
||||||
|
active_register_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int channel = (active_register_ >> 5)&3;
|
||||||
|
if(active_register_ & 0x10) {
|
||||||
|
// latch for volume
|
||||||
|
channels_[channel].volume = value & 0xf;
|
||||||
|
evaluate_output_volume();
|
||||||
|
} else {
|
||||||
|
// latch for tone/data
|
||||||
|
if(channel < 3) {
|
||||||
|
if(value & 0x80) {
|
||||||
|
channels_[channel].divider = (channels_[channel].divider & ~0xf) | (value & 0xf);
|
||||||
|
} else {
|
||||||
|
channels_[channel].divider = static_cast<uint16_t>((channels_[channel].divider & 0xf) | ((value & 0x3f) << 4));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// writes to the noise register always reset the shifter
|
||||||
|
noise_shifter_ = shifter_is_16bit_ ? 0x8000 : 0x4000;
|
||||||
|
|
||||||
|
if(value & 4) {
|
||||||
|
noise_mode_ = shifter_is_16bit_ ? Noise16 : Noise15;
|
||||||
|
} else {
|
||||||
|
noise_mode_ = shifter_is_16bit_ ? Periodic16 : Periodic15;
|
||||||
|
}
|
||||||
|
|
||||||
|
channels_[3].divider = static_cast<uint16_t>(0x10 << (value & 3));
|
||||||
|
// Special case: if these bits are both set, the noise channel should track channel 2,
|
||||||
|
// which is marked with a divider of 0xffff.
|
||||||
|
if(channels_[3].divider == 0x80) channels_[3].divider = 0xffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void SN76489::evaluate_output_volume() {
|
||||||
|
output_volume_ = static_cast<int16_t>(
|
||||||
|
channels_[0].level * volumes_[channels_[0].volume] +
|
||||||
|
channels_[1].level * volumes_[channels_[1].volume] +
|
||||||
|
channels_[2].level * volumes_[channels_[2].volume] +
|
||||||
|
channels_[3].level * volumes_[channels_[3].volume]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SN76489::get_samples(std::size_t number_of_samples, std::int16_t *target) {
|
||||||
|
// For now: assume a divide by eight.
|
||||||
|
|
||||||
|
std::size_t c = 0;
|
||||||
|
while((master_divider_&7) && c < number_of_samples) {
|
||||||
|
target[c] = output_volume_;
|
||||||
|
master_divider_++;
|
||||||
|
c++;
|
||||||
|
}
|
||||||
|
|
||||||
|
while(c < number_of_samples) {
|
||||||
|
bool did_flip = false;
|
||||||
|
|
||||||
|
#define step_channel(c) \
|
||||||
|
if(channels_[c].counter) channels_[c].counter--;\
|
||||||
|
else {\
|
||||||
|
channels_[c].level ^= 1;\
|
||||||
|
channels_[c].counter = channels_[c].divider;\
|
||||||
|
did_flip = true;\
|
||||||
|
}
|
||||||
|
|
||||||
|
step_channel(0);
|
||||||
|
step_channel(1);
|
||||||
|
step_channel(2);
|
||||||
|
|
||||||
|
#undef step_channel
|
||||||
|
|
||||||
|
if(channels_[c].divider != 0xffff) {
|
||||||
|
if(channels_[3].counter) channels_[3].counter--;
|
||||||
|
else {
|
||||||
|
did_flip = true;
|
||||||
|
channels_[c].counter = channels_[c].divider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(did_flip) {
|
||||||
|
channels_[3].level = noise_shifter_ & 1;
|
||||||
|
int new_bit = channels_[3].level;
|
||||||
|
switch(noise_mode_) {
|
||||||
|
default: break;
|
||||||
|
case Noise15:
|
||||||
|
new_bit ^= (noise_shifter_ >> 1);
|
||||||
|
break;
|
||||||
|
case Noise16:
|
||||||
|
new_bit ^= (noise_shifter_ >> 3);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
noise_shifter_ >>= 1;
|
||||||
|
noise_shifter_ |= (new_bit & 1) << (shifter_is_16bit_ ? 15 : 14);
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluate_output_volume();
|
||||||
|
|
||||||
|
for(int ic = 0; ic < 8 && c < number_of_samples; ic++) {
|
||||||
|
target[c] = output_volume_;
|
||||||
|
c++;
|
||||||
|
master_divider_++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
master_divider_ &= 7;
|
||||||
}
|
}
|
||||||
|
@ -20,10 +20,38 @@ class SN76489: public Outputs::Speaker::SampleSource {
|
|||||||
SN76489(Concurrency::DeferringAsyncTaskQueue &task_queue);
|
SN76489(Concurrency::DeferringAsyncTaskQueue &task_queue);
|
||||||
|
|
||||||
/// Writes a new value to the SN76489.
|
/// Writes a new value to the SN76489.
|
||||||
void write(uint8_t value);
|
void set_register(uint8_t value);
|
||||||
|
|
||||||
|
// As per SampleSource.
|
||||||
|
void get_samples(std::size_t number_of_samples, std::int16_t *target);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
std::size_t master_divider_ = 0;
|
||||||
|
int16_t output_volume_ = 0;;
|
||||||
|
void evaluate_output_volume();
|
||||||
|
int volumes_[16];
|
||||||
|
|
||||||
Concurrency::DeferringAsyncTaskQueue &task_queue_;
|
Concurrency::DeferringAsyncTaskQueue &task_queue_;
|
||||||
|
|
||||||
|
struct ToneChannel {
|
||||||
|
// Programmatically-set state; updated by the processor.
|
||||||
|
uint16_t divider = 0;
|
||||||
|
uint8_t volume = 0xf;
|
||||||
|
|
||||||
|
// Active state; self-evolving as a function of time.
|
||||||
|
uint16_t counter = 0;
|
||||||
|
int level = 0;
|
||||||
|
} channels_[4];
|
||||||
|
enum {
|
||||||
|
Periodic15,
|
||||||
|
Periodic16,
|
||||||
|
Noise15,
|
||||||
|
Noise16
|
||||||
|
} noise_mode_ = Periodic15;
|
||||||
|
uint16_t noise_shifter_ = 0;
|
||||||
|
int active_register_ = 0;
|
||||||
|
|
||||||
|
bool shifter_is_16bit_ = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@ class Joystick: public Inputs::Joystick {
|
|||||||
case '6': mask = 0xe; break;
|
case '6': mask = 0xe; break;
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
keypad_ = (keypad_ & 0xf0) | (mask ^ 0xf);
|
keypad_ = (keypad_ & 0xf0) | mask;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -109,6 +109,7 @@ class ConcreteMachine:
|
|||||||
z80_(*this),
|
z80_(*this),
|
||||||
sn76489_(audio_queue_),
|
sn76489_(audio_queue_),
|
||||||
speaker_(sn76489_) {
|
speaker_(sn76489_) {
|
||||||
|
speaker_.set_input_rate(3579545.0f); // TODO: try to find out whether this is correct.
|
||||||
set_clock_rate(3579545);
|
set_clock_rate(3579545);
|
||||||
joysticks_.emplace_back(new Joystick);
|
joysticks_.emplace_back(new Joystick);
|
||||||
joysticks_.emplace_back(new Joystick);
|
joysticks_.emplace_back(new Joystick);
|
||||||
@ -132,7 +133,7 @@ class ConcreteMachine:
|
|||||||
}
|
}
|
||||||
|
|
||||||
Outputs::Speaker::Speaker *get_speaker() override {
|
Outputs::Speaker::Speaker *get_speaker() override {
|
||||||
return nullptr;
|
return &speaker_;
|
||||||
}
|
}
|
||||||
|
|
||||||
void run_for(const Cycles cycles) override {
|
void run_for(const Cycles cycles) override {
|
||||||
@ -171,10 +172,15 @@ class ConcreteMachine:
|
|||||||
|
|
||||||
// MARK: Z80::BusHandler
|
// MARK: Z80::BusHandler
|
||||||
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
|
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
|
||||||
|
time_since_vdp_update_ += cycle.length;
|
||||||
|
time_since_sn76489_update_ += cycle.length;
|
||||||
|
|
||||||
if(time_until_interrupt_ > 0) {
|
if(time_until_interrupt_ > 0) {
|
||||||
time_until_interrupt_ -= cycle.length;
|
time_until_interrupt_ -= cycle.length;
|
||||||
if(time_until_interrupt_ <= HalfCycles(0)) {
|
if(time_until_interrupt_ <= HalfCycles(0)) {
|
||||||
z80_.set_non_maskable_interrupt_line(true, time_until_interrupt_);
|
z80_.set_non_maskable_interrupt_line(true, time_until_interrupt_);
|
||||||
|
update_video();
|
||||||
|
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,8 +192,8 @@ class ConcreteMachine:
|
|||||||
*cycle.value = bios_[address];
|
*cycle.value = bios_[address];
|
||||||
} else if(address >= 0x6000 && address < 0x8000) {
|
} else if(address >= 0x6000 && address < 0x8000) {
|
||||||
*cycle.value = ram_[address & 1023];
|
*cycle.value = ram_[address & 1023];
|
||||||
} else if(address >= 0x8000 && address < 0x8000 + cartridge_.size()) {
|
} else if(address >= 0x8000) {
|
||||||
*cycle.value = cartridge_[address - 0x8000];
|
*cycle.value = cartridge_[(address - 0x8000) % cartridge_.size()]; // This probably isn't how 24kb ROMs work?
|
||||||
} else {
|
} else {
|
||||||
*cycle.value = 0xff;
|
*cycle.value = 0xff;
|
||||||
}
|
}
|
||||||
@ -202,7 +208,7 @@ class ConcreteMachine:
|
|||||||
case CPU::Z80::PartialMachineCycle::Input:
|
case CPU::Z80::PartialMachineCycle::Input:
|
||||||
switch((address >> 5) & 7) {
|
switch((address >> 5) & 7) {
|
||||||
case 5:
|
case 5:
|
||||||
vdp_->run_for(time_since_vdp_update_.flush());
|
update_video();
|
||||||
*cycle.value = vdp_->get_register(address);
|
*cycle.value = vdp_->get_register(address);
|
||||||
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
|
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
|
||||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||||
@ -232,7 +238,7 @@ class ConcreteMachine:
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 5:
|
case 5:
|
||||||
vdp_->run_for(time_since_vdp_update_.flush());
|
update_video();
|
||||||
vdp_->set_register(address, *cycle.value);
|
vdp_->set_register(address, *cycle.value);
|
||||||
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
|
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
|
||||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||||
@ -240,7 +246,7 @@ class ConcreteMachine:
|
|||||||
|
|
||||||
case 7:
|
case 7:
|
||||||
update_audio();
|
update_audio();
|
||||||
sn76489_.write(*cycle.value);
|
sn76489_.set_register(*cycle.value);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default: break;
|
default: break;
|
||||||
@ -250,8 +256,6 @@ class ConcreteMachine:
|
|||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
|
|
||||||
time_since_vdp_update_ += cycle.length;
|
|
||||||
time_since_sn76489_update_ += cycle.length;
|
|
||||||
return HalfCycles(0);
|
return HalfCycles(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -265,6 +269,9 @@ class ConcreteMachine:
|
|||||||
void update_audio() {
|
void update_audio() {
|
||||||
speaker_.run_for(audio_queue_, time_since_sn76489_update_.divide_cycles(Cycles(2)));
|
speaker_.run_for(audio_queue_, time_since_sn76489_update_.divide_cycles(Cycles(2)));
|
||||||
}
|
}
|
||||||
|
void update_video() {
|
||||||
|
vdp_->run_for(time_since_vdp_update_.flush());
|
||||||
|
}
|
||||||
|
|
||||||
CPU::Z80::Processor<ConcreteMachine, false, false> z80_;
|
CPU::Z80::Processor<ConcreteMachine, false, false> z80_;
|
||||||
std::unique_ptr<TI::TMS9918> vdp_;
|
std::unique_ptr<TI::TMS9918> vdp_;
|
||||||
|
Loading…
Reference in New Issue
Block a user