From d4df101ab6ccd3733a2379adde39d3103fc1ba10 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Tue, 27 Feb 2018 22:25:12 -0500 Subject: [PATCH] Makes a first attempt at implementing the SN76489. --- Components/AY38910/AY38910.cpp | 1 + Components/SN76489/SN76489.cpp | 125 ++++++++++++++++++++++++- Components/SN76489/SN76489.hpp | 30 +++++- Machines/ColecoVision/ColecoVision.cpp | 25 +++-- 4 files changed, 168 insertions(+), 13 deletions(-) diff --git a/Components/AY38910/AY38910.cpp b/Components/AY38910/AY38910.cpp index 205fd39e0..67e74b9a1 100644 --- a/Components/AY38910/AY38910.cpp +++ b/Components/AY38910/AY38910.cpp @@ -108,6 +108,7 @@ void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) { } evaluate_output_volume(); + printf("%d ", output_volume_); for(int ic = 0; ic < 8 && c < number_of_samples; ic++) { target[c] = output_volume_; diff --git a/Components/SN76489/SN76489.cpp b/Components/SN76489/SN76489.cpp index fb49704e4..a6e8f3330 100644 --- a/Components/SN76489/SN76489.cpp +++ b/Components/SN76489/SN76489.cpp @@ -8,9 +8,128 @@ #include "SN76489.hpp" +#include + using namespace TI; -SN76489::SN76489(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) {} - -void SN76489::write(uint8_t value) { +SN76489::SN76489(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) { + // Build a volume table. + 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((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(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( + 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; } diff --git a/Components/SN76489/SN76489.hpp b/Components/SN76489/SN76489.hpp index 9212b75de..409df0428 100644 --- a/Components/SN76489/SN76489.hpp +++ b/Components/SN76489/SN76489.hpp @@ -20,10 +20,38 @@ class SN76489: public Outputs::Speaker::SampleSource { SN76489(Concurrency::DeferringAsyncTaskQueue &task_queue); /// 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: + std::size_t master_divider_ = 0; + int16_t output_volume_ = 0;; + void evaluate_output_volume(); + int volumes_[16]; + 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; }; } diff --git a/Machines/ColecoVision/ColecoVision.cpp b/Machines/ColecoVision/ColecoVision.cpp index 375359c1d..91242f97d 100644 --- a/Machines/ColecoVision/ColecoVision.cpp +++ b/Machines/ColecoVision/ColecoVision.cpp @@ -66,7 +66,7 @@ class Joystick: public Inputs::Joystick { case '6': mask = 0xe; break; default: break; } - keypad_ = (keypad_ & 0xf0) | (mask ^ 0xf); + keypad_ = (keypad_ & 0xf0) | mask; } break; @@ -109,6 +109,7 @@ class ConcreteMachine: z80_(*this), sn76489_(audio_queue_), speaker_(sn76489_) { + speaker_.set_input_rate(3579545.0f); // TODO: try to find out whether this is correct. set_clock_rate(3579545); joysticks_.emplace_back(new Joystick); joysticks_.emplace_back(new Joystick); @@ -132,7 +133,7 @@ class ConcreteMachine: } Outputs::Speaker::Speaker *get_speaker() override { - return nullptr; + return &speaker_; } void run_for(const Cycles cycles) override { @@ -171,10 +172,15 @@ class ConcreteMachine: // MARK: Z80::BusHandler 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) { time_until_interrupt_ -= cycle.length; if(time_until_interrupt_ <= HalfCycles(0)) { 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]; } else if(address >= 0x6000 && address < 0x8000) { *cycle.value = ram_[address & 1023]; - } else if(address >= 0x8000 && address < 0x8000 + cartridge_.size()) { - *cycle.value = cartridge_[address - 0x8000]; + } else if(address >= 0x8000) { + *cycle.value = cartridge_[(address - 0x8000) % cartridge_.size()]; // This probably isn't how 24kb ROMs work? } else { *cycle.value = 0xff; } @@ -202,7 +208,7 @@ class ConcreteMachine: case CPU::Z80::PartialMachineCycle::Input: switch((address >> 5) & 7) { case 5: - vdp_->run_for(time_since_vdp_update_.flush()); + update_video(); *cycle.value = vdp_->get_register(address); z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line()); time_until_interrupt_ = vdp_->get_time_until_interrupt(); @@ -232,7 +238,7 @@ class ConcreteMachine: break; case 5: - vdp_->run_for(time_since_vdp_update_.flush()); + update_video(); vdp_->set_register(address, *cycle.value); z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line()); time_until_interrupt_ = vdp_->get_time_until_interrupt(); @@ -240,7 +246,7 @@ class ConcreteMachine: case 7: update_audio(); - sn76489_.write(*cycle.value); + sn76489_.set_register(*cycle.value); break; default: break; @@ -250,8 +256,6 @@ class ConcreteMachine: default: break; } - time_since_vdp_update_ += cycle.length; - time_since_sn76489_update_ += cycle.length; return HalfCycles(0); } @@ -265,6 +269,9 @@ class ConcreteMachine: void update_audio() { 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 z80_; std::unique_ptr vdp_;