diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 891f13c8a..45794c931 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,11 +1,11 @@ name: Build on: [pull_request] jobs: - build-mac: - name: Mac UI on ${{ matrix.os }} + build-mac-xcodebuild: + name: Mac UI / xcodebuild / ${{ matrix.os }} strategy: matrix: - os: [macos-latest] + os: [macos-11, macos-12, macos-13, macos-14] runs-on: ${{ matrix.os }} steps: - name: Checkout @@ -13,13 +13,49 @@ jobs: - name: Make working-directory: OSBindings/Mac run: xcodebuild CODE_SIGN_IDENTITY=- - build-sdl: - name: SDL UI on ${{ matrix.os }} + build-sdl-cmake: + name: SDL UI / cmake / ${{ matrix.os }} strategy: matrix: os: [macos-latest, ubuntu-latest] runs-on: ${{ matrix.os }} steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install dependencies + shell: bash + run: | + case $RUNNER_OS in + Linux) + sudo apt-get --allow-releaseinfo-change update + sudo apt-get --fix-missing install cmake gcc-10 libsdl2-dev + ;; + macOS) + brew install cmake sdl2 + ;; + esac + - name: Make + shell: bash + run: | + case $RUNNER_OS in + Linux) + jobs=$(nproc --all) + ;; + macOS) + jobs=$(sysctl -n hw.activecpu) + ;; + *) + jobs=1 + esac + cmake -S. -Bbuild -DCLK_UI=SDL -DCMAKE_BUILD_TYPE=Release + cmake --build build -v -j"$jobs" + build-sdl-scons: + name: SDL UI / scons / ${{ matrix.os }} + strategy: + matrix: + os: [macos-11, macos-12, macos-13, macos-14, ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: - name: Checkout uses: actions/checkout@v4 - name: Install dependencies diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 000000000..8e7255fed --- /dev/null +++ b/BUILD.md @@ -0,0 +1,99 @@ +![Clock Signal Application Icon](READMEImages/Icon.png) +# Building Clock Signal + +Clock Signal is available as [a macOS native application using +Metal](#macos-app) or as [a cross-platform command-line-driven SDL executable +using OpenGL](#sdl-app). + +## macOS app + +The macOS native application requires a Metal-capable Mac running macOS 10.13 or +later and has no prerequisites beyond the normal system libraries. It can be +built [using Xcode](#building-the-macos-app-using-xcode) or on the command line +[using `xcodebuild`](#building-the-macos-app-using-xcodebuild). + +Machine ROMs are intended to be built into the application bundle; populate the +dummy folders below ROMImages before building. + +The Xcode project is configured to sign the application using the developer's +certificate, but if you are not the developer then you will get a "No signing +certificate" error. To avoid this, you'll specify that you want to sign the +application to run locally. + +### Building the macOS app using Xcode + +Open the Clock Signal Xcode project in OSBindings/Mac. + +To avoid signing errors, edit the project, select the Signing & Capabilities +tab, and change the Signing Certificate drop-down menu from "Development" to +"Sign to Run Locally". + +To avoid crashes when running Clock Signal via Xcode on older Macs due to +"unrecognized selector sent to instance" errors, edit the scheme, and in the Run +section, scroll down to the Metal heading and uncheck the "API Validation" +checkbox. + +To build, choose "Build" from Xcode's Product menu or press +Command + B. + +To build and run, choose "Run" from the Product menu or press +Command + R. + +To see the folder where the Clock Signal application was built, choose "Show +Build Folder in Finder" from the Product menu. Look in the "Products" folder for +a folder named after the configuration (e.g. "Debug" or "Release"). + +### Building the macOS app using `xcodebuild` + +To build, change to the OSBindings/Mac directory in the Terminal, then run +`xcodebuild`, specifying `-` as the code sign identity to sign the application +to run locally to avoid signing errors: + + cd OSBindings/Mac + xcodebuild CODE_SIGN_IDENTITY=- + +`xcodebuild` will create a "build" folder in this directory which is where you +can find the Clock Signal application after it's compiled, in a directory named +after the configuration (e.g. "Debug" or "Release"). + +## SDL app + +The SDL app can be built on Linux, BSD, macOS, and other Unix-like operating +systems. Prerequisites are SDL 2, ZLib and OpenGL (or Mesa). OpenGL 3.2 or +better is required at runtime. It can be built [using +SCons](#building-the-sdl-app-using-scons). + +### Building the SDL app using SCons + +To build, change to the OSBindings/SDL directory and run `scons`. You can add a +`-j` flag to build in parallel. For example, if you have 8 processor cores: + + cd OSBindings/SDL + scons -j8 + +The `clksignal` executable will be created in this directory. You can run it +from here or install it by copying it where you want it, for example: + + cp clksignal /usr/local/bin + +To start an emulator with a particular disk image `file`, if you've installed +`clksignal` to a directory in your `PATH`, run: + + clksignal file + +Or if you're running it from the current directory: + + ./clksignal file + +Other options are availble. Run `clksignal` or `./clksignal` with no arguments +to learn more. + +Setting up `clksignal` as the associated program for supported file types in +your favoured filesystem browser is recommended; it has no file navigation +abilities of its own. + +Some emulated systems require the provision of original machine ROMs. These are +not included and may be located in either /usr/local/share/CLK/ or +/usr/share/CLK/. You will be prompted for them if they are found to be missing. +The structure should mirror that under OSBindings in the source archive; see the +readme.txt in each folder to determine the proper files and names ahead of time. diff --git a/BUILD.txt b/BUILD.txt deleted file mode 100644 index cc2bcf60e..000000000 --- a/BUILD.txt +++ /dev/null @@ -1,30 +0,0 @@ -Linux, BSD -========== - -Prerequisites are SDL 2, ZLib and OpenGL (or Mesa), and SCons for the provided build script. OpenGL 3.2 or better is required at runtime. - -Build: - - cd OSBindings/SDL - scons - -Optionally: - - cp clksignal /usr/bin - -To launch: - - clksignal file - -Setting up clksignal as the associated program for supported file types in your favoured filesystem browser is recommended; it has no file navigation abilities of its own. - -Some emulated systems require the provision of original machine ROMs. These are not included and may be located in either /usr/local/share/CLK/ or /usr/share/CLK/. You will be prompted for them if they are found to be missing. The structure should mirror that under OSBindings in the source archive; see the readme.txt in each folder to determine the proper files and names ahead of time. - -macOS -===== - -There are no prerequisites beyond the normal system libraries; the macOS build is a native Cocoa application. - -Build: open the Xcode project in OSBindings/Mac and press command+b. - -Machine ROMs are intended to be built into the application bundle; populate the dummy folders below ROMImages before building. diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 000000000..975729ae5 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,70 @@ +cmake_minimum_required(VERSION 3.19 FATAL_ERROR) + +project(CLK + LANGUAGES CXX + VERSION 24.01.22 +) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +set(CLK_UIS "SDL") +#list(PREPEND CLK_UIS "QT") +#if(APPLE) +# list(PREPEND CLK_UIS "MAC") +# set(CLK_DEFAULT_UI "MAC") +#else() + set(CLK_DEFAULT_UI "SDL") +#endif() + +set(CLK_UI ${CLK_DEFAULT_UI} CACHE STRING "User interface") +set_property(CACHE CLK_UI PROPERTY STRINGS ${CLK_UIS}) + +if(NOT CLK_UI IN_LIST CLK_UIS) + list(JOIN CLK_UIS ", " CLK_UIS_PRETTY) + message(FATAL_ERROR "Invalid value for 'CLK_UI'; must be one of ${CLK_UIS_PRETTY}") +endif() + +message(STATUS "Configuring for ${CLK_UI} UI") + +list(PREPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +include("CLK_SOURCES") + +add_executable(clksignal ${CLK_SOURCES}) + +if(MSVC) + target_compile_options(clksignal PRIVATE /W4) +else() + # TODO: Add -Wpedandic. + target_compile_options(clksignal PRIVATE -Wall -Wextra) +endif() + +find_package(ZLIB REQUIRED) +target_link_libraries(clksignal PRIVATE ZLIB::ZLIB) + +if(CLK_UI STREQUAL "MAC") + enable_language(OBJC OBJCXX SWIFT) + # TODO: Build the Mac version. +else() + find_package(OpenGL REQUIRED) + target_link_libraries(clksignal PRIVATE OpenGL::GL) + if(APPLE) + target_compile_definitions(clksignal PRIVATE "GL_SILENCE_DEPRECATION" "IGNORE_APPLE") + endif() +endif() + +if(CLK_UI STREQUAL "QT") + # TODO: Build the Qt version. +elseif(APPLE) + set(BLA_VENDOR Apple) + find_package(BLAS REQUIRED) + target_link_libraries(clksignal PRIVATE BLAS::BLAS) +endif() + +if(CLK_UI STREQUAL "SDL") + find_package(SDL2 REQUIRED CONFIG REQUIRED COMPONENTS SDL2) + target_link_libraries(clksignal PRIVATE SDL2::SDL2) +endif() + +# TODO: Investigate building on Windows. diff --git a/Components/6560/6560.cpp b/Components/6560/6560.cpp index 3ca2b145f..51d053994 100644 --- a/Components/6560/6560.cpp +++ b/Components/6560/6560.cpp @@ -19,6 +19,7 @@ AudioGenerator::AudioGenerator(Concurrency::AsyncTaskQueue &audio_queue) void AudioGenerator::set_volume(uint8_t volume) { audio_queue_.enqueue([this, volume]() { volume_ = int16_t(volume) * range_multiplier_; + dc_offset_ = volume_ >> 4; }); } @@ -105,7 +106,8 @@ static uint8_t noise_pattern[] = { // testing against 0x80. The effect should be the same: loading with 0x7f means an output update every cycle, loading with 0x7e // means every second cycle, etc. -void AudioGenerator::get_samples(std::size_t number_of_samples, int16_t *target) { +template +void AudioGenerator::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) { for(unsigned int c = 0; c < number_of_samples; ++c) { update(0, 2, shift); update(1, 1, shift); @@ -114,23 +116,22 @@ void AudioGenerator::get_samples(std::size_t number_of_samples, int16_t *target) // this sums the output of all three sounds channels plus a DC offset for volume; // TODO: what's the real ratio of this stuff? - target[c] = int16_t( + const int16_t sample = (shift_registers_[0]&1) + (shift_registers_[1]&1) + (shift_registers_[2]&1) + - ((noise_pattern[shift_registers_[3] >> 3] >> (shift_registers_[3]&7))&(control_registers_[3] >> 7)&1) - ) * volume_ + (volume_ >> 4); - } -} + ((noise_pattern[shift_registers_[3] >> 3] >> (shift_registers_[3]&7))&(control_registers_[3] >> 7)&1); -void AudioGenerator::skip_samples(std::size_t number_of_samples) { - for(unsigned int c = 0; c < number_of_samples; ++c) { - update(0, 2, shift); - update(1, 1, shift); - update(2, 0, shift); - update(3, 1, increment); + Outputs::Speaker::apply( + target[c], + Outputs::Speaker::MonoSample( + sample * volume_ + dc_offset_ + )); } } +template void AudioGenerator::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); +template void AudioGenerator::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); +template void AudioGenerator::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); void AudioGenerator::set_sample_volume_range(std::int16_t range) { range_multiplier_ = int16_t(range / 64); diff --git a/Components/6560/6560.hpp b/Components/6560/6560.hpp index f8c570924..e87580fad 100644 --- a/Components/6560/6560.hpp +++ b/Components/6560/6560.hpp @@ -12,12 +12,12 @@ #include "../../Concurrency/AsyncTaskQueue.hpp" #include "../../Outputs/CRT/CRT.hpp" #include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" -#include "../../Outputs/Speaker/Implementation/SampleSource.hpp" +#include "../../Outputs/Speaker/Implementation/BufferSource.hpp" namespace MOS::MOS6560 { // audio state -class AudioGenerator: public ::Outputs::Speaker::SampleSource { +class AudioGenerator: public Outputs::Speaker::BufferSource { public: AudioGenerator(Concurrency::AsyncTaskQueue &audio_queue); @@ -25,10 +25,9 @@ class AudioGenerator: public ::Outputs::Speaker::SampleSource { void set_control(int channel, uint8_t value); // For ::SampleSource. - void get_samples(std::size_t number_of_samples, int16_t *target); - void skip_samples(std::size_t number_of_samples); + template + void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target); void set_sample_volume_range(std::int16_t range); - static constexpr bool get_is_stereo() { return false; } private: Concurrency::AsyncTaskQueue &audio_queue_; @@ -37,6 +36,7 @@ class AudioGenerator: public ::Outputs::Speaker::SampleSource { unsigned int shift_registers_[4] = {0, 0, 0, 0}; uint8_t control_registers_[4] = {0, 0, 0, 0}; int16_t volume_ = 0; + int16_t dc_offset_ = 0; int16_t range_multiplier_ = 1; }; diff --git a/Components/AY38910/AY38910.cpp b/Components/AY38910/AY38910.cpp index f02048469..c32df440e 100644 --- a/Components/AY38910/AY38910.cpp +++ b/Components/AY38910/AY38910.cpp @@ -10,17 +10,24 @@ #include "AY38910.hpp" -//namespace GI { -//namespace AY38910 { - using namespace GI::AY38910; +// Note on dividers: the real AY has a built-in divider of 8 +// prior to applying its tone and noise dividers. But the YM fills the +// same total periods for noise and tone with double-precision envelopes. +// Therefore this class implements a divider of 4 and doubles the tone +// and noise periods. The envelope ticks along at the divide-by-four rate, +// but if this is an AY rather than a YM then its lowest bit is forced to 1, +// matching the YM datasheet's depiction of envelope level 31 as equal to +// programmatic volume 15, envelope level 29 as equal to programmatic 14, etc. + template -AY38910::AY38910(Personality personality, Concurrency::AsyncTaskQueue &task_queue) : task_queue_(task_queue) { +AY38910SampleSource::AY38910SampleSource(Personality personality, Concurrency::AsyncTaskQueue &task_queue) : task_queue_(task_queue) { // Don't use the low bit of the envelope position if this is an AY. envelope_position_mask_ |= personality == Personality::AY38910; - // Set up envelope lookup tables. + // Set up envelope lookup tables; these are based on 32 volume levels as used by the YM2149F. + // The AY38910 will just use only even table entries, and therefore only even volumes. for(int c = 0; c < 16; c++) { for(int p = 0; p < 64; p++) { switch(c) { @@ -74,7 +81,8 @@ AY38910::AY38910(Personality personality, Concurrency::AsyncTaskQueue set_sample_volume_range(0); } -template void AY38910::set_sample_volume_range(std::int16_t range) { +template +void AY38910SampleSource::set_sample_volume_range(std::int16_t range) { // Set up volume lookup table; the function below is based on a combination of the graph // from the YM's datasheet, showing a clear power curve, and fitting that to observed // values reported elsewhere. @@ -92,7 +100,8 @@ template void AY38910::set_sample_volume_range(std:: evaluate_output_volume(); } -template void AY38910::set_output_mixing(float a_left, float b_left, float c_left, float a_right, float b_right, float c_right) { +template +void AY38910SampleSource::set_output_mixing(float a_left, float b_left, float c_left, float a_right, float b_right, float c_right) { a_left_ = uint8_t(a_left * 255.0f); b_left_ = uint8_t(b_left * 255.0f); c_left_ = uint8_t(c_left * 255.0f); @@ -101,78 +110,50 @@ template void AY38910::set_output_mixing(float a_lef c_right_ = uint8_t(c_right * 255.0f); } -template void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) { - // Note on structure below: the real AY has a built-in divider of 8 - // prior to applying its tone and noise dividers. But the YM fills the - // same total periods for noise and tone with double-precision envelopes. - // Therefore this class implements a divider of 4 and doubles the tone - // and noise periods. The envelope ticks along at the divide-by-four rate, - // but if this is an AY rather than a YM then its lowest bit is forced to 1, - // matching the YM datasheet's depiction of envelope level 31 as equal to - // programmatic volume 15, envelope level 29 as equal to programmatic 14, etc. - - std::size_t c = 0; - while((master_divider_&3) && c < number_of_samples) { - if constexpr (is_stereo) { - reinterpret_cast(target)[c] = output_volume_; - } else { - target[c] = int16_t(output_volume_); - } - master_divider_++; - c++; - } - - while(c < number_of_samples) { -#define step_channel(c) \ - if(tone_counters_[c]) tone_counters_[c]--;\ - else {\ - tone_outputs_[c] ^= 1;\ - tone_counters_[c] = tone_periods_[c] << 1;\ - } - - // Update the tone channels. - step_channel(0); - step_channel(1); - step_channel(2); - -#undef step_channel - - // Update the noise generator. This recomputes the new bit repeatedly but harmlessly, only shifting - // it into the official 17 upon divider underflow. - if(noise_counter_) noise_counter_--; +template +void AY38910SampleSource::advance() { + const auto step_channel = [&](int c) { + if(tone_counters_[c]) --tone_counters_[c]; else { - noise_counter_ = noise_period_ << 1; // To cover the double resolution of envelopes. - noise_output_ ^= noise_shift_register_&1; - noise_shift_register_ |= ((noise_shift_register_ ^ (noise_shift_register_ >> 3))&1) << 17; - noise_shift_register_ >>= 1; + tone_outputs_[c] ^= 1; + tone_counters_[c] = tone_periods_[c] << 1; } + }; - // Update the envelope generator. Table based for pattern lookup, with a 'refill' step: a way of - // implementing non-repeating patterns by locking them to the final table position. - if(envelope_divider_) envelope_divider_--; - else { - envelope_divider_ = envelope_period_; - envelope_position_ ++; - if(envelope_position_ == 64) envelope_position_ = envelope_overflow_masks_[output_registers_[13]]; - } + // Update the tone channels. + step_channel(0); + step_channel(1); + step_channel(2); - evaluate_output_volume(); - - for(int ic = 0; ic < 4 && c < number_of_samples; ic++) { - if constexpr (is_stereo) { - reinterpret_cast(target)[c] = output_volume_; - } else { - target[c] = int16_t(output_volume_); - } - c++; - master_divider_++; - } + // Update the noise generator. This recomputes the new bit repeatedly but harmlessly, only shifting + // it into the official 17 upon divider underflow. + if(noise_counter_) noise_counter_--; + else { + noise_counter_ = noise_period_ << 1; // To cover the double resolution of envelopes. + noise_output_ ^= noise_shift_register_&1; + noise_shift_register_ |= ((noise_shift_register_ ^ (noise_shift_register_ >> 3))&1) << 17; + noise_shift_register_ >>= 1; } - master_divider_ &= 3; + // Update the envelope generator. Table based for pattern lookup, with a 'refill' step: a way of + // implementing non-repeating patterns by locking them to the final table position. + if(envelope_divider_) envelope_divider_--; + else { + envelope_divider_ = envelope_period_; + envelope_position_ ++; + if(envelope_position_ == 64) envelope_position_ = envelope_overflow_masks_[output_registers_[13]]; + } + + evaluate_output_volume(); } -template void AY38910::evaluate_output_volume() { +template +typename Outputs::Speaker::SampleT::type AY38910SampleSource::level() const { + return output_volume_; +} + +template +void AY38910SampleSource::evaluate_output_volume() { int envelope_volume = envelope_shapes_[output_registers_[13]][envelope_position_ | envelope_position_mask_]; // The output level for a channel is: @@ -214,19 +195,18 @@ template void AY38910::evaluate_output_volume() { // Mix additively, weighting if in stereo. if constexpr (is_stereo) { - int16_t *const output_volumes = reinterpret_cast(&output_volume_); - output_volumes[0] = int16_t(( + output_volume_.left = int16_t(( volumes_[volumes[0]] * channel_levels[0] * a_left_ + volumes_[volumes[1]] * channel_levels[1] * b_left_ + volumes_[volumes[2]] * channel_levels[2] * c_left_ ) >> 8); - output_volumes[1] = int16_t(( + output_volume_.right = int16_t(( volumes_[volumes[0]] * channel_levels[0] * a_right_ + volumes_[volumes[1]] * channel_levels[1] * b_right_ + volumes_[volumes[2]] * channel_levels[2] * c_right_ ) >> 8); } else { - output_volume_ = uint32_t( + output_volume_ = int16_t( volumes_[volumes[0]] * channel_levels[0] + volumes_[volumes[1]] * channel_levels[1] + volumes_[volumes[2]] * channel_levels[2] @@ -234,18 +214,21 @@ template void AY38910::evaluate_output_volume() { } } -template bool AY38910::is_zero_level() const { +template +bool AY38910SampleSource::is_zero_level() const { // Confirm that the AY is trivially at the zero level if all three volume controls are set to fixed zero. return output_registers_[0x8] == 0 && output_registers_[0x9] == 0 && output_registers_[0xa] == 0; } // MARK: - Register manipulation -template void AY38910::select_register(uint8_t r) { +template +void AY38910SampleSource::select_register(uint8_t r) { selected_register_ = r; } -template void AY38910::set_register_value(uint8_t value) { +template +void AY38910SampleSource::set_register_value(uint8_t value) { // There are only 16 registers. if(selected_register_ > 15) return; @@ -314,7 +297,8 @@ template void AY38910::set_register_value(uint8_t va if(update_port_a) set_port_output(false); } -template uint8_t AY38910::get_register_value() { +template +uint8_t AY38910SampleSource::get_register_value() { // This table ensures that bits that aren't defined within the AY are returned as 0s // when read, conforming to CPC-sourced unit tests. const uint8_t register_masks[16] = { @@ -328,24 +312,28 @@ template uint8_t AY38910::get_register_value() { // MARK: - Port querying -template uint8_t AY38910::get_port_output(bool port_b) { +template +uint8_t AY38910SampleSource::get_port_output(bool port_b) { return registers_[port_b ? 15 : 14]; } // MARK: - Bus handling -template void AY38910::set_port_handler(PortHandler *handler) { +template +void AY38910SampleSource::set_port_handler(PortHandler *handler) { port_handler_ = handler; set_port_output(true); set_port_output(false); } -template void AY38910::set_data_input(uint8_t r) { +template +void AY38910SampleSource::set_data_input(uint8_t r) { data_input_ = r; update_bus(); } -template void AY38910::set_port_output(bool port_b) { +template +void AY38910SampleSource::set_port_output(bool port_b) { // Per the data sheet: "each [IO] pin is provided with an on-chip pull-up resistor, // so that when in the "input" mode, all pins will read normally high". Therefore, // report programmer selection of input mode as creating an output of 0xff. @@ -355,7 +343,8 @@ template void AY38910::set_port_output(bool port_b) } } -template uint8_t AY38910::get_data_output() { +template +uint8_t AY38910SampleSource::get_data_output() { if(control_state_ == Read && selected_register_ >= 14 && selected_register_ < 16) { // Per http://cpctech.cpc-live.com/docs/psgnotes.htm if a port is defined as output then the // value returned to the CPU when reading it is the and of the output value and any input. @@ -371,7 +360,8 @@ template uint8_t AY38910::get_data_output() { return data_output_; } -template void AY38910::set_control_lines(ControlLines control_lines) { +template +void AY38910SampleSource::set_control_lines(ControlLines control_lines) { switch(int(control_lines)) { default: control_state_ = Inactive; break; @@ -386,7 +376,8 @@ template void AY38910::set_control_lines(ControlLine update_bus(); } -template void AY38910::update_bus() { +template +void AY38910SampleSource::update_bus() { // Assume no output, unless this turns out to be a read. data_output_ = 0xff; switch(control_state_) { @@ -398,5 +389,10 @@ template void AY38910::update_bus() { } // Ensure both mono and stereo versions of the AY are built. -template class GI::AY38910::AY38910; -template class GI::AY38910::AY38910; +template class GI::AY38910::AY38910SampleSource; +template class GI::AY38910::AY38910SampleSource; + +// Perform an explicit instantiation of the BufferSource to hope for +// appropriate inlining of advance() and level(). +template struct GI::AY38910::AY38910; +template struct GI::AY38910::AY38910; diff --git a/Components/AY38910/AY38910.hpp b/Components/AY38910/AY38910.hpp index fdbd9eaaf..0c21c1063 100644 --- a/Components/AY38910/AY38910.hpp +++ b/Components/AY38910/AY38910.hpp @@ -8,7 +8,7 @@ #pragma once -#include "../../Outputs/Speaker/Implementation/SampleSource.hpp" +#include "../../Outputs/Speaker/Implementation/BufferSource.hpp" #include "../../Concurrency/AsyncTaskQueue.hpp" #include "../../Reflection/Struct.hpp" @@ -66,10 +66,10 @@ enum class Personality { This AY has an attached mono or stereo mixer. */ -template class AY38910: public ::Outputs::Speaker::SampleSource { +template class AY38910SampleSource { public: /// Creates a new AY38910. - AY38910(Personality, Concurrency::AsyncTaskQueue &); + AY38910SampleSource(Personality, Concurrency::AsyncTaskQueue &); /// Sets the value the AY would read from its data lines if it were not outputting. void set_data_input(uint8_t r); @@ -105,11 +105,11 @@ template 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::SampleSource. - void get_samples(std::size_t number_of_samples, int16_t *target); + // Sample generation. + typename Outputs::Speaker::SampleT::type level() const; + void advance(); bool is_zero_level() const; void set_sample_volume_range(std::int16_t range); - static constexpr bool get_is_stereo() { return is_stereo; } private: Concurrency::AsyncTaskQueue &task_queue_; @@ -118,8 +118,6 @@ template class AY38910: public ::Outputs::Speaker::SampleSource 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}; - int master_divider_ = 0; - int tone_periods_[3] = {0, 0, 0}; int tone_counters_[3] = {0, 0, 0}; int tone_outputs_[3] = {0, 0, 0}; @@ -150,7 +148,7 @@ template class AY38910: public ::Outputs::Speaker::SampleSource uint8_t data_input_, data_output_; - uint32_t output_volume_; + typename Outputs::Speaker::SampleT::type output_volume_; void update_bus(); PortHandler *port_handler_ = nullptr; @@ -166,6 +164,20 @@ template class AY38910: public ::Outputs::Speaker::SampleSource friend struct State; }; +/// Defines a default AY to be the sample source with a master divider of 4; +/// real AYs have a divide-by-8 step built in but YMs apply only a divide by 4. +/// +/// The implementation of AY38910SampleSource combines those two worlds +/// by always applying a divide by four and scaling other things as appropriate. +template struct AY38910: + public AY38910SampleSource, + public Outputs::Speaker::SampleSource, stereo, 4> { + + // Use the same constructor as `AY38910SampleSource` (along with inheriting + // the rest of its interface). + using AY38910SampleSource::AY38910SampleSource; +}; + /*! Provides helper code, to provide something closer to the interface exposed by many AY-deploying machines of the era. diff --git a/Components/AudioToggle/AudioToggle.cpp b/Components/AudioToggle/AudioToggle.cpp index 7188badd9..77c329972 100644 --- a/Components/AudioToggle/AudioToggle.cpp +++ b/Components/AudioToggle/AudioToggle.cpp @@ -8,27 +8,23 @@ #include "AudioToggle.hpp" +#include + using namespace Audio; Audio::Toggle::Toggle(Concurrency::AsyncTaskQueue &audio_queue) : audio_queue_(audio_queue) {} -void Toggle::get_samples(std::size_t number_of_samples, std::int16_t *target) { - for(std::size_t sample = 0; sample < number_of_samples; ++sample) { - target[sample] = level_; - } -} - void Toggle::set_sample_volume_range(std::int16_t range) { volume_ = range; + level_ = level_active_ ? volume_ : 0; } -void Toggle::skip_samples(std::size_t) {} - void Toggle::set_output(bool enabled) { if(is_enabled_ == enabled) return; is_enabled_ = enabled; audio_queue_.enqueue([this, enabled] { + level_active_ = enabled; level_ = enabled ? volume_ : 0; }); } diff --git a/Components/AudioToggle/AudioToggle.hpp b/Components/AudioToggle/AudioToggle.hpp index 8209b4653..06312225c 100644 --- a/Components/AudioToggle/AudioToggle.hpp +++ b/Components/AudioToggle/AudioToggle.hpp @@ -8,7 +8,7 @@ #pragma once -#include "../../Outputs/Speaker/Implementation/SampleSource.hpp" +#include "../../Outputs/Speaker/Implementation/BufferSource.hpp" #include "../../Concurrency/AsyncTaskQueue.hpp" namespace Audio { @@ -16,13 +16,18 @@ namespace Audio { /*! Provides a sample source that can programmatically be set to one of two values. */ -class Toggle: public Outputs::Speaker::SampleSource { +class Toggle: public Outputs::Speaker::BufferSource { public: Toggle(Concurrency::AsyncTaskQueue &audio_queue); - void get_samples(std::size_t number_of_samples, std::int16_t *target); + template + void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) { + Outputs::Speaker::fill(target, target + number_of_samples, level_); + } void set_sample_volume_range(std::int16_t range); - void skip_samples(const std::size_t number_of_samples); + bool is_zero_level() const { + return !level_; + } void set_output(bool enabled); bool get_output() const; @@ -34,6 +39,7 @@ class Toggle: public Outputs::Speaker::SampleSource { // Accessed on the audio thread. int16_t level_ = 0, volume_ = 0; + bool level_active_ = false; }; } diff --git a/Components/KonamiSCC/KonamiSCC.cpp b/Components/KonamiSCC/KonamiSCC.cpp index 81a2af502..c4c1abc8d 100644 --- a/Components/KonamiSCC/KonamiSCC.cpp +++ b/Components/KonamiSCC/KonamiSCC.cpp @@ -19,15 +19,16 @@ bool SCC::is_zero_level() const { return !(channel_enable_ & 0x1f); } -void SCC::get_samples(std::size_t number_of_samples, std::int16_t *target) { +template +void SCC::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) { if(is_zero_level()) { - std::memset(target, 0, sizeof(std::int16_t) * number_of_samples); + Outputs::Speaker::fill(target, target + number_of_samples, Outputs::Speaker::MonoSample()); return; } std::size_t c = 0; while((master_divider_&7) && c < number_of_samples) { - target[c] = transient_output_level_; + Outputs::Speaker::apply(target[c], transient_output_level_); master_divider_++; c++; } @@ -44,12 +45,15 @@ void SCC::get_samples(std::size_t number_of_samples, std::int16_t *target) { evaluate_output_volume(); for(int ic = 0; ic < 8 && c < number_of_samples; ++ic) { - target[c] = transient_output_level_; + Outputs::Speaker::apply(target[c], transient_output_level_); c++; master_divider_++; } } } +template void SCC::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); +template void SCC::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); +template void SCC::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); void SCC::write(uint16_t address, uint8_t value) { address &= 0xff; @@ -111,5 +115,3 @@ uint8_t SCC::read(uint16_t address) { } return 0xff; } - - diff --git a/Components/KonamiSCC/KonamiSCC.hpp b/Components/KonamiSCC/KonamiSCC.hpp index 9b16e8a1d..5a379c3d0 100644 --- a/Components/KonamiSCC/KonamiSCC.hpp +++ b/Components/KonamiSCC/KonamiSCC.hpp @@ -8,7 +8,7 @@ #pragma once -#include "../../Outputs/Speaker/Implementation/SampleSource.hpp" +#include "../../Outputs/Speaker/Implementation/BufferSource.hpp" #include "../../Concurrency/AsyncTaskQueue.hpp" namespace Konami { @@ -20,7 +20,7 @@ namespace Konami { and five channels of output. The original SCC uses the same wave for channels four and five, the SCC+ supports different waves for the two channels. */ -class SCC: public ::Outputs::Speaker::SampleSource { +class SCC: public ::Outputs::Speaker::BufferSource { public: /// Creates a new SCC. SCC(Concurrency::AsyncTaskQueue &task_queue); @@ -29,9 +29,9 @@ class SCC: public ::Outputs::Speaker::SampleSource { bool is_zero_level() const; /// As per ::SampleSource; provides audio output. - void get_samples(std::size_t number_of_samples, std::int16_t *target); + template + void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target); void set_sample_volume_range(std::int16_t range); - static constexpr bool get_is_stereo() { return false; } /// Writes to the SCC. void write(uint16_t address, uint8_t value); @@ -45,7 +45,7 @@ class SCC: public ::Outputs::Speaker::SampleSource { // State from here on down is accessed ony from the audio thread. int master_divider_ = 0; std::int16_t master_volume_ = 0; - int16_t transient_output_level_ = 0; + Outputs::Speaker::MonoSample transient_output_level_ = 0; struct Channel { int period = 0; diff --git a/Components/OPx/Implementation/OPLBase.hpp b/Components/OPx/Implementation/OPLBase.hpp index a3067395c..1890646a9 100644 --- a/Components/OPx/Implementation/OPLBase.hpp +++ b/Components/OPx/Implementation/OPLBase.hpp @@ -8,12 +8,12 @@ #pragma once -#include "../../../Outputs/Speaker/Implementation/SampleSource.hpp" +#include "../../../Outputs/Speaker/Implementation/BufferSource.hpp" #include "../../../Concurrency/AsyncTaskQueue.hpp" namespace Yamaha::OPL { -template class OPLBase: public ::Outputs::Speaker::SampleSource { +template class OPLBase: public ::Outputs::Speaker::BufferSource { public: void write(uint16_t address, uint8_t value) { if(address & 1) { diff --git a/Components/OPx/OPLL.cpp b/Components/OPx/OPLL.cpp index f5d8feddd..b1bb81935 100644 --- a/Components/OPx/OPLL.cpp +++ b/Components/OPx/OPLL.cpp @@ -278,7 +278,8 @@ void OPLL::set_sample_volume_range(std::int16_t range) { total_volume_ = range; } -void OPLL::get_samples(std::size_t number_of_samples, std::int16_t *target) { +template +void OPLL::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) { // Both the OPLL and the OPL2 divide the input clock by 72 to get the base tick frequency; // unlike the OPL2 the OPLL time-divides the output for 'mixing'. @@ -289,12 +290,16 @@ void OPLL::get_samples(std::size_t number_of_samples, std::int16_t *target) { while(number_of_samples--) { if(!audio_offset_) update_all_channels(); - *target = output_levels_[audio_offset_ / channel_output_period]; + Outputs::Speaker::apply(*target, output_levels_[audio_offset_ / channel_output_period]); ++target; audio_offset_ = (audio_offset_ + 1) % update_period; } } +template void OPLL::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); +template void OPLL::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); +template void OPLL::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); + void OPLL::update_all_channels() { oscillator_.update(); diff --git a/Components/OPx/OPLL.hpp b/Components/OPx/OPLL.hpp index 36bb0b51d..bca92a969 100644 --- a/Components/OPx/OPLL.hpp +++ b/Components/OPx/OPLL.hpp @@ -19,24 +19,26 @@ namespace Yamaha::OPL { -class OPLL: public OPLBase { +class OPLL: public OPLBase { public: /// Creates a new OPLL or VRC7. OPLL(Concurrency::AsyncTaskQueue &task_queue, int audio_divider = 1, bool is_vrc7 = false); /// As per ::SampleSource; provides audio output. - void get_samples(std::size_t number_of_samples, std::int16_t *target); + template + void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target); void set_sample_volume_range(std::int16_t range); + bool is_zero_level() const { return false; } // TODO. // The OPLL is generally 'half' as loud as it's told to be. This won't strictly be true in // rhythm mode, but it's correct for melodic output. - double get_average_output_peak() const { return 0.5; } + double average_output_peak() const { return 0.5; } /// Reads from the OPL. uint8_t read(uint16_t address); private: - friend OPLBase; + friend OPLBase; void write_register(uint8_t address, uint8_t value); int audio_divider_ = 0; diff --git a/Components/SN76489/SN76489.cpp b/Components/SN76489/SN76489.cpp index 8705b24c7..007df3516 100644 --- a/Components/SN76489/SN76489.cpp +++ b/Components/SN76489/SN76489.cpp @@ -99,10 +99,11 @@ void SN76489::evaluate_output_volume() { ); } -void SN76489::get_samples(std::size_t number_of_samples, std::int16_t *target) { +template +void SN76489::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) { std::size_t c = 0; while((master_divider_& (master_divider_period_ - 1)) && c < number_of_samples) { - target[c] = output_volume_; + Outputs::Speaker::apply(target[c], output_volume_); master_divider_++; c++; } @@ -151,7 +152,7 @@ void SN76489::get_samples(std::size_t number_of_samples, std::int16_t *target) { evaluate_output_volume(); for(int ic = 0; ic < master_divider_period_ && c < number_of_samples; ++ic) { - target[c] = output_volume_; + Outputs::Speaker::apply(target[c], output_volume_); c++; master_divider_++; } @@ -159,3 +160,6 @@ void SN76489::get_samples(std::size_t number_of_samples, std::int16_t *target) { master_divider_ &= (master_divider_period_ - 1); } +template void SN76489::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); +template void SN76489::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); +template void SN76489::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); diff --git a/Components/SN76489/SN76489.hpp b/Components/SN76489/SN76489.hpp index d419692f8..5e79e3e83 100644 --- a/Components/SN76489/SN76489.hpp +++ b/Components/SN76489/SN76489.hpp @@ -8,12 +8,12 @@ #pragma once -#include "../../Outputs/Speaker/Implementation/SampleSource.hpp" +#include "../../Outputs/Speaker/Implementation/BufferSource.hpp" #include "../../Concurrency/AsyncTaskQueue.hpp" namespace TI { -class SN76489: public Outputs::Speaker::SampleSource { +class SN76489: public Outputs::Speaker::BufferSource { public: enum class Personality { SN76489, @@ -28,10 +28,10 @@ class SN76489: public Outputs::Speaker::SampleSource { void write(uint8_t value); // As per SampleSource. - void get_samples(std::size_t number_of_samples, std::int16_t *target); + template + 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 range); - static constexpr bool get_is_stereo() { return false; } private: int master_divider_ = 0; diff --git a/InstructionSets/README.md b/InstructionSets/README.md index fc50a84ea..a0c779083 100644 --- a/InstructionSets/README.md +++ b/InstructionSets/README.md @@ -1,8 +1,9 @@ -# Instruction Sets +# Instruction Sets Code in here provides the means to disassemble, and to execute code for certain instruction sets. It **does not seek to emulate specific processors** other than in terms of implementing their instruction sets. So: + * it doesn't involve itself in the actual bus signalling of real processors; and * instruction-level timing (e.g. total cycle counts) may be unimplemented, and is likely to be incomplete. @@ -13,6 +14,7 @@ This part of CLK is intended primarily to provide disassembly services for stati A decoder extracts fully-decoded instructions from a data stream for its associated architecture. The meaning of 'fully-decoded' is flexible but it means that a caller can easily discern at least: + * the operation in use; * its addressing mode; and * relevant registers. @@ -20,6 +22,7 @@ The meaning of 'fully-decoded' is flexible but it means that a caller can easily It may be assumed that callers will have access to the original data stream for immediate values, if it is sensible to do so. In deciding what to expose, what to store ahead of time and what to obtain just-in-time a decoder should have an eye on two principal consumers: + 1. disassemblers; and 2. instruction executors. @@ -50,6 +53,7 @@ A sample interface: std::pair decode(word_type *stream, size_t length) { ... } In this sample the returned pair provides an `int` size that is one of: + * a positive number, indicating a completed decoding that consumed that many `word_type`s; or * a negative number, indicating the [negatived] minimum number of `word_type`s that the caller should try to get hold of before calling `decode` again. @@ -58,6 +62,7 @@ A caller is permitted to react in any way it prefers to negative numbers; they'r ## Parsers A parser sits one level above a decoder; it is handed: + * a start address; * a closing bound; and * a target. @@ -65,6 +70,7 @@ A parser sits one level above a decoder; it is handed: It is responsible for parsing the instruction stream from the start address up to and not beyond the closing bound, and no further than any unconditional branches. It should post to the target: + * any instructions fully decoded; * any conditional branch destinations encountered; * any immediately-knowable accessed addresses; and @@ -75,6 +81,7 @@ So a parser has the same two primary potential recipients as a decoder: diassemb ## Executors An executor is responsible for only one thing: + * mapping from decoded instructions to objects that can perform those instructions. An executor is assumed to bundle all the things that go into instruction set execution: processor state and memory, alongside a parser. diff --git a/Machines/Amiga/Amiga.cpp b/Machines/Amiga/Amiga.cpp index acce943d3..834ec0058 100644 --- a/Machines/Amiga/Amiga.cpp +++ b/Machines/Amiga/Amiga.cpp @@ -221,7 +221,7 @@ class ConcreteMachine: // MARK: - MachineTypes::MouseMachine. Inputs::Mouse &get_mouse() final { - return chipset_.get_mouse();; + return chipset_.get_mouse(); } // MARK: - MachineTypes::JoystickMachine. diff --git a/Machines/Apple/AppleIIgs/Sound.cpp b/Machines/Apple/AppleIIgs/Sound.cpp index 0a5c78869..84b0dda5c 100644 --- a/Machines/Apple/AppleIIgs/Sound.cpp +++ b/Machines/Apple/AppleIIgs/Sound.cpp @@ -108,7 +108,7 @@ uint8_t GLU::get_data() { switch(address & 0xe0) { case 0x00: return local_.oscillators[address & 0x1f].velocity & 0xff; - case 0x20: return local_.oscillators[address & 0x1f].velocity >> 8;; + case 0x20: return local_.oscillators[address & 0x1f].velocity >> 8; case 0x40: return local_.oscillators[address & 0x1f].volume; case 0x60: return local_.oscillators[address & 0x1f].sample(local_.ram_); // i.e. look up what the sample was on demand. case 0x80: return local_.oscillators[address & 0x1f].address; @@ -159,29 +159,34 @@ void GLU::run_for(Cycles cycles) { pending_store_write_time_ += cycles.as(); } -void GLU::get_samples(std::size_t number_of_samples, std::int16_t *target) { +template +void GLU::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) { // Update remote state, generating audio. - generate_audio(number_of_samples, target); + generate_audio(number_of_samples, target); } +template void GLU::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); +template void GLU::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); +template void GLU::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); -void GLU::skip_samples(const std::size_t number_of_samples) { - // Update remote state, without generating audio. - skip_audio(remote_, number_of_samples); - // Apply any pending stores. - std::atomic_thread_fence(std::memory_order::memory_order_acquire); - const uint32_t final_time = pending_store_read_time_ + uint32_t(number_of_samples); - while(true) { - auto next_store = pending_stores_[pending_store_read_].load(std::memory_order::memory_order_acquire); - if(!next_store.enabled) break; - if(next_store.time >= final_time) break; - remote_.ram_[next_store.address] = next_store.value; - next_store.enabled = false; - pending_stores_[pending_store_read_].store(next_store, std::memory_order::memory_order_relaxed); - - pending_store_read_ = (pending_store_read_ + 1) & (StoreBufferSize - 1); - } -} +//void GLU::skip_samples(const std::size_t number_of_samples) { +// // Update remote state, without generating audio. +// skip_audio(remote_, number_of_samples); +// +// // Apply any pending stores. +// std::atomic_thread_fence(std::memory_order::memory_order_acquire); +// const uint32_t final_time = pending_store_read_time_ + uint32_t(number_of_samples); +// while(true) { +// auto next_store = pending_stores_[pending_store_read_].load(std::memory_order::memory_order_acquire); +// if(!next_store.enabled) break; +// if(next_store.time >= final_time) break; +// remote_.ram_[next_store.address] = next_store.value; +// next_store.enabled = false; +// pending_stores_[pending_store_read_].store(next_store, std::memory_order::memory_order_relaxed); +// +// pending_store_read_ = (pending_store_read_ + 1) & (StoreBufferSize - 1); +// } +//} void GLU::set_sample_volume_range(std::int16_t range) { output_range_ = range; @@ -256,7 +261,8 @@ void GLU::skip_audio(EnsoniqState &state, size_t number_of_samples) { } } -void GLU::generate_audio(size_t number_of_samples, std::int16_t *target) { +template +void GLU::generate_audio(size_t number_of_samples, Outputs::Speaker::MonoSample *target) { auto next_store = pending_stores_[pending_store_read_].load(std::memory_order::memory_order_acquire); uint8_t next_amplitude = 255; for(size_t sample = 0; sample < number_of_samples; sample++) { @@ -325,7 +331,12 @@ void GLU::generate_audio(size_t number_of_samples, std::int16_t *target) { // Maximum total output was 32 channels times a 16-bit range. Map that down. // TODO: dynamic total volume? - target[sample] = (output * output_range_) >> 20; + Outputs::Speaker::apply( + target[sample], + Outputs::Speaker::MonoSample( + (output * output_range_) >> 20 + ) + ); // Apply any RAM writes that interleave here. ++pending_store_read_time_; diff --git a/Machines/Apple/AppleIIgs/Sound.hpp b/Machines/Apple/AppleIIgs/Sound.hpp index 55c4f4c20..711873cbd 100644 --- a/Machines/Apple/AppleIIgs/Sound.hpp +++ b/Machines/Apple/AppleIIgs/Sound.hpp @@ -12,11 +12,11 @@ #include "../../../ClockReceiver/ClockReceiver.hpp" #include "../../../Concurrency/AsyncTaskQueue.hpp" -#include "../../../Outputs/Speaker/Implementation/SampleSource.hpp" +#include "../../../Outputs/Speaker/Implementation/BufferSource.hpp" namespace Apple::IIgs::Sound { -class GLU: public Outputs::Speaker::SampleSource { +class GLU: public Outputs::Speaker::BufferSource { // TODO: isn't this stereo? public: GLU(Concurrency::AsyncTaskQueue &audio_queue); @@ -34,9 +34,10 @@ class GLU: public Outputs::Speaker::SampleSource { bool get_interrupt_line(); // SampleSource. - void get_samples(std::size_t number_of_samples, std::int16_t *target); + template + void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target); void set_sample_volume_range(std::int16_t range); - void skip_samples(const std::size_t number_of_samples); + bool is_zero_level() const { return false; } // TODO. private: Concurrency::AsyncTaskQueue &audio_queue_; @@ -94,7 +95,8 @@ class GLU: public Outputs::Speaker::SampleSource { // Functions to update an EnsoniqState; these don't belong to the state itself // because they also access the pending stores (inter alia). - void generate_audio(size_t number_of_samples, std::int16_t *target); + template + void generate_audio(size_t number_of_samples, Outputs::Speaker::MonoSample *target); void skip_audio(EnsoniqState &state, size_t number_of_samples); // Audio-thread state. diff --git a/Machines/Apple/Macintosh/Audio.cpp b/Machines/Apple/Macintosh/Audio.cpp index d24497990..2eab2a710 100644 --- a/Machines/Apple/Macintosh/Audio.cpp +++ b/Machines/Apple/Macintosh/Audio.cpp @@ -8,6 +8,8 @@ #include "Audio.hpp" +#include + using namespace Apple::Macintosh; namespace { @@ -69,7 +71,8 @@ void Audio::set_volume_multiplier() { volume_multiplier_ = int16_t(output_volume_ * volume_ * enabled_mask_); } -void Audio::get_samples(std::size_t number_of_samples, int16_t *target) { +template +void Audio::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) { // TODO: the implementation below acts as if the hardware uses pulse-amplitude modulation; // in fact it uses pulse-width modulation. But the scale for pulses isn't specified, so // that's something to return to. @@ -79,11 +82,8 @@ void Audio::get_samples(std::size_t number_of_samples, int16_t *target) { const auto cycles_left_in_sample = std::min(number_of_samples, sample_length - subcycle_offset_); // Determine the output level, and output that many samples. - // (Hoping that the copiler substitutes an effective memset16-type operation here). const int16_t output_level = volume_multiplier_ * (int16_t(sample_queue_.buffer[sample_queue_.read_pointer].load(std::memory_order::memory_order_relaxed)) - 128); - for(size_t c = 0; c < cycles_left_in_sample; ++c) { - target[c] = output_level; - } + Outputs::Speaker::fill(target, target + cycles_left_in_sample, output_level); target += cycles_left_in_sample; // Advance the sample pointer. @@ -95,3 +95,6 @@ void Audio::get_samples(std::size_t number_of_samples, int16_t *target) { number_of_samples -= cycles_left_in_sample; } } +template void Audio::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); +template void Audio::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); +template void Audio::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); diff --git a/Machines/Apple/Macintosh/Audio.hpp b/Machines/Apple/Macintosh/Audio.hpp index e132015e0..1adb4b06a 100644 --- a/Machines/Apple/Macintosh/Audio.hpp +++ b/Machines/Apple/Macintosh/Audio.hpp @@ -10,7 +10,7 @@ #include "../../../Concurrency/AsyncTaskQueue.hpp" #include "../../../ClockReceiver/ClockReceiver.hpp" -#include "../../../Outputs/Speaker/Implementation/SampleSource.hpp" +#include "../../../Outputs/Speaker/Implementation/BufferSource.hpp" #include #include @@ -23,7 +23,7 @@ namespace Apple::Macintosh { Designed to be clocked at half the rate of the real hardware — i.e. a shade less than 4Mhz. */ -class Audio: public ::Outputs::Speaker::SampleSource { +class Audio: public ::Outputs::Speaker::BufferSource { public: Audio(Concurrency::AsyncTaskQueue &task_queue); @@ -50,10 +50,10 @@ class Audio: public ::Outputs::Speaker::SampleSource { void set_enabled(bool on); // to satisfy ::Outputs::Speaker (included via ::Outputs::Filter. - void get_samples(std::size_t number_of_samples, int16_t *target); + template + 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 range); - constexpr static bool get_is_stereo() { return false; } private: Concurrency::AsyncTaskQueue &task_queue_; diff --git a/Machines/Apple/Macintosh/DriveSpeedAccumulator.hpp b/Machines/Apple/Macintosh/DriveSpeedAccumulator.hpp index 981f0b923..21a28066b 100644 --- a/Machines/Apple/Macintosh/DriveSpeedAccumulator.hpp +++ b/Machines/Apple/Macintosh/DriveSpeedAccumulator.hpp @@ -28,7 +28,7 @@ class DriveSpeedAccumulator { Sets the delegate to receive drive speed changes. */ void set_delegate(Delegate *delegate) { - delegate_ = delegate;; + delegate_ = delegate; } private: diff --git a/Machines/Atari/2600/TIASound.cpp b/Machines/Atari/2600/TIASound.cpp index d109c3187..22ab1aec7 100644 --- a/Machines/Atari/2600/TIASound.cpp +++ b/Machines/Atari/2600/TIASound.cpp @@ -40,9 +40,10 @@ void Atari2600::TIASound::set_control(int channel, uint8_t control) { #define advance_poly5(c) poly5_counter_[channel] = (poly5_counter_[channel] >> 1) | (((poly5_counter_[channel] << 4) ^ (poly5_counter_[channel] << 2))&0x010) #define advance_poly9(c) poly9_counter_[channel] = (poly9_counter_[channel] >> 1) | (((poly9_counter_[channel] << 4) ^ (poly9_counter_[channel] << 8))&0x100) -void Atari2600::TIASound::get_samples(std::size_t number_of_samples, int16_t *target) { +template +void Atari2600::TIASound::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) { for(unsigned int c = 0; c < number_of_samples; c++) { - target[c] = 0; + Outputs::Speaker::MonoSample output = 0; for(int channel = 0; channel < 2; channel++) { divider_counter_[channel] ++; int divider_value = divider_counter_[channel] / (38 / CPUTicksPerAudioTick); @@ -119,10 +120,14 @@ void Atari2600::TIASound::get_samples(std::size_t number_of_samples, int16_t *ta break; } - target[c] += (volume_[channel] * per_channel_volume_ * level) >> 4; + output += (volume_[channel] * per_channel_volume_ * level) >> 4; } + Outputs::Speaker::apply(target[c], output); } } +template void Atari2600::TIASound::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); +template void Atari2600::TIASound::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); +template void Atari2600::TIASound::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); void Atari2600::TIASound::set_sample_volume_range(std::int16_t range) { per_channel_volume_ = range / 2; diff --git a/Machines/Atari/2600/TIASound.hpp b/Machines/Atari/2600/TIASound.hpp index 67976be58..a71a16e9e 100644 --- a/Machines/Atari/2600/TIASound.hpp +++ b/Machines/Atari/2600/TIASound.hpp @@ -8,7 +8,7 @@ #pragma once -#include "../../../Outputs/Speaker/Implementation/SampleSource.hpp" +#include "../../../Outputs/Speaker/Implementation/BufferSource.hpp" #include "../../../Concurrency/AsyncTaskQueue.hpp" namespace Atari2600 { @@ -17,7 +17,7 @@ namespace Atari2600 { // will give greater resolution to changes in audio state. 1, 2 and 19 are the only divisors of 38. constexpr int CPUTicksPerAudioTick = 2; -class TIASound: public Outputs::Speaker::SampleSource { +class TIASound: public Outputs::Speaker::BufferSource { public: TIASound(Concurrency::AsyncTaskQueue &audio_queue); @@ -26,9 +26,9 @@ class TIASound: public Outputs::Speaker::SampleSource { void set_control(int channel, uint8_t control); // To satisfy ::SampleSource. - void get_samples(std::size_t number_of_samples, int16_t *target); + template + void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target); void set_sample_volume_range(std::int16_t range); - static constexpr bool get_is_stereo() { return false; } private: Concurrency::AsyncTaskQueue &audio_queue_; diff --git a/Machines/Electron/SoundGenerator.cpp b/Machines/Electron/SoundGenerator.cpp index f0a58f5c5..14aa4fa22 100644 --- a/Machines/Electron/SoundGenerator.cpp +++ b/Machines/Electron/SoundGenerator.cpp @@ -19,21 +19,26 @@ void SoundGenerator::set_sample_volume_range(std::int16_t range) { volume_ = unsigned(range / 2); } -void SoundGenerator::get_samples(std::size_t number_of_samples, int16_t *target) { +template +void SoundGenerator::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) { + if constexpr (action == Outputs::Speaker::Action::Ignore) { + counter_ = (counter_ + number_of_samples) % ((divider_+1) * 2); + return; + } + if(is_enabled_) { while(number_of_samples--) { - *target = int16_t((counter_ / (divider_+1)) * volume_); + Outputs::Speaker::apply(*target, Outputs::Speaker::MonoSample((counter_ / (divider_+1)) * volume_)); target++; counter_ = (counter_ + 1) % ((divider_+1) * 2); } } else { - std::memset(target, 0, sizeof(int16_t) * number_of_samples); + Outputs::Speaker::fill(target, target + number_of_samples, Outputs::Speaker::MonoSample(0)); } } - -void SoundGenerator::skip_samples(std::size_t number_of_samples) { - counter_ = (counter_ + number_of_samples) % ((divider_+1) * 2); -} +template void SoundGenerator::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); +template void SoundGenerator::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); +template void SoundGenerator::apply_samples(std::size_t, Outputs::Speaker::MonoSample *); void SoundGenerator::set_divider(uint8_t divider) { audio_queue_.enqueue([this, divider]() { diff --git a/Machines/Electron/SoundGenerator.hpp b/Machines/Electron/SoundGenerator.hpp index 32cd04074..4699b9984 100644 --- a/Machines/Electron/SoundGenerator.hpp +++ b/Machines/Electron/SoundGenerator.hpp @@ -8,12 +8,12 @@ #pragma once -#include "../../Outputs/Speaker/Implementation/SampleSource.hpp" +#include "../../Outputs/Speaker/Implementation/BufferSource.hpp" #include "../../Concurrency/AsyncTaskQueue.hpp" namespace Electron { -class SoundGenerator: public ::Outputs::Speaker::SampleSource { +class SoundGenerator: public ::Outputs::Speaker::BufferSource { public: SoundGenerator(Concurrency::AsyncTaskQueue &audio_queue); @@ -23,11 +23,10 @@ class SoundGenerator: public ::Outputs::Speaker::SampleSource { static constexpr unsigned int clock_rate_divider = 8; - // To satisfy ::SampleSource. - void get_samples(std::size_t number_of_samples, int16_t *target); - void skip_samples(std::size_t number_of_samples); + // For BufferSource. + template + void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target); void set_sample_volume_range(std::int16_t range); - static constexpr bool get_is_stereo() { return false; } private: Concurrency::AsyncTaskQueue &audio_queue_; diff --git a/Machines/Enterprise/Dave.cpp b/Machines/Enterprise/Dave.cpp index e619f6352..5da74b673 100644 --- a/Machines/Enterprise/Dave.cpp +++ b/Machines/Enterprise/Dave.cpp @@ -99,11 +99,9 @@ void Audio::update_channel(int c) { channels_[c].output |= output; } -void Audio::get_samples(std::size_t number_of_samples, int16_t *target) { - struct Frame { - int16_t left, right; - } output_level; - Frame *target_frames = reinterpret_cast(target); +template +void Audio::apply_samples(std::size_t number_of_samples, Outputs::Speaker::StereoSample *target) { + Outputs::Speaker::StereoSample output_level; size_t c = 0; while(c < number_of_samples) { @@ -133,14 +131,11 @@ void Audio::get_samples(std::size_t number_of_samples, int16_t *target) { while(global_divider_ && c < number_of_samples) { --global_divider_; - target_frames[c] = output_level; + Outputs::Speaker::apply(target[c], output_level); ++c; } - global_divider_ = global_divider_reload_; - if(!global_divider_) { - global_divider_ = global_divider_reload_; - } + poly_state_[int(Channel::Distortion::FourBit)] = poly4_.next(); poly_state_[int(Channel::Distortion::FiveBit)] = poly5_.next(); poly_state_[int(Channel::Distortion::SevenBit)] = poly7_.next(); @@ -209,6 +204,9 @@ void Audio::get_samples(std::size_t number_of_samples, int16_t *target) { } } } +template void Audio::apply_samples(std::size_t, Outputs::Speaker::StereoSample *); +template void Audio::apply_samples(std::size_t, Outputs::Speaker::StereoSample *); +template void Audio::apply_samples(std::size_t, Outputs::Speaker::StereoSample *); // MARK: - Interrupt source diff --git a/Machines/Enterprise/Dave.hpp b/Machines/Enterprise/Dave.hpp index 21c08cfe8..7951ae1a6 100644 --- a/Machines/Enterprise/Dave.hpp +++ b/Machines/Enterprise/Dave.hpp @@ -13,7 +13,7 @@ #include "../../ClockReceiver/ClockReceiver.hpp" #include "../../Concurrency/AsyncTaskQueue.hpp" #include "../../Numeric/LFSR.hpp" -#include "../../Outputs/Speaker/Implementation/SampleSource.hpp" +#include "../../Outputs/Speaker/Implementation/BufferSource.hpp" namespace Enterprise::Dave { @@ -26,7 +26,7 @@ enum class Interrupt: uint8_t { /*! Models the audio-production subset of Dave's behaviour. */ -class Audio: public Outputs::Speaker::SampleSource { +class Audio: public Outputs::Speaker::BufferSource { public: Audio(Concurrency::AsyncTaskQueue &audio_queue); @@ -37,8 +37,8 @@ class Audio: public Outputs::Speaker::SampleSource { // MARK: - SampleSource. void set_sample_volume_range(int16_t range); - static constexpr bool get_is_stereo() { return true; } // Dave produces stereo sound. - void get_samples(std::size_t number_of_samples, int16_t *target); + template + void apply_samples(std::size_t number_of_samples, Outputs::Speaker::StereoSample *target); private: Concurrency::AsyncTaskQueue &audio_queue_; diff --git a/Machines/MSX/MSX.cpp b/Machines/MSX/MSX.cpp index 8d3ea168d..f4e02b8f2 100644 --- a/Machines/MSX/MSX.cpp +++ b/Machines/MSX/MSX.cpp @@ -41,7 +41,7 @@ #include "../../Outputs/Log.hpp" #include "../../Outputs/Speaker/Implementation/CompoundSource.hpp" #include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" -#include "../../Outputs/Speaker/Implementation/SampleSource.hpp" +#include "../../Outputs/Speaker/Implementation/BufferSource.hpp" #include "../../Configurable/StandardOptions.hpp" #include "../../ClockReceiver/ForceInline.hpp" diff --git a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp index 62406a629..c20e43fff 100644 --- a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp +++ b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp @@ -28,7 +28,7 @@ #include "../../../Outputs/Speaker/Implementation/CompoundSource.hpp" #include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" -#include "../../../Outputs/Speaker/Implementation/SampleSource.hpp" +#include "../../../Outputs/Speaker/Implementation/BufferSource.hpp" #include "../../../Storage/Tape/Tape.hpp" #include "../../../Storage/Tape/Parsers/Spectrum.hpp" diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 09dd756a7..3babbde3e 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -1533,7 +1533,7 @@ 4B680CE123A5553100451D43 /* 68000ComparativeTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = 68000ComparativeTests.mm; sourceTree = ""; }; 4B680CE323A555CA00451D43 /* 68000 Comparative Tests */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "68000 Comparative Tests"; sourceTree = ""; }; 4B683B002727BE6F0043E541 /* Amiga Blitter Tests */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "Amiga Blitter Tests"; sourceTree = ""; }; - 4B698D1A1FE768A100696C91 /* SampleSource.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = SampleSource.hpp; sourceTree = ""; }; + 4B698D1A1FE768A100696C91 /* BufferSource.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = BufferSource.hpp; sourceTree = ""; }; 4B69DEB52AB79E4F0055B217 /* Instruction.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Instruction.cpp; sourceTree = ""; }; 4B69FB3B1C4D908A00B5F0AA /* Tape.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Tape.cpp; sourceTree = ""; }; 4B69FB3C1C4D908A00B5F0AA /* Tape.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Tape.hpp; sourceTree = ""; }; @@ -4004,7 +4004,7 @@ isa = PBXGroup; children = ( 4B8EF6071FE5AF830076CCDD /* LowpassSpeaker.hpp */, - 4B698D1A1FE768A100696C91 /* SampleSource.hpp */, + 4B698D1A1FE768A100696C91 /* BufferSource.hpp */, 4B770A961FE9EE770026DC70 /* CompoundSource.hpp */, ); path = Implementation; diff --git a/OSBindings/Mac/Clock SignalTests/68000 Comparative Tests/readme.md b/OSBindings/Mac/Clock SignalTests/68000 Comparative Tests/readme.md index 75d3e49be..e8155ad0c 100644 --- a/OSBindings/Mac/Clock SignalTests/68000 Comparative Tests/readme.md +++ b/OSBindings/Mac/Clock SignalTests/68000 Comparative Tests/readme.md @@ -3,12 +3,14 @@ Tests contained in this folder are original to Clock Signal. All are JSON. Tests assume a test machine consisting of a vanilla 68000 with 16mb of RAM. For each test either: + 1. start from a reset, e.g. if you have a prefetch queue you need to fill; or 2. just apply the entire initial state, which indicates the proper PC and A7 for itself. Then execute to the end of a single instruction (including any generated exception). Each file contains an array of dictionaries. Each dictionary is a single test and includes: + * a name; * initial memory contents; * initial register state; @@ -43,11 +45,13 @@ So the output is very scattergun approach, with a lot of redundancy. ## Known Issues Errors in generation mean that: + 1. MOVE is mostly untested; MOVEq is well-tested and other MOVEs appear within the test set as per the approximate generation algorithm above but due to an error in the generation of move.json, all of its opcodes are $2000 less than they should be, causing them to hit various instructions other than MOVE; 2. there is sparse coverage of the rotates and shifts: LS[L/R], AS[L/R], RO[L/R] and ROX[L/R]; and 3. there are similarly few tests of MULU. Issues with comparing results between multiple emulators in the case of unusual instructions mean that no tests have been generated for: + 1. MOVE [to or from] SR; 2. TRAP; 3. TRAPV; diff --git a/OSBindings/Mac/Clock SignalTests/AllSuiteA/readme.md b/OSBindings/Mac/Clock SignalTests/AllSuiteA/readme.md index 959121be8..fa93be7e5 100644 --- a/OSBindings/Mac/Clock SignalTests/AllSuiteA/readme.md +++ b/OSBindings/Mac/Clock SignalTests/AllSuiteA/readme.md @@ -1,2 +1,3 @@ # AllSuiteA Test Suite + This file was sourced from hmc-6502 — https://github.com/cminter/hmc-6502 — and was obtained with an unknown licence, believed to be BSD Simplified. \ No newline at end of file diff --git a/OSBindings/Mac/Clock SignalTests/Amiga Blitter Tests/README.md b/OSBindings/Mac/Clock SignalTests/Amiga Blitter Tests/README.md index 7f67d6fd7..1b3a13f5b 100644 --- a/OSBindings/Mac/Clock SignalTests/Amiga Blitter Tests/README.md +++ b/OSBindings/Mac/Clock SignalTests/Amiga Blitter Tests/README.md @@ -1,7 +1,8 @@ -## Amiga Blitter Tests +# Amiga Blitter Tests These tests record register writes and subsequent memory accesses by the Amiga Blitter over a variety of test cases. It is believed that they test all functionality other than stippled lines. They were generated using a slightly-inaccurate public domain model of the chip rather than from the real thing. In particular: + * these tests record the output as though the Blitter weren't pipelined — assuming all channels enabled, it always reads via A, then B, then C, then writes via D. The real Blitter performs two cycles of reads before its first write, and adds a final write with no additional reads; and * the tests do not record which pointer is used for a write target and therefore do not observe that the Blitter will use pointer C as a write destination for the first pixel of a line. diff --git a/OSBindings/Mac/Clock SignalTests/BCDTest/readme.md b/OSBindings/Mac/Clock SignalTests/BCDTest/readme.md index 881548c66..ebaceac92 100644 --- a/OSBindings/Mac/Clock SignalTests/BCDTest/readme.md +++ b/OSBindings/Mac/Clock SignalTests/BCDTest/readme.md @@ -1,2 +1,3 @@ # BCDTest + This test was obtained from http://stardot.org.uk/forums/viewtopic.php?t=8793#p97168 and is the work of David Banks, https://github.com/hoglet67 . It is believed to be public domain. \ No newline at end of file diff --git a/OSBindings/Mac/Clock SignalTests/Wolfgang Lorenz 6502 test suite/readme.md b/OSBindings/Mac/Clock SignalTests/Wolfgang Lorenz 6502 test suite/readme.md index d1e697f5c..ec1d0237a 100644 --- a/OSBindings/Mac/Clock SignalTests/Wolfgang Lorenz 6502 test suite/readme.md +++ b/OSBindings/Mac/Clock SignalTests/Wolfgang Lorenz 6502 test suite/readme.md @@ -1,2 +1,3 @@ # Wolfgang Lorenz’s Test Suite + The files in this folder were authored by Wolfgang Lorenz and were sourced as per http://www.softwolves.com/arkiv/cbm-hackers/7/7114.html ; they are believed to be public domain software. \ No newline at end of file diff --git a/OSBindings/SDL/main.cpp b/OSBindings/SDL/main.cpp index 1da6e2c8f..17657e81c 100644 --- a/OSBindings/SDL/main.cpp +++ b/OSBindings/SDL/main.cpp @@ -562,7 +562,7 @@ int main(int argc, char *argv[]) { } std::cout << "." << std::endl << std::endl; - std::cout << "Further machine options:" << std::endl << std::endl;; + std::cout << "Further machine options:" << std::endl << std::endl; const auto targets = Machine::TargetsByMachineName(false); const auto runtime_options = Machine::AllOptionsByMachineName(); diff --git a/Outputs/Speaker/Implementation/BufferSource.hpp b/Outputs/Speaker/Implementation/BufferSource.hpp new file mode 100644 index 000000000..3ac7eb323 --- /dev/null +++ b/Outputs/Speaker/Implementation/BufferSource.hpp @@ -0,0 +1,151 @@ +// +// SampleSource.hpp +// Clock Signal +// +// Created by Thomas Harte on 17/12/2017. +// Copyright 2017 Thomas Harte. All rights reserved. +// + +#pragma once + +#include +#include +#include +#include + +#include "../Speaker.hpp" + +namespace Outputs::Speaker { + +enum class Action { + /// New values should be _stored_ to the sample buffer. + Store, + /// New values should be _added_ to the sample buffer. + Mix, + /// New values shouldn't be stored; the source can skip generation of them if desired. + Ignore, +}; + +template void apply(SampleT &lhs, SampleT rhs) { + switch(action) { + case Action::Mix: lhs += rhs; break; + case Action::Store: lhs = rhs; break; + case Action::Ignore: break; + } +} + +template void fill(IteratorT begin, IteratorT end, SampleT value) { + switch(action) { + case Action::Mix: + while(begin != end) { + apply(*begin, value); + ++begin; + } + break; + case Action::Store: + std::fill(begin, end, value); + break; + case Action::Ignore: break; + } +} + +/*! + A sample source is something that can provide a stream of audio. + This optional base class provides the interface expected to be exposed + by the template parameter to LowpassSpeaker. +*/ +template +class BufferSource { + 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). + + No default implementation is provided. + */ + template + void apply_samples(std::size_t number_of_samples, typename SampleT::type *target); + + /*! + @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); + + /*! + 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; } +}; + +/// +template +struct SampleSource: public BufferSource { + public: + template + void apply_samples(std::size_t number_of_samples, typename SampleT::type *target) { + auto &source = *static_cast(this); + + if constexpr (divider == 1) { + while(number_of_samples--) { + apply(*target, source.level()); + ++target; + source.advance(); + } + } else { + std::size_t c = 0; + + // Fill in the tail of any partially-captured level. + auto level = source.level(); + while(c < number_of_samples && master_divider_ != divider) { + apply(target[c], level); + ++c; + ++master_divider_; + } + source.advance(); + + // Provide all full levels. + auto whole_steps = static_cast((number_of_samples - c) / divider); + while(whole_steps--) { + fill(&target[c], &target[c + divider], source.level()); + c += divider; + source.advance(); + } + + // Provide the head of a further partial capture. + level = source.level(); + master_divider_ = static_cast(number_of_samples - c); + fill(&target[c], &target[number_of_samples], source.level()); + } + } + + // TODO: use a concept here, when C++20 filters through. + // + // Until then: sample sources should implement this. +// typename SampleT::type level() const; +// void advance(); + + private: + int master_divider_{}; +}; + +} diff --git a/Outputs/Speaker/Implementation/CompoundSource.hpp b/Outputs/Speaker/Implementation/CompoundSource.hpp index 75f798e55..bdfe93545 100644 --- a/Outputs/Speaker/Implementation/CompoundSource.hpp +++ b/Outputs/Speaker/Implementation/CompoundSource.hpp @@ -8,21 +8,126 @@ #pragma once -#include "SampleSource.hpp" +#include "BufferSource.hpp" +#include #include #include #include namespace Outputs::Speaker { +/// @returns @c true if any of the templated sources is stereo; @c false otherwise. +template constexpr bool is_stereo() { + bool is_stereo = false; + ([&] { + is_stereo |= S::is_stereo; + }(), ...); + return is_stereo; +} + +/// @returns @c true if the variadic template arguments are ordered as all stereo sources followed by +/// all mono; @c false otherwise. +template constexpr bool are_properly_ordered() { + bool is_ordered = true; + bool is_stereo = true; + ([&] { + if(S::is_stereo && !is_stereo) { + is_ordered = false; + } + is_stereo &= S::is_stereo; + }(), ...); + return is_ordered; +} + /*! A CompoundSource adds together the sound generated by multiple individual SampleSources. An owner may optionally assign relative volumes. */ template class CompoundSource: - public Outputs::Speaker::SampleSource { + public Outputs::Speaker::BufferSource, ::Outputs::Speaker::is_stereo()> { + private: + template class CompoundSourceHolder { + public: + static constexpr bool is_stereo = false; + void set_scaled_volume_range(int16_t, double *, double) {} + static constexpr std::size_t size() { return 0; } + double total_scale(double *) const { return 0.0; } + }; + + template class CompoundSourceHolder { + public: + CompoundSourceHolder(S &source, R &...next) : source_(source), next_source_(next...) {} + + static constexpr bool is_stereo = S::is_stereo || CompoundSourceHolder::is_stereo; + + template + void apply_samples(std::size_t number_of_samples, typename ::Outputs::Speaker::SampleT::type *target) { + + // If this is the step at which a mono-to-stereo adaptation happens, apply it. + if constexpr (output_stereo && !S::is_stereo) { + // There'll be only one place in the chain that this conversion happens, but it'll + // happen there often. So avoid continuously reallocating. + if(conversion_source_.size() < number_of_samples) { + conversion_source_.resize(number_of_samples); + } + + // Populate the conversion buffer with this source and all below. + apply_samples(number_of_samples, conversion_source_.data()); + + // 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])); + } + return; + } + + 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()); + } + 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) { + const auto scaled_range = volumes[0] / double(source_.average_output_peak()) * double(range) / scale; + source_.set_sample_volume_range(int16_t(scaled_range)); + next_source_.set_scaled_volume_range(range, &volumes[1], scale); + } + + static constexpr std::size_t size() { + return 1 + CompoundSourceHolder::size(); + } + + double total_scale(double *volumes) const { + return (volumes[0] / source_.average_output_peak()) + next_source_.total_scale(&volumes[1]); + } + + private: + S &source_; + CompoundSourceHolder next_source_; + std::vector conversion_source_; + }; + public: + using Sample = typename SampleT<::Outputs::Speaker::is_stereo()>::type; + + // To ensure at most one mono to stereo conversion, require appropriate source ordering. + static_assert(are_properly_ordered(), "Sources should be listed with all stereo sources before all mono sources"); + CompoundSource(T &... sources) : source_holder_(sources...) { // Default: give all sources equal volume. const auto volume = 1.0 / double(source_holder_.size()); @@ -31,12 +136,9 @@ template class CompoundSource: } } - void get_samples(std::size_t number_of_samples, std::int16_t *target) { - source_holder_.template get_samples(number_of_samples, target); - } - - void skip_samples(const std::size_t number_of_samples) { - source_holder_.skip_samples(number_of_samples); + template + void apply_samples(std::size_t number_of_samples, Sample *target) { + source_holder_.template apply_samples()>(number_of_samples, target); } /*! @@ -59,16 +161,11 @@ template class CompoundSource: average_output_peak_ = 1.0 / source_holder_.total_scale(volumes_.data()); } - /*! - @returns true if any of the sources owned by this CompoundSource is stereo. - */ - static constexpr bool get_is_stereo() { return CompoundSourceHolder::get_is_stereo(); } - /*! @returns the average output peak given the sources owned by this CompoundSource and the current relative volumes. */ - double get_average_output_peak() const { + double average_output_peak() const { return average_output_peak_; } @@ -78,94 +175,6 @@ template class CompoundSource: source_holder_.set_scaled_volume_range(volume_range_, volumes_.data(), scale); } - template class CompoundSourceHolder: public Outputs::Speaker::SampleSource { - public: - template void get_samples(std::size_t number_of_samples, std::int16_t *target) { - std::memset(target, 0, sizeof(std::int16_t) * number_of_samples); - } - - void set_scaled_volume_range(int16_t, double *, double) {} - - static constexpr std::size_t size() { - return 0; - } - - static constexpr bool get_is_stereo() { - return false; - } - - double total_scale(double *) const { - return 0.0; - } - }; - - template class CompoundSourceHolder { - public: - CompoundSourceHolder(S &source, R &...next) : source_(source), next_source_(next...) {} - - template void get_samples(std::size_t number_of_samples, std::int16_t *target) { - // Get the rest of the output. - next_source_.template get_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 — just pass the call onward. - source_.skip_samples(number_of_samples); - return; - } - - // Get this component's output. - auto buffer_size = number_of_samples * (output_stereo ? 2 : 1); - int16_t local_samples[buffer_size]; - source_.get_samples(number_of_samples, local_samples); - - // Merge it in; furthermore if total output is stereo but this source isn't, - // map it to stereo. - if constexpr (output_stereo == S::get_is_stereo()) { - while(buffer_size--) { - target[buffer_size] += local_samples[buffer_size]; - } - } else { - // This will happen only if mapping from mono to stereo, never in the - // other direction, because the compound source outputs stereo if any - // subcomponent does. So it outputs mono only if no stereo devices are - // in the mixing chain. - while(buffer_size--) { - target[buffer_size] += local_samples[buffer_size >> 1]; - } - } - - // TODO: accelerate above? - } - - void skip_samples(const std::size_t number_of_samples) { - source_.skip_samples(number_of_samples); - next_source_.skip_samples(number_of_samples); - } - - void set_scaled_volume_range(int16_t range, double *volumes, double scale) { - const auto scaled_range = volumes[0] / double(source_.get_average_output_peak()) * double(range) / scale; - source_.set_sample_volume_range(int16_t(scaled_range)); - next_source_.set_scaled_volume_range(range, &volumes[1], scale); - } - - static constexpr std::size_t size() { - return 1 + CompoundSourceHolder::size(); - } - - static constexpr bool get_is_stereo() { - return S::get_is_stereo() || CompoundSourceHolder::get_is_stereo(); - } - - double total_scale(double *volumes) const { - return (volumes[0] / source_.get_average_output_peak()) + next_source_.total_scale(&volumes[1]); - } - - private: - S &source_; - CompoundSourceHolder next_source_; - }; - CompoundSourceHolder source_holder_; std::vector volumes_; int16_t volume_range_ = 0; diff --git a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp index 294e19589..4b3d5d16e 100644 --- a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp +++ b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp @@ -8,6 +8,7 @@ #pragma once +#include "BufferSource.hpp" #include "../Speaker.hpp" #include "../../../SignalProcessing/FIRFilter.hpp" #include "../../../ClockReceiver/ClockReceiver.hpp" @@ -348,7 +349,7 @@ template class PushLowpass: public LowpassBase class PullLowpass: public LowpassBase, SampleSource::get_is_stereo()> { +template class PullLowpass: public LowpassBase, SampleSource::is_stereo> { public: PullLowpass(SampleSource &sample_source) : sample_source_(sample_source) { // Propagate an initial volume level. @@ -362,7 +363,7 @@ template class PullLowpass: public LowpassBase class PullLowpass: public LowpassBase, SampleSource::get_is_stereo()>; + using BaseT = LowpassBase, SampleSource::is_stereo>; friend BaseT; using BaseT::process; @@ -396,15 +397,20 @@ template class PullLowpass: public LowpassBase(count, nullptr); } int get_scale() { - return int(65536.0 / sample_source_.get_average_output_peak()); + return int(65536.0 / sample_source_.average_output_peak()); } void get_samples(size_t length, int16_t *target) { - sample_source_.get_samples(length, target); + if constexpr (SampleSource::is_stereo) { + StereoSample *const stereo_target = reinterpret_cast(target); + sample_source_.template apply_samples(length, stereo_target); + } else { + sample_source_.template apply_samples(length, target); + } } }; diff --git a/Outputs/Speaker/Implementation/SampleSource.hpp b/Outputs/Speaker/Implementation/SampleSource.hpp deleted file mode 100644 index 55e26b221..000000000 --- a/Outputs/Speaker/Implementation/SampleSource.hpp +++ /dev/null @@ -1,72 +0,0 @@ -// -// SampleSource.hpp -// Clock Signal -// -// Created by Thomas Harte on 17/12/2017. -// Copyright 2017 Thomas Harte. All rights reserved. -// - -#pragma once - -#include -#include - -namespace Outputs::Speaker { - -/*! - A sample source is something that can provide a stream of audio. - This optional base class provides the interface expected to be exposed - by the template parameter to LowpassSpeaker. -*/ -class SampleSource { - public: - /*! - Should write the next @c number_of_samples to @c target. - */ - void get_samples([[maybe_unused]] std::size_t number_of_samples, [[maybe_unused]] std::int16_t *target) {} - - /*! - Should skip the next @c number_of_samples. Subclasses of this SampleSource - need not implement this if it would no more efficient to do so than it is - merely to call get_samples and throw the result away, as per the default - implementation below. - */ - void skip_samples(const std::size_t number_of_samples) { - std::int16_t scratch_pad[number_of_samples]; - get_samples(number_of_samples, scratch_pad); - } - - /*! - @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([[maybe_unused]] std::int16_t volume) {} - - /*! - Indicates whether this component will write stereo samples. - */ - static constexpr bool get_is_stereo() { return false; } - - /*! - 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 get_average_output_peak() const { return 1.0; } -}; - -} diff --git a/Outputs/Speaker/Speaker.hpp b/Outputs/Speaker/Speaker.hpp index c35b182c1..edeee7811 100644 --- a/Outputs/Speaker/Speaker.hpp +++ b/Outputs/Speaker/Speaker.hpp @@ -14,6 +14,41 @@ namespace Outputs::Speaker { +using MonoSample = int16_t; +struct StereoSample { +#if TARGET_RT_BIG_ENDIAN + int16_t right = 0, left = 0; +#else + int16_t left = 0, right = 0; +#endif + + StereoSample() = default; + StereoSample(const StereoSample &rhs) { + left = rhs.left; + right = rhs.right; + } + StereoSample(MonoSample value) { + left = right = value; + } + + StereoSample &operator +=(const StereoSample &rhs) { + left += rhs.left; + right += rhs.right; + return *this; + } + + StereoSample operator +(const StereoSample &rhs) { + StereoSample result; + result.left = left + rhs.left; + result.right = right + rhs.right; + return *this; + } +}; + +template struct SampleT { + using type = std::conditional_t; +}; + /*! Provides a communication point for sound; machines that have a speaker provide an audio output. diff --git a/Packaging/README.md b/Packaging/README.md index ee1a0e521..19b4b34c3 100644 --- a/Packaging/README.md +++ b/Packaging/README.md @@ -1,8 +1,9 @@ # RPM packaging for clksignal -This simple Ansible playbook creates and installs an RPM package of the current release of clksignal + +This simple Ansible playbook creates and installs an RPM package of the current release of clksignal. If the version that you build is newer than what you have installed, it will be automatically upgraded. ## Usage -`ansible-playbook main.yml -K + ansible-playbook main.yml -K diff --git a/Processors/65816/Implementation/65816Implementation.hpp b/Processors/65816/Implementation/65816Implementation.hpp index be8492f77..6dd58d050 100644 --- a/Processors/65816/Implementation/65816Implementation.hpp +++ b/Processors/65816/Implementation/65816Implementation.hpp @@ -793,7 +793,7 @@ template void Processor + using namespace CPU; AllRAMProcessor::AllRAMProcessor(std::size_t memory_size) : diff --git a/README.md b/README.md index ec0209fe0..d4a650c5e 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,19 @@ ![Clock Signal Application Icon](READMEImages/Icon.png) + # Clock Signal + Clock Signal ('CLK') is an emulator that seeks to be invisible. Users directly launch classic software, avoiding the learning curves associated with emulators and with classic machines. macOS and source releases are [hosted on GitHub](https://github.com/TomHarte/CLK/releases). A Qt-based Linux build is available as a [Snap](https://snapcraft.io/clock-signal). This emulator seeks to offer: + * single-click load of any piece of source media for any supported platform; * with a heavy signal processing tilt for accurate reproduction of original outputs; * avoiding latency as much as possible. It currently contains emulations of the: + * Acorn Electron; * Amstrad CPC; * Apple II/II+ and IIe; @@ -26,6 +30,7 @@ It currently contains emulations of the: * Sinclair ZX Spectrum. Also present but very much upcoming are the: + * Commodore Amiga; and * early PC compatible. diff --git a/Reflection/Struct.cpp b/Reflection/Struct.cpp index babd12744..81c62fb2e 100644 --- a/Reflection/Struct.cpp +++ b/Reflection/Struct.cpp @@ -110,7 +110,7 @@ template <> bool Reflection::set(Struct &target, const std::string &name, bool v if(!target_type) return false; if(*target_type == typeid(bool)) { - target.set(name, &value, offset);; + target.set(name, &value, offset); } return false; diff --git a/Storage/Disk/Encodings/MFM/Parser.cpp b/Storage/Disk/Encodings/MFM/Parser.cpp index 7f3a848e7..b6ad3a58a 100644 --- a/Storage/Disk/Encodings/MFM/Parser.cpp +++ b/Storage/Disk/Encodings/MFM/Parser.cpp @@ -37,7 +37,7 @@ void Parser::install_track(const Storage::Disk::Track::Address &address) { // Just try all three in succession. append(parse_track(*track, Density::Single), sectors_by_id); append(parse_track(*track, Density::Double), sectors_by_id); - append(parse_track(*track, Density::High), sectors_by_id);; + append(parse_track(*track, Density::High), sectors_by_id); } sectors_by_address_by_track_.emplace(address, std::move(sectors_by_id)); diff --git a/cmake/CLK_SOURCES.cmake b/cmake/CLK_SOURCES.cmake new file mode 100644 index 000000000..903728b12 --- /dev/null +++ b/cmake/CLK_SOURCES.cmake @@ -0,0 +1,256 @@ +# Generated by generate_CLK_SOURCES. + +set(CLK_SOURCES + Analyser/Dynamic/ConfidenceCounter.cpp + Analyser/Dynamic/ConfidenceSummary.cpp + Analyser/Dynamic/MultiMachine/Implementation/MultiConfigurable.cpp + Analyser/Dynamic/MultiMachine/Implementation/MultiJoystickMachine.cpp + Analyser/Dynamic/MultiMachine/Implementation/MultiKeyboardMachine.cpp + Analyser/Dynamic/MultiMachine/Implementation/MultiMediaTarget.cpp + Analyser/Dynamic/MultiMachine/Implementation/MultiProducer.cpp + Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.cpp + Analyser/Dynamic/MultiMachine/MultiMachine.cpp + Analyser/Static/Acorn/Disk.cpp + Analyser/Static/Acorn/StaticAnalyser.cpp + Analyser/Static/Acorn/Tape.cpp + Analyser/Static/Amiga/StaticAnalyser.cpp + Analyser/Static/AmstradCPC/StaticAnalyser.cpp + Analyser/Static/AppleII/StaticAnalyser.cpp + Analyser/Static/AppleIIgs/StaticAnalyser.cpp + Analyser/Static/Atari2600/StaticAnalyser.cpp + Analyser/Static/AtariST/StaticAnalyser.cpp + Analyser/Static/Coleco/StaticAnalyser.cpp + Analyser/Static/Commodore/Disk.cpp + Analyser/Static/Commodore/File.cpp + Analyser/Static/Commodore/StaticAnalyser.cpp + Analyser/Static/Commodore/Tape.cpp + Analyser/Static/Disassembler/6502.cpp + Analyser/Static/Disassembler/Z80.cpp + Analyser/Static/DiskII/StaticAnalyser.cpp + Analyser/Static/Enterprise/StaticAnalyser.cpp + Analyser/Static/FAT12/StaticAnalyser.cpp + Analyser/Static/MSX/StaticAnalyser.cpp + Analyser/Static/MSX/Tape.cpp + Analyser/Static/Macintosh/StaticAnalyser.cpp + Analyser/Static/Oric/StaticAnalyser.cpp + Analyser/Static/Oric/Tape.cpp + Analyser/Static/PCCompatible/StaticAnalyser.cpp + Analyser/Static/Sega/StaticAnalyser.cpp + Analyser/Static/StaticAnalyser.cpp + Analyser/Static/ZX8081/StaticAnalyser.cpp + Analyser/Static/ZXSpectrum/StaticAnalyser.cpp + + Components/1770/1770.cpp + Components/5380/ncr5380.cpp + Components/6522/Implementation/IRQDelegatePortHandler.cpp + Components/6560/6560.cpp + Components/6850/6850.cpp + Components/68901/MFP68901.cpp + Components/8272/i8272.cpp + Components/8530/z8530.cpp + Components/9918/Implementation/9918.cpp + Components/AY38910/AY38910.cpp + Components/AudioToggle/AudioToggle.cpp + Components/DiskII/DiskII.cpp + Components/DiskII/DiskIIDrive.cpp + Components/DiskII/IWM.cpp + Components/DiskII/MacintoshDoubleDensityDrive.cpp + Components/KonamiSCC/KonamiSCC.cpp + Components/OPx/OPLL.cpp + Components/RP5C01/RP5C01.cpp + Components/SN76489/SN76489.cpp + Components/Serial/Line.cpp + + Inputs/Keyboard.cpp + + InstructionSets/M50740/Decoder.cpp + InstructionSets/M50740/Executor.cpp + InstructionSets/M68k/Decoder.cpp + InstructionSets/M68k/Instruction.cpp + InstructionSets/PowerPC/Decoder.cpp + InstructionSets/x86/Decoder.cpp + InstructionSets/x86/Instruction.cpp + + Machines/Amiga/Amiga.cpp + Machines/Amiga/Audio.cpp + Machines/Amiga/Bitplanes.cpp + Machines/Amiga/Blitter.cpp + Machines/Amiga/Chipset.cpp + Machines/Amiga/Copper.cpp + Machines/Amiga/Disk.cpp + Machines/Amiga/Keyboard.cpp + Machines/Amiga/MouseJoystick.cpp + Machines/Amiga/Sprites.cpp + Machines/AmstradCPC/AmstradCPC.cpp + Machines/AmstradCPC/Keyboard.cpp + Machines/Apple/ADB/Bus.cpp + Machines/Apple/ADB/Keyboard.cpp + Machines/Apple/ADB/Mouse.cpp + Machines/Apple/ADB/ReactiveDevice.cpp + Machines/Apple/AppleII/AppleII.cpp + Machines/Apple/AppleII/DiskIICard.cpp + Machines/Apple/AppleII/Joystick.cpp + Machines/Apple/AppleII/SCSICard.cpp + Machines/Apple/AppleII/Video.cpp + Machines/Apple/AppleIIgs/ADB.cpp + Machines/Apple/AppleIIgs/AppleIIgs.cpp + Machines/Apple/AppleIIgs/MemoryMap.cpp + Machines/Apple/AppleIIgs/Sound.cpp + Machines/Apple/AppleIIgs/Video.cpp + Machines/Apple/Macintosh/Audio.cpp + Machines/Apple/Macintosh/DriveSpeedAccumulator.cpp + Machines/Apple/Macintosh/Keyboard.cpp + Machines/Apple/Macintosh/Macintosh.cpp + Machines/Apple/Macintosh/Video.cpp + Machines/Atari/2600/Atari2600.cpp + Machines/Atari/2600/TIA.cpp + Machines/Atari/2600/TIASound.cpp + Machines/Atari/ST/AtariST.cpp + Machines/Atari/ST/DMAController.cpp + Machines/Atari/ST/IntelligentKeyboard.cpp + Machines/Atari/ST/Video.cpp + Machines/ColecoVision/ColecoVision.cpp + Machines/Commodore/1540/Implementation/C1540.cpp + Machines/Commodore/SerialBus.cpp + Machines/Commodore/Vic-20/Keyboard.cpp + Machines/Commodore/Vic-20/Vic20.cpp + Machines/Electron/Electron.cpp + Machines/Electron/Keyboard.cpp + Machines/Electron/Plus3.cpp + Machines/Electron/SoundGenerator.cpp + Machines/Electron/Tape.cpp + Machines/Electron/Video.cpp + Machines/Enterprise/Dave.cpp + Machines/Enterprise/EXDos.cpp + Machines/Enterprise/Enterprise.cpp + Machines/Enterprise/Keyboard.cpp + Machines/Enterprise/Nick.cpp + Machines/KeyboardMachine.cpp + Machines/MSX/DiskROM.cpp + Machines/MSX/Keyboard.cpp + Machines/MSX/MSX.cpp + Machines/MSX/MemorySlotHandler.cpp + Machines/MasterSystem/MasterSystem.cpp + Machines/Oric/BD500.cpp + Machines/Oric/Jasmin.cpp + Machines/Oric/Keyboard.cpp + Machines/Oric/Microdisc.cpp + Machines/Oric/Oric.cpp + Machines/Oric/Video.cpp + Machines/PCCompatible/PCCompatible.cpp + Machines/Sinclair/Keyboard/Keyboard.cpp + Machines/Sinclair/ZX8081/Video.cpp + Machines/Sinclair/ZX8081/ZX8081.cpp + Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp + Machines/Utility/MachineForTarget.cpp + Machines/Utility/MemoryFuzzer.cpp + Machines/Utility/MemoryPacker.cpp + Machines/Utility/ROMCatalogue.cpp + Machines/Utility/StringSerialiser.cpp + Machines/Utility/Typer.cpp + + Outputs/CRT/CRT.cpp + Outputs/DisplayMetrics.cpp + Outputs/OpenGL/Primitives/Rectangle.cpp + Outputs/OpenGL/Primitives/Shader.cpp + Outputs/OpenGL/Primitives/TextureTarget.cpp + Outputs/OpenGL/ScanTarget.cpp + Outputs/OpenGL/ScanTargetGLSLFragments.cpp + Outputs/ScanTarget.cpp + Outputs/ScanTargets/BufferingScanTarget.cpp + + Processors/6502/Implementation/6502Storage.cpp + Processors/6502/State/State.cpp + Processors/65816/Implementation/65816Base.cpp + Processors/65816/Implementation/65816Storage.cpp + Processors/Z80/Implementation/PartialMachineCycle.cpp + Processors/Z80/Implementation/Z80Base.cpp + Processors/Z80/Implementation/Z80Storage.cpp + Processors/Z80/State/State.cpp + + Reflection/Struct.cpp + + SignalProcessing/FIRFilter.cpp + + Storage/Cartridge/Cartridge.cpp + Storage/Cartridge/Encodings/CommodoreROM.cpp + Storage/Cartridge/Formats/BinaryDump.cpp + Storage/Cartridge/Formats/PRG.cpp + Storage/Data/Commodore.cpp + Storage/Data/ZX8081.cpp + Storage/Disk/Controller/DiskController.cpp + Storage/Disk/Controller/MFMDiskController.cpp + Storage/Disk/DiskImage/Formats/2MG.cpp + Storage/Disk/DiskImage/Formats/AcornADF.cpp + Storage/Disk/DiskImage/Formats/AmigaADF.cpp + Storage/Disk/DiskImage/Formats/AppleDSK.cpp + Storage/Disk/DiskImage/Formats/CPCDSK.cpp + Storage/Disk/DiskImage/Formats/D64.cpp + Storage/Disk/DiskImage/Formats/DMK.cpp + Storage/Disk/DiskImage/Formats/FAT12.cpp + Storage/Disk/DiskImage/Formats/G64.cpp + Storage/Disk/DiskImage/Formats/HFE.cpp + Storage/Disk/DiskImage/Formats/IMD.cpp + Storage/Disk/DiskImage/Formats/IPF.cpp + Storage/Disk/DiskImage/Formats/MFMSectorDump.cpp + Storage/Disk/DiskImage/Formats/MSA.cpp + Storage/Disk/DiskImage/Formats/MacintoshIMG.cpp + Storage/Disk/DiskImage/Formats/NIB.cpp + Storage/Disk/DiskImage/Formats/OricMFMDSK.cpp + Storage/Disk/DiskImage/Formats/PCBooter.cpp + Storage/Disk/DiskImage/Formats/SSD.cpp + Storage/Disk/DiskImage/Formats/STX.cpp + Storage/Disk/DiskImage/Formats/Utility/ImplicitSectors.cpp + Storage/Disk/DiskImage/Formats/WOZ.cpp + Storage/Disk/Drive.cpp + Storage/Disk/Encodings/AppleGCR/Encoder.cpp + Storage/Disk/Encodings/AppleGCR/SegmentParser.cpp + Storage/Disk/Encodings/CommodoreGCR.cpp + Storage/Disk/Encodings/MFM/Encoder.cpp + Storage/Disk/Encodings/MFM/Parser.cpp + Storage/Disk/Encodings/MFM/SegmentParser.cpp + Storage/Disk/Encodings/MFM/Shifter.cpp + Storage/Disk/Parsers/CPM.cpp + Storage/Disk/Parsers/FAT.cpp + Storage/Disk/Track/PCMSegment.cpp + Storage/Disk/Track/PCMTrack.cpp + Storage/Disk/Track/TrackSerialiser.cpp + Storage/Disk/Track/UnformattedTrack.cpp + Storage/FileHolder.cpp + Storage/MassStorage/Encodings/MacintoshVolume.cpp + Storage/MassStorage/Formats/DAT.cpp + Storage/MassStorage/Formats/DSK.cpp + Storage/MassStorage/Formats/HDV.cpp + Storage/MassStorage/Formats/HFV.cpp + Storage/MassStorage/MassStorageDevice.cpp + Storage/MassStorage/SCSI/DirectAccessDevice.cpp + Storage/MassStorage/SCSI/SCSI.cpp + Storage/MassStorage/SCSI/Target.cpp + Storage/State/SNA.cpp + Storage/State/SZX.cpp + Storage/State/Z80.cpp + Storage/Tape/Formats/CAS.cpp + Storage/Tape/Formats/CSW.cpp + Storage/Tape/Formats/CommodoreTAP.cpp + Storage/Tape/Formats/OricTAP.cpp + Storage/Tape/Formats/TZX.cpp + Storage/Tape/Formats/TapePRG.cpp + Storage/Tape/Formats/TapeUEF.cpp + Storage/Tape/Formats/ZX80O81P.cpp + Storage/Tape/Formats/ZXSpectrumTAP.cpp + Storage/Tape/Parsers/Acorn.cpp + Storage/Tape/Parsers/Commodore.cpp + Storage/Tape/Parsers/MSX.cpp + Storage/Tape/Parsers/Oric.cpp + Storage/Tape/Parsers/Spectrum.cpp + Storage/Tape/Parsers/ZX8081.cpp + Storage/Tape/PulseQueuedTape.cpp + Storage/Tape/Tape.cpp + Storage/TimedEventLoop.cpp +) + +if(CLK_UI STREQUAL "SDL") + list(APPEND CLK_SOURCES + OSBindings/SDL/main.cpp + ) +endif() diff --git a/cmake/generate_CLK_SOURCES b/cmake/generate_CLK_SOURCES new file mode 100755 index 000000000..fc5e2e344 --- /dev/null +++ b/cmake/generate_CLK_SOURCES @@ -0,0 +1,58 @@ +#!/usr/bin/env bash + +# Run this script to regenerate CLK_SOURCES.cmake after you add or remove any +# source files. + +set -euo pipefail + +script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd) +top_dir="$script_dir/.." + +out="$script_dir/CLK_SOURCES.cmake" +trap 'rm -f "$tmp"' EXIT +tmp=$(mktemp "$out.XXXXXXXX") + +awkscript=' +BEGIN { + print ind pre "CLK_SOURCES" +} + +{ + if (NR > 1 && last != $1) + print "" + last = $1 + sub("^", ind "\t") + print +} + +END { + print ind ")" +} +' + +write_sources() { + local ind="$1" + local pre="$2" + shift 2 + find -s "$@" | awk -F/ -vind="$ind" -vpre="$pre" "$awkscript" || exit $? +} + +cd "$top_dir" +printf '# Generated by %s.\n\n' "${BASH_SOURCE[0]##*/}" > "$tmp" +write_sources '' 'set(' [A-Z]* -name OSBindings -prune -o \ + -name '*AllRAM*.cpp' -prune -o -name '*.cpp' -print >> "$tmp" +# TODO: Add 'Mac/Clock Signal' +# TODO: Add Qt +for dir in SDL; do + ui=$(echo "${dir%%/*}" | tr '[:lower:]' '[:upper:]') + dir="OSBindings/$dir" + printf '\nif(CLK_UI STREQUAL "%s")\n' "$ui" >> "$tmp" + if [[ $ui = "MAC" ]]; then + args=('-name' '*.m' '-o' '-name' '*.metal' '-o' '-name' '*.mm' '-o' '-name' '*.swift') + else + args=('-name' '*.cpp') + fi + write_sources $'\t' 'list(APPEND ' "$dir" "${args[@]}" >> "$tmp" + printf 'endif()\n' >> "$tmp" +done +mv "$tmp" "$out"