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 @@
+
+# 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