From 15b5a62e01ad36a96dcce063d6bc5ea36aa617e8 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Tue, 23 Jan 2024 22:05:30 -0500 Subject: [PATCH 01/15] Mockingboard: start sketching out intermediate clocking. --- Analyser/Static/AppleII/Target.hpp | 6 ++ Components/AY38910/AY38910.hpp | 2 +- Machines/Apple/AppleII/AppleII.cpp | 89 +++++++++++++++++++++--------- 3 files changed, 71 insertions(+), 26 deletions(-) diff --git a/Analyser/Static/AppleII/Target.hpp b/Analyser/Static/AppleII/Target.hpp index 0ad262201..8002a2009 100644 --- a/Analyser/Static/AppleII/Target.hpp +++ b/Analyser/Static/AppleII/Target.hpp @@ -34,12 +34,14 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImpl class AY38910: public ::Outputs::Speaker::SampleSource */ void set_output_mixing(float a_left, float b_left, float c_left, float a_right = 1.0, float b_right = 1.0, float c_right = 1.0); - // to satisfy ::Outputs::Speaker (included via ::Outputs::Filter. + // To satisfy ::Outputs::Speaker::SampleSource. void get_samples(std::size_t number_of_samples, int16_t *target); bool is_zero_level() const; void set_sample_volume_range(std::int16_t range); diff --git a/Machines/Apple/AppleII/AppleII.cpp b/Machines/Apple/AppleII/AppleII.cpp index dace81906..0880bf8d0 100644 --- a/Machines/Apple/AppleII/AppleII.cpp +++ b/Machines/Apple/AppleII/AppleII.cpp @@ -41,16 +41,57 @@ namespace { -constexpr int DiskIISlot = 6; // Apple recommended slot 6 for the (first) Disk II. -constexpr int SCSISlot = 7; // Install the SCSI card in slot 7, to one-up any connected Disk II. +constexpr int DiskIISlot = 6; // Apple recommended slot 6 for the (first) Disk II. +constexpr int SCSISlot = 7; // Install the SCSI card in slot 7, to one-up any connected Disk II. +constexpr int MockingboardSlot = 4; // Conventional Mockingboard slot. + + +// The system's master clock rate. +// +// Quick note on this: +// +// * 64 out of 65 CPU cycles last for 14 cycles of the master clock; +// * every 65th cycle lasts for 16 cycles of the master clock; +// * that keeps CPU cycles in-phase with the colour subcarrier: each line of output is 64*14 + 16 = 912 master cycles long; +// * ... and since hsync is placed to make each line 228 colour clocks long that means 4 master clocks per colour clock; +// * ... which is why all Apple II video modes are expressible as four binary outputs per colour clock; +// * ... and hence seven pixels per memory access window clock in high-res mode, 14 in double high-res, etc. +constexpr float master_clock = 14318180.0; + +template +class StretchedAudioSource { + public: + void get_samples(std::size_t number_of_samples, int16_t *target) { + // Approach should look something like — assuming a fully digital source_: + // + // (1) sample_offset_ is always kept in the range [0, 912); + // (2) break down sample generation into 912-cycle blocks, getting the underlying source to provide + // the first 910 samples in each, then duplicate the final two. + // + // (possibly subject to an appropriate divider, with 'by two' being the obvious potential choice) + (void)number_of_samples; + (void)target; + } + + // Direct passthroughs. + void set_sample_volume_range(std::int16_t range) { + source_.set_sample_volume_range(range); + } + bool is_zero_level() const { return source_.is_zero_level(); } + static constexpr bool get_is_stereo() { return SourceT::get_is_stereo(); } + + private: + SourceT source_; + std::size_t sample_offset_ = 0; + + static constexpr std::size_t clock_length = 7; +}; } namespace Apple { namespace II { -#define is_iie() ((model == Analyser::Static::AppleII::Target::Model::IIe) || (model == Analyser::Static::AppleII::Target::Model::EnhancedIIe)) - template class ConcreteMachine: public Apple::II::Machine, public MachineTypes::TimedMachine, @@ -83,7 +124,7 @@ template class ConcreteMachine: false>; Processor m6502_; VideoBusHandler video_bus_handler_; - Apple::II::Video::Video video_; + Apple::II::Video::Video video_; int cycles_into_current_line_ = 0; Cycles cycles_since_video_update_; @@ -225,7 +266,7 @@ template class ConcreteMachine: const auto zero_state = auxiliary_switches_.zero_state(); uint8_t *const ram = zero_state ? aux_ram_ : ram_; - uint8_t *const rom = is_iie() ? &rom_[3840] : rom_.data(); + uint8_t *const rom = is_iie(model) ? &rom_[3840] : rom_.data(); // Which way the region here is mapped to be banks 1 and 2 is // arbitrary. @@ -286,7 +327,7 @@ template class ConcreteMachine: } bool set_key_pressed(Key key, char value, bool is_pressed, bool is_repeat) final { - if constexpr (!is_iie()) { + if constexpr (!is_iie(model)) { if(is_repeat && !repeat_is_pressed_) return true; } @@ -297,7 +338,7 @@ template class ConcreteMachine: case Key::Down: value = 0x0a; break; case Key::Up: value = 0x0b; break; case Key::Backspace: - if(is_iie()) { + if(is_iie(model)) { value = 0x7f; break; } else { @@ -305,7 +346,7 @@ template class ConcreteMachine: } case Key::Enter: value = 0x0d; break; case Key::Tab: - if (is_iie()) { + if (is_iie(model)) { value = '\t'; break; } else { @@ -316,7 +357,7 @@ template class ConcreteMachine: case Key::LeftOption: case Key::RightMeta: - if (is_iie()) { + if (is_iie(model)) { open_apple_is_pressed = is_pressed; return true; } else { @@ -325,7 +366,7 @@ template class ConcreteMachine: case Key::RightOption: case Key::LeftMeta: - if (is_iie()) { + if (is_iie(model)) { closed_apple_is_pressed = is_pressed; return true; } else { @@ -346,7 +387,7 @@ template class ConcreteMachine: case Key::F9: case Key::F10: case Key::F11: repeat_is_pressed_ = is_pressed; - if constexpr (!is_iie()) { + if constexpr (!is_iie(model)) { if(is_pressed && (!is_repeat || character_is_pressed_)) { keyboard_input_ = uint8_t(last_pressed_character_ | 0x80); } @@ -374,12 +415,12 @@ template class ConcreteMachine: } // Prior to the IIe, the keyboard could produce uppercase only. - if(!is_iie()) value = char(toupper(value)); + if(!is_iie(model)) value = char(toupper(value)); if(control_is_pressed_ && isalpha(value)) value &= 0xbf; // TODO: properly map IIe keys - if(!is_iie() && shift_is_pressed_) { + if(!is_iie(model) && shift_is_pressed_) { switch(value) { case 0x27: value = 0x22; break; // ' -> " case 0x2c: value = 0x3c; break; // , -> < @@ -484,8 +525,6 @@ template class ConcreteMachine: language_card_(*this), auxiliary_switches_(*this), keyboard_(&m6502_) { - // The system's master clock rate. - constexpr float master_clock = 14318180.0; // This is where things get slightly convoluted: establish the machine as having a clock rate // equal to the number of cycles of work the 6502 will actually achieve. Which is less than @@ -629,7 +668,7 @@ template class ConcreteMachine: if(write_pages_[address >> 8]) write_pages_[address >> 8][address & 0xff] = *value; } - if(is_iie()) { + if(is_iie(model)) { auxiliary_switches_.access(address, isReadOperation(operation)); } } else { @@ -669,7 +708,7 @@ template class ConcreteMachine: *value &= 0x7f; if( joysticks_.button(0) || - (is_iie() && keyboard_.open_apple_is_pressed) + (is_iie(model) && keyboard_.open_apple_is_pressed) ) *value |= 0x80; break; @@ -677,7 +716,7 @@ template class ConcreteMachine: *value &= 0x7f; if( joysticks_.button(1) || - (is_iie() && keyboard_.closed_apple_is_pressed) + (is_iie(model) && keyboard_.closed_apple_is_pressed) ) *value |= 0x80; break; @@ -699,7 +738,7 @@ template class ConcreteMachine: } break; // The IIe-only state reads follow... -#define IIeSwitchRead(s) *value = keyboard_.get_keyboard_input(); if(is_iie()) *value = (*value & 0x7f) | (s ? 0x80 : 0x00); +#define IIeSwitchRead(s) *value = keyboard_.get_keyboard_input(); if(is_iie(model)) *value = (*value & 0x7f) | (s ? 0x80 : 0x00); case 0xc011: IIeSwitchRead(language_card_.state().bank2); break; case 0xc012: IIeSwitchRead(language_card_.state().read); break; case 0xc013: IIeSwitchRead(auxiliary_switches_.switches().read_auxiliary_memory); break; @@ -718,12 +757,12 @@ template class ConcreteMachine: #undef IIeSwitchRead case 0xc07f: - if(is_iie()) *value = (*value & 0x7f) | (video_.get_annunciator_3() ? 0x80 : 0x00); + if(is_iie(model)) *value = (*value & 0x7f) | (video_.get_annunciator_3() ? 0x80 : 0x00); break; } } else { // Write-only switches. All IIe as currently implemented. - if(is_iie()) { + if(is_iie(model)) { auxiliary_switches_.access(address, false); switch(address) { default: break; @@ -775,7 +814,7 @@ template class ConcreteMachine: case 0xc05e: case 0xc05f: - if(is_iie()) { + if(is_iie(model)) { update_video(); video_.set_annunciator_3(!(address&1)); } @@ -785,7 +824,7 @@ template class ConcreteMachine: keyboard_.clear_keyboard_input(); // On the IIe, reading C010 returns additional key info. - if(is_iie() && isReadOperation(operation)) { + if(is_iie(model) && isReadOperation(operation)) { *value = (keyboard_.get_key_is_down() ? 0x80 : 0x00) | (keyboard_.get_keyboard_input() & 0x7f); } break; @@ -922,7 +961,7 @@ template class ConcreteMachine: } bool prefers_logical_input() final { - return is_iie(); + return is_iie(model); } Inputs::Keyboard &get_keyboard() final { From 17cad731777584b7ef9297847e56540c6423c88e Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Mon, 29 Jan 2024 16:45:20 -0500 Subject: [PATCH 02/15] Attempt an implementation of `StretchedAudioSource`. --- Machines/Apple/AppleII/AppleII.cpp | 68 +++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/Machines/Apple/AppleII/AppleII.cpp b/Machines/Apple/AppleII/AppleII.cpp index 0880bf8d0..16cf8b89e 100644 --- a/Machines/Apple/AppleII/AppleII.cpp +++ b/Machines/Apple/AppleII/AppleII.cpp @@ -58,19 +58,65 @@ constexpr int MockingboardSlot = 4; // Conventional Mockingboard slot. // * ... and hence seven pixels per memory access window clock in high-res mode, 14 in double high-res, etc. constexpr float master_clock = 14318180.0; -template +/// Provides an audio source with will hold an instance of @c SourceT for audio generation, repeating one of its output samples +/// after every @c PacketClocks of output. +template class StretchedAudioSource { public: + template + StretchedAudioSource(Args &&... args) : source_(std::forward(args)...) {} + void get_samples(std::size_t number_of_samples, int16_t *target) { - // Approach should look something like — assuming a fully digital source_: + // Possible: the sample to duplicate fell exactly at the end of the last batch. + if(!remaining_packet_) { + remaining_packet_ = PacketClocks; + if constexpr (source_.get_is_stero()) { + target[0] = carry[0]; + target[1] = carry[1]; + target += 2; + } else { + target[0] = carry[0]; + ++target; + } + --number_of_samples; + } + + // Divide required input into groups of PacketClocks samples; get all of those + // from the underlying source and then duplicate the final one. // - // (1) sample_offset_ is always kept in the range [0, 912); - // (2) break down sample generation into 912-cycle blocks, getting the underlying source to provide - // the first 910 samples in each, then duplicate the final two. - // - // (possibly subject to an appropriate divider, with 'by two' being the obvious potential choice) - (void)number_of_samples; - (void)target; + // Duplicating into the current output packet won't be possible if it happens + // to line up exactly with the end of the PacketClocks, in which case hold a copy + // of the last level output in `carry` until next time. + while(number_of_samples) { + const auto target_length = std::min(number_of_samples, remaining_packet_); + source_.get_samples(target_length, target); + + target += target_length * (1 + source_.get_is_stero()); + number_of_samples -= target_length; + remaining_packet_ -= target_length; + + if(!remaining_packet_) { + if(number_of_samples) { + remaining_packet_ = PacketClocks; + + if constexpr (source_.get_is_stero()) { + target[0] = target[-2]; + target[1] = target[-1]; + target += 2; + } else { + target[0] = target[-1]; + ++target; + } + } else { + if constexpr (source_.get_is_stero()) { + carry[0] = target[-2]; + carry[1] = target[-1]; + } else { + carry[0] = target[-1]; + } + } + } + } } // Direct passthroughs. @@ -82,9 +128,9 @@ class StretchedAudioSource { private: SourceT source_; - std::size_t sample_offset_ = 0; - static constexpr std::size_t clock_length = 7; + std::size_t remaining_packet_ = PacketClocks; + std::array carry; }; } From 27059233b3a8472d3017aa1722d3d4ebe3e027a2 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Tue, 13 Feb 2024 22:38:18 -0500 Subject: [PATCH 03/15] Use sample source to simplify stretching AY. --- Machines/Apple/AppleII/AppleII.cpp | 94 ++++++++++-------------------- 1 file changed, 30 insertions(+), 64 deletions(-) diff --git a/Machines/Apple/AppleII/AppleII.cpp b/Machines/Apple/AppleII/AppleII.cpp index 16cf8b89e..63ab3713a 100644 --- a/Machines/Apple/AppleII/AppleII.cpp +++ b/Machines/Apple/AppleII/AppleII.cpp @@ -15,6 +15,7 @@ #include "../../../Processors/6502/6502.hpp" #include "../../../Components/AudioToggle/AudioToggle.hpp" +#include "../../../Components/AY38910/AY38910.hpp" #include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" #include "../../../Outputs/Log.hpp" @@ -58,79 +59,44 @@ constexpr int MockingboardSlot = 4; // Conventional Mockingboard slot. // * ... and hence seven pixels per memory access window clock in high-res mode, 14 in double high-res, etc. constexpr float master_clock = 14318180.0; -/// Provides an audio source with will hold an instance of @c SourceT for audio generation, repeating one of its output samples -/// after every @c PacketClocks of output. -template -class StretchedAudioSource { - public: - template - StretchedAudioSource(Args &&... args) : source_(std::forward(args)...) {} +/// Provides an AY that runs at the CPU rate divided by 4 given an input of the master clock divided by 2, +/// allowing for stretched CPU clock cycles. +struct StretchedAY: + public GI::AY38910::AY38910SampleSource, + public Outputs::Speaker::BufferSource { - void get_samples(std::size_t number_of_samples, int16_t *target) { - // Possible: the sample to duplicate fell exactly at the end of the last batch. - if(!remaining_packet_) { - remaining_packet_ = PacketClocks; - if constexpr (source_.get_is_stero()) { - target[0] = carry[0]; - target[1] = carry[1]; - target += 2; - } else { - target[0] = carry[0]; - ++target; - } - --number_of_samples; - } + template + void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) { - // Divide required input into groups of PacketClocks samples; get all of those - // from the underlying source and then duplicate the final one. + // (1) take 64 windows of 7 input cycles followed by one window of 8 input cycles; + // (2) after each four windows, advance the underlying AY. // - // Duplicating into the current output packet won't be possible if it happens - // to line up exactly with the end of the PacketClocks, in which case hold a copy - // of the last level output in `carry` until next time. - while(number_of_samples) { - const auto target_length = std::min(number_of_samples, remaining_packet_); - source_.get_samples(target_length, target); + // i.e. advance after: + // + // * 28 cycles, {16 times, then 15 times, then 15 times, then 15 times}; + // * 29 cycles, once. + // + // so: + // 16, 1; 15, 1; 15, 1; 15, 1 + // + // i.e. add an extra one on the 17th, 33rd, 49th and 65th ticks in a 65-tick loop. + for(std::size_t c = 0; c < number_of_samples; c++) { + ++subdivider_; + if(subdivider_ == 28) { + ++phase_; + subdivider_ = (phase_ & 15) ? 0 : -1; + if(phase_ == 65) phase_ = 0; - target += target_length * (1 + source_.get_is_stero()); - number_of_samples -= target_length; - remaining_packet_ -= target_length; - - if(!remaining_packet_) { - if(number_of_samples) { - remaining_packet_ = PacketClocks; - - if constexpr (source_.get_is_stero()) { - target[0] = target[-2]; - target[1] = target[-1]; - target += 2; - } else { - target[0] = target[-1]; - ++target; - } - } else { - if constexpr (source_.get_is_stero()) { - carry[0] = target[-2]; - carry[1] = target[-1]; - } else { - carry[0] = target[-1]; - } - } + advance(); } + + target[c] = level(); } } - // Direct passthroughs. - void set_sample_volume_range(std::int16_t range) { - source_.set_sample_volume_range(range); - } - bool is_zero_level() const { return source_.is_zero_level(); } - static constexpr bool get_is_stereo() { return SourceT::get_is_stereo(); } - private: - SourceT source_; - - std::size_t remaining_packet_ = PacketClocks; - std::array carry; + int phase_ = 0; + int subdivider_ = 0; }; } From 2a684ab3029a20b62995652ebab0da8c92a04757 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Wed, 14 Feb 2024 10:55:53 -0500 Subject: [PATCH 04/15] Include a single AY in the mix if appropriate. --- Machines/Apple/AppleII/AppleII.cpp | 48 ++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/Machines/Apple/AppleII/AppleII.cpp b/Machines/Apple/AppleII/AppleII.cpp index 63ab3713a..8680d6ce3 100644 --- a/Machines/Apple/AppleII/AppleII.cpp +++ b/Machines/Apple/AppleII/AppleII.cpp @@ -17,6 +17,7 @@ #include "../../../Components/AudioToggle/AudioToggle.hpp" #include "../../../Components/AY38910/AY38910.hpp" +#include "../../../Outputs/Speaker/Implementation/CompoundSource.hpp" #include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" #include "../../../Outputs/Log.hpp" @@ -65,6 +66,8 @@ struct StretchedAY: public GI::AY38910::AY38910SampleSource, public Outputs::Speaker::BufferSource { + using GI::AY38910::AY38910SampleSource::AY38910SampleSource; + template void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) { @@ -104,7 +107,7 @@ struct StretchedAY: namespace Apple { namespace II { -template class ConcreteMachine: +template class ConcreteMachine: public Apple::II::Machine, public MachineTypes::TimedMachine, public MachineTypes::ScanProducer, @@ -162,9 +165,23 @@ template class ConcreteMachine: Concurrency::AsyncTaskQueue audio_queue_; Audio::Toggle audio_toggle_; - Outputs::Speaker::PullLowpass speaker_; + StretchedAY ay_; + using SourceT = + std::conditional_t, Audio::Toggle>; + using LowpassT = Outputs::Speaker::PullLowpass; + + Outputs::Speaker::CompoundSource mixer_; + Outputs::Speaker::PullLowpass speaker_; Cycles cycles_since_audio_update_; + constexpr SourceT &lowpass_source() { + if constexpr (has_mockingboard) { + return mixer_; + } else { + return audio_toggle_; + } + } + // MARK: - Cards static constexpr size_t NoActiveCard = 7; // There is no 'card 0' in internal numbering. size_t active_card_ = NoActiveCard; @@ -533,7 +550,9 @@ template class ConcreteMachine: video_bus_handler_(ram_, aux_ram_), video_(video_bus_handler_), audio_toggle_(audio_queue_), - speaker_(audio_toggle_), + ay_(GI::AY38910::Personality::AY38910, audio_queue_), + mixer_(audio_toggle_, ay_), + speaker_(lowpass_source()), language_card_(*this), auxiliary_switches_(*this), keyboard_(&m6502_) { @@ -1039,12 +1058,23 @@ using namespace Apple::II; std::unique_ptr Machine::AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { using Target = Analyser::Static::AppleII::Target; const Target *const appleii_target = dynamic_cast(target); - switch(appleii_target->model) { - default: return nullptr; - case Target::Model::II: return std::make_unique>(*appleii_target, rom_fetcher); - case Target::Model::IIplus: return std::make_unique>(*appleii_target, rom_fetcher); - case Target::Model::IIe: return std::make_unique>(*appleii_target, rom_fetcher); - case Target::Model::EnhancedIIe: return std::make_unique>(*appleii_target, rom_fetcher); + + if(appleii_target->has_mockingboard) { + switch(appleii_target->model) { + default: return nullptr; + case Target::Model::II: return std::make_unique>(*appleii_target, rom_fetcher); + case Target::Model::IIplus: return std::make_unique>(*appleii_target, rom_fetcher); + case Target::Model::IIe: return std::make_unique>(*appleii_target, rom_fetcher); + case Target::Model::EnhancedIIe: return std::make_unique>(*appleii_target, rom_fetcher); + } + } else { + switch(appleii_target->model) { + default: return nullptr; + case Target::Model::II: return std::make_unique>(*appleii_target, rom_fetcher); + case Target::Model::IIplus: return std::make_unique>(*appleii_target, rom_fetcher); + case Target::Model::IIe: return std::make_unique>(*appleii_target, rom_fetcher); + case Target::Model::EnhancedIIe: return std::make_unique>(*appleii_target, rom_fetcher); + } } } From 0dcceff4102b2571e3a5a326837ebe4f8b8d116c Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Wed, 14 Feb 2024 14:31:38 -0500 Subject: [PATCH 05/15] There's actually two AYs. --- Components/AY38910/AY38910.hpp | 1 + Machines/Apple/AppleII/AppleII.cpp | 36 +++++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/Components/AY38910/AY38910.hpp b/Components/AY38910/AY38910.hpp index 0c21c1063..acdc945b7 100644 --- a/Components/AY38910/AY38910.hpp +++ b/Components/AY38910/AY38910.hpp @@ -70,6 +70,7 @@ template class AY38910SampleSource { public: /// Creates a new AY38910. AY38910SampleSource(Personality, Concurrency::AsyncTaskQueue &); + AY38910SampleSource(const AY38910SampleSource &) = delete; /// Sets the value the AY would read from its data lines if it were not outputting. void set_data_input(uint8_t r); diff --git a/Machines/Apple/AppleII/AppleII.cpp b/Machines/Apple/AppleII/AppleII.cpp index 8680d6ce3..4eabdfebd 100644 --- a/Machines/Apple/AppleII/AppleII.cpp +++ b/Machines/Apple/AppleII/AppleII.cpp @@ -60,13 +60,43 @@ constexpr int MockingboardSlot = 4; // Conventional Mockingboard slot. // * ... and hence seven pixels per memory access window clock in high-res mode, 14 in double high-res, etc. constexpr float master_clock = 14318180.0; +class AYPair { + public: + AYPair(Concurrency::AsyncTaskQueue &queue) : + ays_{ + {GI::AY38910::Personality::AY38910, queue}, + {GI::AY38910::Personality::AY38910, queue}, + } {} + + void advance() { + ays_[0].advance(); + ays_[1].advance(); + } + + void set_sample_volume_range(std::int16_t range) { + ays_[0].set_sample_volume_range(range >> 1); + ays_[1].set_sample_volume_range(range >> 1); + } + + bool is_zero_level() const { + return ays_[0].is_zero_level() && ays_[1].is_zero_level(); + } + + Outputs::Speaker::MonoSample level() const { + return ays_[0].level() + ays_[1].level(); + } + + private: + GI::AY38910::AY38910SampleSource ays_[2]; +}; + /// Provides an AY that runs at the CPU rate divided by 4 given an input of the master clock divided by 2, /// allowing for stretched CPU clock cycles. struct StretchedAY: - public GI::AY38910::AY38910SampleSource, + AYPair, public Outputs::Speaker::BufferSource { - using GI::AY38910::AY38910SampleSource::AY38910SampleSource; + using AYPair::AYPair; template void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) { @@ -550,7 +580,7 @@ template video_bus_handler_(ram_, aux_ram_), video_(video_bus_handler_), audio_toggle_(audio_queue_), - ay_(GI::AY38910::Personality::AY38910, audio_queue_), + ay_(audio_queue_), mixer_(audio_toggle_, ay_), speaker_(lowpass_source()), language_card_(*this), From 07c11e8268a7a1389c793fa80da80c9f6b2487a5 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Wed, 14 Feb 2024 15:18:19 -0500 Subject: [PATCH 06/15] Begin 6522 wiring. --- Analyser/Static/AppleII/Target.hpp | 2 +- Machines/Apple/AppleII/AppleII.cpp | 25 ++++++++--- Machines/Apple/AppleII/Mockingboard.hpp | 44 +++++++++++++++++++ .../Clock Signal.xcodeproj/project.pbxproj | 2 + 4 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 Machines/Apple/AppleII/Mockingboard.hpp diff --git a/Analyser/Static/AppleII/Target.hpp b/Analyser/Static/AppleII/Target.hpp index 8002a2009..f08bf1f68 100644 --- a/Analyser/Static/AppleII/Target.hpp +++ b/Analyser/Static/AppleII/Target.hpp @@ -34,7 +34,7 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImpl { + public Outputs::Speaker::BufferSource { using AYPair::AYPair; @@ -195,12 +196,12 @@ template Concurrency::AsyncTaskQueue audio_queue_; Audio::Toggle audio_toggle_; - StretchedAY ay_; + StretchedAYPair ays_; using SourceT = - std::conditional_t, Audio::Toggle>; + std::conditional_t, Audio::Toggle>; using LowpassT = Outputs::Speaker::PullLowpass; - Outputs::Speaker::CompoundSource mixer_; + Outputs::Speaker::CompoundSource mixer_; Outputs::Speaker::PullLowpass speaker_; Cycles cycles_since_audio_update_; @@ -255,6 +256,10 @@ template pick_card_messaging_group(card); } + Apple::II::Mockingboard *mockingboard() { + return dynamic_cast(cards_[MockingboardSlot - 1].get()); + } + Apple::II::DiskIICard *diskii_card() { return dynamic_cast(cards_[DiskIISlot - 1].get()); } @@ -580,8 +585,8 @@ template video_bus_handler_(ram_, aux_ram_), video_(video_bus_handler_), audio_toggle_(audio_queue_), - ay_(audio_queue_), - mixer_(audio_toggle_, ay_), + ays_(audio_queue_), + mixer_(audio_toggle_, ays_), speaker_(lowpass_source()), language_card_(*this), auxiliary_switches_(*this), @@ -659,6 +664,12 @@ template install_card(SCSISlot, new Apple::II::SCSICard(roms, int(master_clock / 14.0f))); } + if(target.has_mockingboard) { + // The Mockingboard has a parasitic relationship with this class due to the way + // that audio outputs are implemented in this emulator. + install_card(MockingboardSlot, new Apple::II::Mockingboard()); + } + rom_ = std::move(roms.find(system)->second); // The IIe and Enhanced IIe ROMs often distributed are oversized; trim if necessary. if(system == ROM::Name::AppleIIe || system == ROM::Name::AppleIIEnhancedE) { diff --git a/Machines/Apple/AppleII/Mockingboard.hpp b/Machines/Apple/AppleII/Mockingboard.hpp new file mode 100644 index 000000000..dbde129e5 --- /dev/null +++ b/Machines/Apple/AppleII/Mockingboard.hpp @@ -0,0 +1,44 @@ +// +// Mockingboard.hpp +// Clock Signal +// +// Created by Thomas Harte on 14/02/2024. +// Copyright © 2024 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "Card.hpp" + +#include "../../../Components/6522/6522.hpp" + + +namespace Apple::II { + +class Mockingboard: public Card { + public: + Mockingboard() : + vias_{ {handlers_[0]}, {handlers_[1]} } {} + + void perform_bus_operation(Select select, bool is_read, uint16_t address, uint8_t *value) final { + if(!(select & Device)) { + return; + } + + int index = (address >> 7) & 1; + if(is_read) { + *value = vias_[index].read(address); + } else { + vias_[index].write(address, *value); + } + } + + private: + class AYVIA: public MOS::MOS6522::IRQDelegatePortHandler { + }; + + MOS::MOS6522::MOS6522 vias_[2]; + AYVIA handlers_[2]; +}; + +} diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 3babbde3e..069031998 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -1568,6 +1568,7 @@ 4B71368D1F788112008B8ED9 /* Parser.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Parser.hpp; sourceTree = ""; }; 4B71368F1F789C93008B8ED9 /* SegmentParser.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = SegmentParser.cpp; sourceTree = ""; }; 4B7136901F789C93008B8ED9 /* SegmentParser.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = SegmentParser.hpp; sourceTree = ""; }; + 4B72FAEF2B7D51BB000C7E90 /* Mockingboard.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Mockingboard.hpp; sourceTree = ""; }; 4B74CF7F2312FA9C00500CE8 /* HFV.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = HFV.hpp; sourceTree = ""; }; 4B74CF802312FA9C00500CE8 /* HFV.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = HFV.cpp; sourceTree = ""; }; 4B75F978280D7C5100121055 /* 68000DecoderTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = 68000DecoderTests.mm; sourceTree = ""; }; @@ -4840,6 +4841,7 @@ 4B2E86E125DC95150024F1E9 /* Joystick.hpp */, 4BF40A5525424C770033EA39 /* LanguageCardSwitches.hpp */, 4BE0151C286A8C8E00EA42E9 /* MemorySwitches.hpp */, + 4B72FAEF2B7D51BB000C7E90 /* Mockingboard.hpp */, 4B4C81C428B3C5CD00F84AE9 /* SCSICard.hpp */, 4BCE004F227CE8CA000CA200 /* Video.hpp */, 4B8DF4F2254E141700F3433C /* VideoSwitches.hpp */, From 1e877c7563a889461d913b1a1cc0969ca842a01d Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Wed, 14 Feb 2024 22:01:03 -0500 Subject: [PATCH 07/15] Add a clock to the 6522s, enable interrupts. --- Components/6522/6522.hpp | 2 +- Machines/Apple/AppleII/AppleII.cpp | 14 +++++++++++ Machines/Apple/AppleII/Card.hpp | 9 ++++++- Machines/Apple/AppleII/Mockingboard.hpp | 31 +++++++++++++++++++++++-- 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/Components/6522/6522.hpp b/Components/6522/6522.hpp index df8502f71..733fccd75 100644 --- a/Components/6522/6522.hpp +++ b/Components/6522/6522.hpp @@ -68,7 +68,7 @@ class IRQDelegatePortHandler: public PortHandler { /// Sets the delegate that will receive notification of changes in the interrupt line. void set_interrupt_delegate(Delegate *delegate); - /// Overrides PortHandler::set_interrupt_status, notifying the delegate if one is set. + /// Overrides @c PortHandler::set_interrupt_status, notifying the delegate if one is set. void set_interrupt_status(bool new_status); private: diff --git a/Machines/Apple/AppleII/AppleII.cpp b/Machines/Apple/AppleII/AppleII.cpp index a1d9aecdc..f2bc2e0ac 100644 --- a/Machines/Apple/AppleII/AppleII.cpp +++ b/Machines/Apple/AppleII/AppleII.cpp @@ -256,6 +256,20 @@ template pick_card_messaging_group(card); } + void card_did_change_interrupt_flags(Apple::II::Card *) final { + bool nmi = false; + bool irq = false; + + for(const auto &card: cards_) { + if(card) { + nmi |= card->nmi(); + irq |= card->irq(); + } + } + m6502_.set_nmi_line(nmi); + m6502_.set_irq_line(irq); + } + Apple::II::Mockingboard *mockingboard() { return dynamic_cast(cards_[MockingboardSlot - 1].get()); } diff --git a/Machines/Apple/AppleII/Card.hpp b/Machines/Apple/AppleII/Card.hpp index a5b6f0afa..54d43cd0c 100644 --- a/Machines/Apple/AppleII/Card.hpp +++ b/Machines/Apple/AppleII/Card.hpp @@ -54,7 +54,7 @@ class Card { This is posted only to cards that announced a select constraint. Cards with no constraints, that want to be informed of every machine cycle, will receive - a call to perform_bus_operation every cycle and should use that for time keeping. + a call to @c perform_bus_operation every cycle and should use that for time keeping. */ virtual void run_for([[maybe_unused]] Cycles cycles, [[maybe_unused]] int stretches) {} @@ -93,8 +93,15 @@ class Card { /*! Cards may supply a target for activity observation if desired. */ virtual void set_activity_observer([[maybe_unused]] Activity::Observer *observer) {} + /// @returns The current semantic NMI line output of this card. + virtual bool nmi() { return false; } + + /// @returns The current semantic IRQ line output of this card. + virtual bool irq() { return false; } + struct Delegate { virtual void card_did_change_select_constraints(Card *card) = 0; + virtual void card_did_change_interrupt_flags(Card *card) = 0; }; void set_delegate(Delegate *delegate) { delegate_ = delegate; diff --git a/Machines/Apple/AppleII/Mockingboard.hpp b/Machines/Apple/AppleII/Mockingboard.hpp index dbde129e5..b56ebecc5 100644 --- a/Machines/Apple/AppleII/Mockingboard.hpp +++ b/Machines/Apple/AppleII/Mockingboard.hpp @@ -18,7 +18,10 @@ namespace Apple::II { class Mockingboard: public Card { public: Mockingboard() : - vias_{ {handlers_[0]}, {handlers_[1]} } {} + vias_{ {handlers_[0]}, {handlers_[1]} } { + set_select_constraints(0); + handlers_[0].card = handlers_[1].card = this; + } void perform_bus_operation(Select select, bool is_read, uint16_t address, uint8_t *value) final { if(!(select & Device)) { @@ -33,8 +36,32 @@ class Mockingboard: public Card { } } + void run_for(Cycles cycles, int) final { + vias_[0].run_for(cycles); + vias_[1].run_for(cycles); + } + + bool nmi() final { + return handlers_[1].interrupt; + } + + bool irq() final { + return handlers_[0].interrupt; + } + + void did_change_interrupt_flags() { + delegate_->card_did_change_interrupt_flags(this); + } + private: - class AYVIA: public MOS::MOS6522::IRQDelegatePortHandler { + struct AYVIA: public MOS::MOS6522::PortHandler { + void set_interrupt_status(bool status) { + interrupt = status; + card->did_change_interrupt_flags(); + } + + bool interrupt; + Mockingboard *card = nullptr; }; MOS::MOS6522::MOS6522 vias_[2]; From 3ac5fdafab40e49b8918643e2258e3bcda41eb37 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Wed, 14 Feb 2024 22:15:21 -0500 Subject: [PATCH 08/15] Enables AY audio, albeit underclocked. --- Machines/Apple/AppleII/AppleII.cpp | 34 +----------- Machines/Apple/AppleII/Mockingboard.hpp | 74 +++++++++++++++++++++++-- 2 files changed, 70 insertions(+), 38 deletions(-) diff --git a/Machines/Apple/AppleII/AppleII.cpp b/Machines/Apple/AppleII/AppleII.cpp index f2bc2e0ac..d54ba8722 100644 --- a/Machines/Apple/AppleII/AppleII.cpp +++ b/Machines/Apple/AppleII/AppleII.cpp @@ -61,40 +61,10 @@ constexpr int MockingboardSlot = 4; // Conventional Mockingboard slot. // * ... and hence seven pixels per memory access window clock in high-res mode, 14 in double high-res, etc. constexpr float master_clock = 14318180.0; -class AYPair { - public: - AYPair(Concurrency::AsyncTaskQueue &queue) : - ays_{ - {GI::AY38910::Personality::AY38910, queue}, - {GI::AY38910::Personality::AY38910, queue}, - } {} - - void advance() { - ays_[0].advance(); - ays_[1].advance(); - } - - void set_sample_volume_range(std::int16_t range) { - ays_[0].set_sample_volume_range(range >> 1); - ays_[1].set_sample_volume_range(range >> 1); - } - - bool is_zero_level() const { - return ays_[0].is_zero_level() && ays_[1].is_zero_level(); - } - - Outputs::Speaker::MonoSample level() const { - return ays_[0].level() + ays_[1].level(); - } - - private: - GI::AY38910::AY38910SampleSource ays_[2]; -}; - /// Provides an AY that runs at the CPU rate divided by 4 given an input of the master clock divided by 2, /// allowing for stretched CPU clock cycles. struct StretchedAYPair: - AYPair, + Apple::II::AYPair, public Outputs::Speaker::BufferSource { using AYPair::AYPair; @@ -681,7 +651,7 @@ template if(target.has_mockingboard) { // The Mockingboard has a parasitic relationship with this class due to the way // that audio outputs are implemented in this emulator. - install_card(MockingboardSlot, new Apple::II::Mockingboard()); + install_card(MockingboardSlot, new Apple::II::Mockingboard(ays_)); } rom_ = std::move(roms.find(system)->second); diff --git a/Machines/Apple/AppleII/Mockingboard.hpp b/Machines/Apple/AppleII/Mockingboard.hpp index b56ebecc5..b3a3c1dcf 100644 --- a/Machines/Apple/AppleII/Mockingboard.hpp +++ b/Machines/Apple/AppleII/Mockingboard.hpp @@ -12,15 +12,48 @@ #include "../../../Components/6522/6522.hpp" - namespace Apple::II { +class AYPair { + public: + AYPair(Concurrency::AsyncTaskQueue &queue) : + ays_{ + {GI::AY38910::Personality::AY38910, queue}, + {GI::AY38910::Personality::AY38910, queue}, + } {} + + void advance() { + ays_[0].advance(); + ays_[1].advance(); + } + + void set_sample_volume_range(std::int16_t range) { + ays_[0].set_sample_volume_range(range >> 1); + ays_[1].set_sample_volume_range(range >> 1); + } + + bool is_zero_level() const { + return ays_[0].is_zero_level() && ays_[1].is_zero_level(); + } + + Outputs::Speaker::MonoSample level() const { + return ays_[0].level() + ays_[1].level(); + } + + GI::AY38910::AY38910SampleSource &get(int index) { + return ays_[index]; + } + + private: + GI::AY38910::AY38910SampleSource ays_[2]; +}; + class Mockingboard: public Card { public: - Mockingboard() : - vias_{ {handlers_[0]}, {handlers_[1]} } { + Mockingboard(AYPair &ays) : + vias_{ {handlers_[0]}, {handlers_[1]} }, + handlers_{ {*this, ays.get(0)}, {*this, ays.get(1)}} { set_select_constraints(0); - handlers_[0].card = handlers_[1].card = this; } void perform_bus_operation(Select select, bool is_read, uint16_t address, uint8_t *value) final { @@ -55,13 +88,42 @@ class Mockingboard: public Card { private: struct AYVIA: public MOS::MOS6522::PortHandler { + AYVIA(Mockingboard &card, GI::AY38910::AY38910SampleSource &ay) : + card(card), ay(ay) {} + void set_interrupt_status(bool status) { interrupt = status; - card->did_change_interrupt_flags(); + card.did_change_interrupt_flags(); + } + + void set_port_output(MOS::MOS6522::Port port, uint8_t value, uint8_t) { + if(port) { + using ControlLines = GI::AY38910::ControlLines; + ay.set_control_lines( + ControlLines( + ((value & 1) ? ControlLines::BC1 : 0) | + ((value & 2) ? ControlLines::BDIR : 0) | + ((value & 4) ? ControlLines::BC2 : 0) + ) + ); + + // TODO: all lines disabled sees to map to reset? Possibly? + // Cf. https://gswv.apple2.org.za/a2zine/Docs/Mockingboard_MiniManual.html + } else { + ay.set_data_input(value); + } + } + + uint8_t get_port_input(MOS::MOS6522::Port port) { + if(!port) { + return ay.get_data_output(); + } + return 0xff; } bool interrupt; - Mockingboard *card = nullptr; + Mockingboard &card; + GI::AY38910::AY38910SampleSource &ay; }; MOS::MOS6522::MOS6522 vias_[2]; From 0103761b7bfedbe4a4f51426c1dce7e0fe7e25bc Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Wed, 14 Feb 2024 22:16:37 -0500 Subject: [PATCH 09/15] Corrects AY audio tone. --- Machines/Apple/AppleII/AppleII.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Machines/Apple/AppleII/AppleII.cpp b/Machines/Apple/AppleII/AppleII.cpp index d54ba8722..c9ee8a097 100644 --- a/Machines/Apple/AppleII/AppleII.cpp +++ b/Machines/Apple/AppleII/AppleII.cpp @@ -147,7 +147,7 @@ template void update_video() { video_.run_for(cycles_since_video_update_.flush()); } - static constexpr int audio_divider = 8; + static constexpr int audio_divider = has_mockingboard ? 1 : 8; void update_audio() { speaker_.run_for(audio_queue_, cycles_since_audio_update_.divide(Cycles(audio_divider))); } From be11f31d5dd194a1cd0dc3dfa6a17a9bc1e9a46c Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Wed, 14 Feb 2024 22:22:42 -0500 Subject: [PATCH 10/15] Support reset. --- Components/AY38910/AY38910.cpp | 13 +++++++++++++ Components/AY38910/AY38910.hpp | 13 ++++++++----- Machines/Apple/AppleII/Mockingboard.hpp | 5 +++-- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Components/AY38910/AY38910.cpp b/Components/AY38910/AY38910.cpp index c32df440e..0984b94ac 100644 --- a/Components/AY38910/AY38910.cpp +++ b/Components/AY38910/AY38910.cpp @@ -376,6 +376,19 @@ void AY38910SampleSource::set_control_lines(ControlLines control_line update_bus(); } +template +void AY38910SampleSource::reset() { + // TODO: the below is a guess. Look up real answers. + + selected_register_ = 0; + std::fill(registers_, registers_ + 16, 0); + + task_queue_.enqueue([&] { + std::fill(output_registers_, output_registers_ + 16, 0); + evaluate_output_volume(); + }); +} + template void AY38910SampleSource::update_bus() { // Assume no output, unless this turns out to be a read. diff --git a/Components/AY38910/AY38910.hpp b/Components/AY38910/AY38910.hpp index acdc945b7..755861dcf 100644 --- a/Components/AY38910/AY38910.hpp +++ b/Components/AY38910/AY38910.hpp @@ -81,6 +81,9 @@ template class AY38910SampleSource { /// Sets the current control line state, as a bit field. void set_control_lines(ControlLines control_lines); + /// Strobes the reset line. + void reset(); + /*! Gets the value that would appear on the requested interface port if it were in output mode. @parameter port_b @c true to get the value for Port B, @c false to get the value for Port A. @@ -116,12 +119,12 @@ template class AY38910SampleSource { Concurrency::AsyncTaskQueue &task_queue_; int selected_register_ = 0; - uint8_t registers_[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - uint8_t output_registers_[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + uint8_t registers_[16]{}; + uint8_t output_registers_[16]{}; - int tone_periods_[3] = {0, 0, 0}; - int tone_counters_[3] = {0, 0, 0}; - int tone_outputs_[3] = {0, 0, 0}; + int tone_periods_[3]{}; + int tone_counters_[3]{}; + int tone_outputs_[3]{}; int noise_period_ = 0; int noise_counter_ = 0; diff --git a/Machines/Apple/AppleII/Mockingboard.hpp b/Machines/Apple/AppleII/Mockingboard.hpp index b3a3c1dcf..afb7555ce 100644 --- a/Machines/Apple/AppleII/Mockingboard.hpp +++ b/Machines/Apple/AppleII/Mockingboard.hpp @@ -107,8 +107,9 @@ class Mockingboard: public Card { ) ); - // TODO: all lines disabled sees to map to reset? Possibly? - // Cf. https://gswv.apple2.org.za/a2zine/Docs/Mockingboard_MiniManual.html + if(!value) { + ay.reset(); + } } else { ay.set_data_input(value); } From 809bc9d6a89b67acf9e38f03a7bcec9ff72d6299 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Wed, 14 Feb 2024 22:46:57 -0500 Subject: [PATCH 11/15] Add TODO. --- Machines/Apple/AppleII/Card.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Machines/Apple/AppleII/Card.hpp b/Machines/Apple/AppleII/Card.hpp index 54d43cd0c..22c4411c5 100644 --- a/Machines/Apple/AppleII/Card.hpp +++ b/Machines/Apple/AppleII/Card.hpp @@ -49,6 +49,10 @@ class Card { // by an access to $cfff. }; + // TODO: I think IO and Device are the wrong way around above. + // See https://retrocomputing.stackexchange.com/questions/5730/how-did-the-address-decode-for-apple-ii-expansion-cards-work + // and/or https://gglabs.us/sites/gglabs.us/files/a2scsi-A00_sch.pdf + /*! Advances time by @c cycles, of which @c stretches were stretched. From cb22278c7facb95134d2e3e2288c34a723ee5082 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 15 Feb 2024 08:54:52 -0500 Subject: [PATCH 12/15] Switch meaning of bit 2. --- Components/AY38910/AY38910.cpp | 11 +++++++++++ Components/AY38910/AY38910.hpp | 5 +++++ Machines/Apple/AppleII/Mockingboard.hpp | 6 ++---- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Components/AY38910/AY38910.cpp b/Components/AY38910/AY38910.cpp index 0984b94ac..625ffb6d0 100644 --- a/Components/AY38910/AY38910.cpp +++ b/Components/AY38910/AY38910.cpp @@ -376,6 +376,17 @@ void AY38910SampleSource::set_control_lines(ControlLines control_line update_bus(); } +template +void AY38910SampleSource::set_reset(bool active) { + if(active == reset_) return; + reset_ = active; + + // Reset upon the leading edge; TODO: is this right? + if(reset_) { + reset(); + } +} + template void AY38910SampleSource::reset() { // TODO: the below is a guess. Look up real answers. diff --git a/Components/AY38910/AY38910.hpp b/Components/AY38910/AY38910.hpp index 755861dcf..afb412e01 100644 --- a/Components/AY38910/AY38910.hpp +++ b/Components/AY38910/AY38910.hpp @@ -84,6 +84,9 @@ template class AY38910SampleSource { /// Strobes the reset line. void reset(); + /// Sets the current value of the reset line. + void set_reset(bool reset); + /*! Gets the value that would appear on the requested interface port if it were in output mode. @parameter port_b @c true to get the value for Port B, @c false to get the value for Port A. @@ -118,6 +121,8 @@ template class AY38910SampleSource { private: Concurrency::AsyncTaskQueue &task_queue_; + bool reset_ = false; + int selected_register_ = 0; uint8_t registers_[16]{}; uint8_t output_registers_[16]{}; diff --git a/Machines/Apple/AppleII/Mockingboard.hpp b/Machines/Apple/AppleII/Mockingboard.hpp index afb7555ce..b068b136d 100644 --- a/Machines/Apple/AppleII/Mockingboard.hpp +++ b/Machines/Apple/AppleII/Mockingboard.hpp @@ -103,13 +103,11 @@ class Mockingboard: public Card { ControlLines( ((value & 1) ? ControlLines::BC1 : 0) | ((value & 2) ? ControlLines::BDIR : 0) | - ((value & 4) ? ControlLines::BC2 : 0) + ControlLines::BC2 ) ); - if(!value) { - ay.reset(); - } + ay.set_reset(!(value & 4)); } else { ay.set_data_input(value); } From 1c8261dc097cd1431cb1a8c02c6fba823fc1cf37 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 15 Feb 2024 09:10:19 -0500 Subject: [PATCH 13/15] Add Mockingboard to macOS UI. --- .../Machine/StaticAnalyser/CSStaticAnalyser.h | 2 +- .../StaticAnalyser/CSStaticAnalyser.mm | 3 +- .../Base.lproj/MachinePicker.xib | 82 +++++++++++-------- .../MachinePicker/MachinePicker.swift | 5 +- 4 files changed, 53 insertions(+), 39 deletions(-) diff --git a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h index 1df0c77b7..e7f863ba9 100644 --- a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h +++ b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h @@ -141,7 +141,7 @@ typedef int Kilobytes; - (instancetype)initWithAmigaModel:(CSMachineAmigaModel)model chipMemorySize:(Kilobytes)chipMemorySize fastMemorySize:(Kilobytes)fastMemorySize; - (instancetype)initWithAmstradCPCModel:(CSMachineCPCModel)model; -- (instancetype)initWithAppleIIModel:(CSMachineAppleIIModel)model diskController:(CSMachineAppleIIDiskController)diskController; +- (instancetype)initWithAppleIIModel:(CSMachineAppleIIModel)model diskController:(CSMachineAppleIIDiskController)diskController hasMockingboard:(BOOL)hasMockingboard; - (instancetype)initWithAppleIIgsModel:(CSMachineAppleIIgsModel)model memorySize:(Kilobytes)memorySize; - (instancetype)initWithAtariSTModel:(CSMachineAtariSTModel)model memorySize:(Kilobytes)memorySize; - (instancetype)initWithElectronDFS:(BOOL)dfs adfs:(BOOL)adfs ap6:(BOOL)ap6 sidewaysRAM:(BOOL)sidewaysRAM; diff --git a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm index 9bdcc1f9e..f825e844f 100644 --- a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm +++ b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm @@ -93,7 +93,7 @@ return self; } -- (instancetype)initWithAppleIIModel:(CSMachineAppleIIModel)model diskController:(CSMachineAppleIIDiskController)diskController { +- (instancetype)initWithAppleIIModel:(CSMachineAppleIIModel)model diskController:(CSMachineAppleIIDiskController)diskController hasMockingboard:(BOOL)hasMockingboard { self = [super init]; if(self) { using Target = Analyser::Static::AppleII::Target; @@ -110,6 +110,7 @@ case CSMachineAppleIIDiskControllerSixteenSector: target->disk_controller = Target::DiskController::SixteenSector; break; case CSMachineAppleIIDiskControllerThirteenSector: target->disk_controller = Target::DiskController::ThirteenSector; break; } + target->has_mockingboard = hasMockingboard; _targets.push_back(std::move(target)); } return self; diff --git a/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib b/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib index 7b53dd6f6..b445ea9eb 100644 --- a/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib +++ b/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib @@ -1,8 +1,8 @@ - + - + @@ -18,7 +18,7 @@ - + @@ -49,7 +49,7 @@ Gw - + @@ -104,7 +104,7 @@ Gw - + @@ -135,7 +135,7 @@ Gw - + @@ -143,7 +143,7 @@ Gw - + @@ -202,7 +202,7 @@ Gw - + @@ -223,19 +223,19 @@ Gw - + - - + + - - + + @@ -243,7 +243,7 @@ Gw - + @@ -258,7 +258,7 @@ Gw - + @@ -271,16 +271,25 @@ Gw + - + + + @@ -292,7 +301,7 @@ Gw - + @@ -300,7 +309,7 @@ Gw - + @@ -357,7 +366,7 @@ Gw - + @@ -515,7 +524,7 @@ Gw - + @@ -523,7 +532,7 @@ Gw - + @@ -531,7 +540,7 @@ Gw - + @@ -539,7 +548,7 @@ Gw - + @@ -547,7 +556,7 @@ Gw - + @@ -593,7 +602,7 @@ Gw - + @@ -653,7 +662,7 @@ Gw - + @@ -661,7 +670,7 @@ Gw - + @@ -716,7 +725,7 @@ Gw - + @@ -754,7 +763,7 @@ Gw - + @@ -796,7 +805,7 @@ Gw - + @@ -804,7 +813,7 @@ Gw - + @@ -876,7 +885,7 @@ Gw - + @@ -884,7 +893,7 @@ Gw - + @@ -937,7 +946,7 @@ Gw - + @@ -985,7 +994,7 @@ Gw - + @@ -1026,7 +1035,7 @@ Gw - + @@ -1075,6 +1084,7 @@ Gw + diff --git a/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift b/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift index d9afc3431..a9217dba3 100644 --- a/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift +++ b/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift @@ -26,6 +26,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { // MARK: - Apple II properties @IBOutlet var appleIIModelButton: NSPopUpButton! @IBOutlet var appleIIDiskControllerButton: NSPopUpButton! + @IBOutlet var appleIIMockingboardButton: NSButton! // MARK: - Apple IIgs properties @IBOutlet var appleIIgsModelButton: NSPopUpButton! @@ -117,6 +118,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { // Apple II settings appleIIModelButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.appleIIModel")) appleIIDiskControllerButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.appleIIDiskController")) + appleIIMockingboardButton.state = standardUserDefaults.bool(forKey: "new.appleIIMockingboard") ? .on : .off // Apple IIgs settings appleIIgsModelButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.appleIIgsModel")) @@ -187,6 +189,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { // Apple II settings standardUserDefaults.set(appleIIModelButton.selectedTag(), forKey: "new.appleIIModel") standardUserDefaults.set(appleIIDiskControllerButton.selectedTag(), forKey: "new.appleIIDiskController") + standardUserDefaults.set(appleIIMockingboardButton.state == .on, forKey: "new.appleIIMockingboard") // Apple IIgs settings standardUserDefaults.set(appleIIgsModelButton.selectedTag(), forKey: "new.appleIIgsModel") @@ -292,7 +295,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { default: diskController = .none } - return CSStaticAnalyser(appleIIModel: model, diskController: diskController) + return CSStaticAnalyser(appleIIModel: model, diskController: diskController, hasMockingboard: appleIIMockingboardButton.state == .on) case "appleiigs": var model: CSMachineAppleIIgsModel = .ROM00 From e1fdda928adb2087340f36d75896d4d6252f7d8f Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 15 Feb 2024 09:13:17 -0500 Subject: [PATCH 14/15] Add Mockingboard to Qt UI. --- OSBindings/Qt/mainwindow.cpp | 2 ++ OSBindings/Qt/mainwindow.ui | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/OSBindings/Qt/mainwindow.cpp b/OSBindings/Qt/mainwindow.cpp index 37080d954..e21fc48ac 100644 --- a/OSBindings/Qt/mainwindow.cpp +++ b/OSBindings/Qt/mainwindow.cpp @@ -1042,6 +1042,8 @@ void MainWindow::start_appleII() { case 2: target->disk_controller = Target::DiskController::None; break; } + target->has_mockingboard = ui->appleIIMockingboardCheckBox->isChecked(); + launchTarget(std::move(target)); } diff --git a/OSBindings/Qt/mainwindow.ui b/OSBindings/Qt/mainwindow.ui index eac216907..82ad39f93 100644 --- a/OSBindings/Qt/mainwindow.ui +++ b/OSBindings/Qt/mainwindow.ui @@ -202,6 +202,13 @@ + + + + Has Mockingboard + + + From 51de1892c0f6710933826dfe5f6ff84e27ce3e18 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 15 Feb 2024 09:42:33 -0500 Subject: [PATCH 15/15] With minor infrastructure fixes, switch Mockingboard to stereo. --- Machines/Apple/AppleII/AppleII.cpp | 10 ++--- Machines/Apple/AppleII/Mockingboard.hpp | 11 ++++-- .../Speaker/Implementation/CompoundSource.hpp | 37 +++++++++---------- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/Machines/Apple/AppleII/AppleII.cpp b/Machines/Apple/AppleII/AppleII.cpp index c9ee8a097..ff9e19f67 100644 --- a/Machines/Apple/AppleII/AppleII.cpp +++ b/Machines/Apple/AppleII/AppleII.cpp @@ -65,12 +65,12 @@ constexpr float master_clock = 14318180.0; /// allowing for stretched CPU clock cycles. struct StretchedAYPair: Apple::II::AYPair, - public Outputs::Speaker::BufferSource { + public Outputs::Speaker::BufferSource { using AYPair::AYPair; template - void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) { + void apply_samples(std::size_t number_of_samples, Outputs::Speaker::StereoSample *target) { // (1) take 64 windows of 7 input cycles followed by one window of 8 input cycles; // (2) after each four windows, advance the underlying AY. @@ -168,10 +168,10 @@ template Audio::Toggle audio_toggle_; StretchedAYPair ays_; using SourceT = - std::conditional_t, Audio::Toggle>; + std::conditional_t, Audio::Toggle>; using LowpassT = Outputs::Speaker::PullLowpass; - Outputs::Speaker::CompoundSource mixer_; + Outputs::Speaker::CompoundSource mixer_; Outputs::Speaker::PullLowpass speaker_; Cycles cycles_since_audio_update_; @@ -570,7 +570,7 @@ template video_(video_bus_handler_), audio_toggle_(audio_queue_), ays_(audio_queue_), - mixer_(audio_toggle_, ays_), + mixer_(ays_, audio_toggle_), speaker_(lowpass_source()), language_card_(*this), auxiliary_switches_(*this), diff --git a/Machines/Apple/AppleII/Mockingboard.hpp b/Machines/Apple/AppleII/Mockingboard.hpp index b068b136d..b7d98544c 100644 --- a/Machines/Apple/AppleII/Mockingboard.hpp +++ b/Machines/Apple/AppleII/Mockingboard.hpp @@ -28,16 +28,19 @@ class AYPair { } void set_sample_volume_range(std::int16_t range) { - ays_[0].set_sample_volume_range(range >> 1); - ays_[1].set_sample_volume_range(range >> 1); + ays_[0].set_sample_volume_range(range); + ays_[1].set_sample_volume_range(range); } bool is_zero_level() const { return ays_[0].is_zero_level() && ays_[1].is_zero_level(); } - Outputs::Speaker::MonoSample level() const { - return ays_[0].level() + ays_[1].level(); + Outputs::Speaker::StereoSample level() const { + Outputs::Speaker::StereoSample level; + level.left = ays_[0].level(); + level.right = ays_[1].level(); + return level; } GI::AY38910::AY38910SampleSource &get(int index) { diff --git a/Outputs/Speaker/Implementation/CompoundSource.hpp b/Outputs/Speaker/Implementation/CompoundSource.hpp index bdfe93545..2cafa10c7 100644 --- a/Outputs/Speaker/Implementation/CompoundSource.hpp +++ b/Outputs/Speaker/Implementation/CompoundSource.hpp @@ -77,29 +77,28 @@ template class CompoundSource: // Map up and return. for(std::size_t c = 0; c < number_of_samples; c++) { - Outputs::Speaker::apply(target[c].left, StereoSample(conversion_source_[c])); + Outputs::Speaker::apply(target[c], StereoSample(conversion_source_[c])); } - return; - } + } else { + constexpr bool is_final_source = sizeof...(R) == 0; - constexpr bool is_final_source = sizeof...(R) == 0; - - // Get the rest of the output, if any. - if constexpr (!is_final_source) { - next_source_.template apply_samples(number_of_samples, target); - } - - if(source_.is_zero_level()) { - // This component is currently outputting silence; therefore don't add anything to the output - // audio. Zero fill only if this is the final source (as everything above will be additive). - if constexpr (is_final_source) { - Outputs::Speaker::fill(target, target + number_of_samples, typename SampleT::type()); + // Get the rest of the output, if any. + if constexpr (!is_final_source) { + next_source_.template apply_samples(number_of_samples, target); } - return; - } - // Add in this component's output. - source_.template apply_samples(number_of_samples, target); + if(source_.is_zero_level()) { + // This component is currently outputting silence; therefore don't add anything to the output + // audio. Zero fill only if this is the final source (as everything above will be additive). + if constexpr (is_final_source) { + Outputs::Speaker::fill(target, target + number_of_samples, typename SampleT::type()); + } + return; + } + + // Add in this component's output. + source_.template apply_samples(number_of_samples, target); + } } void set_scaled_volume_range(int16_t range, double *volumes, double scale) {