1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-11-26 08:49:37 +00:00

Merge pull request #1328 from TomHarte/PerSampleAudio

Improve SampleSource infrastructure.
This commit is contained in:
Thomas Harte 2024-02-12 16:38:01 -05:00 committed by GitHub
commit fd45745600
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 504 additions and 336 deletions

View File

@ -19,6 +19,7 @@ AudioGenerator::AudioGenerator(Concurrency::AsyncTaskQueue<false> &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 <Outputs::Speaker::Action action>
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<action>(
target[c],
Outputs::Speaker::MonoSample(
sample * volume_ + dc_offset_
));
}
}
template void AudioGenerator::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *);
template void AudioGenerator::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *);
template void AudioGenerator::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *);
void AudioGenerator::set_sample_volume_range(std::int16_t range) {
range_multiplier_ = int16_t(range / 64);

View File

@ -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<AudioGenerator, false> {
public:
AudioGenerator(Concurrency::AsyncTaskQueue<false> &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 <Outputs::Speaker::Action action>
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<false> &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;
};

View File

@ -101,7 +101,12 @@ template <bool is_stereo> void AY38910<is_stereo>::set_output_mixing(float a_lef
c_right_ = uint8_t(c_right * 255.0f);
}
template <bool is_stereo> void AY38910<is_stereo>::get_samples(std::size_t number_of_samples, int16_t *target) {
template <bool is_stereo>
template <Outputs::Speaker::Action action>
void AY38910<is_stereo>::apply_samples(
std::size_t number_of_samples,
typename Outputs::Speaker::SampleT<is_stereo>::type *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.
@ -113,11 +118,7 @@ template <bool is_stereo> void AY38910<is_stereo>::get_samples(std::size_t numbe
std::size_t c = 0;
while((master_divider_&3) && c < number_of_samples) {
if constexpr (is_stereo) {
reinterpret_cast<uint32_t *>(target)[c] = output_volume_;
} else {
target[c] = int16_t(output_volume_);
}
Outputs::Speaker::apply<action>(target[c], output_volume_);
master_divider_++;
c++;
}
@ -159,11 +160,7 @@ template <bool is_stereo> void AY38910<is_stereo>::get_samples(std::size_t numbe
evaluate_output_volume();
for(int ic = 0; ic < 4 && c < number_of_samples; ic++) {
if constexpr (is_stereo) {
reinterpret_cast<uint32_t *>(target)[c] = output_volume_;
} else {
target[c] = int16_t(output_volume_);
}
Outputs::Speaker::apply<action>(target[c], output_volume_);
c++;
master_divider_++;
}
@ -172,6 +169,13 @@ template <bool is_stereo> void AY38910<is_stereo>::get_samples(std::size_t numbe
master_divider_ &= 3;
}
template void AY38910<false>::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, typename Outputs::Speaker::SampleT<false>::type *);
template void AY38910<false>::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, typename Outputs::Speaker::SampleT<false>::type *);
template void AY38910<false>::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, typename Outputs::Speaker::SampleT<false>::type *);
template void AY38910<true>::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, typename Outputs::Speaker::SampleT<true>::type *);
template void AY38910<true>::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, typename Outputs::Speaker::SampleT<true>::type *);
template void AY38910<true>::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, typename Outputs::Speaker::SampleT<true>::type *);
template <bool is_stereo> void AY38910<is_stereo>::evaluate_output_volume() {
int envelope_volume = envelope_shapes_[output_registers_[13]][envelope_position_ | envelope_position_mask_];
@ -214,19 +218,18 @@ template <bool is_stereo> void AY38910<is_stereo>::evaluate_output_volume() {
// Mix additively, weighting if in stereo.
if constexpr (is_stereo) {
int16_t *const output_volumes = reinterpret_cast<int16_t *>(&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]

View File

@ -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,7 +66,7 @@ enum class Personality {
This AY has an attached mono or stereo mixer.
*/
template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource {
template <bool stereo> class AY38910: public ::Outputs::Speaker::BufferSource<AY38910<stereo>, stereo> {
public:
/// Creates a new AY38910.
AY38910(Personality, Concurrency::AsyncTaskQueue<false> &);
@ -105,11 +105,11 @@ template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource
*/
void set_output_mixing(float a_left, float b_left, float c_left, float a_right = 1.0, float b_right = 1.0, float c_right = 1.0);
// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter.
void get_samples(std::size_t number_of_samples, int16_t *target);
// Buffer generation.
template <Outputs::Speaker::Action action>
void apply_samples(std::size_t number_of_samples, typename Outputs::Speaker::SampleT<stereo>::type *target);
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<false> &task_queue_;
@ -150,7 +150,7 @@ template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource
uint8_t data_input_, data_output_;
uint32_t output_volume_;
typename Outputs::Speaker::SampleT<stereo>::type output_volume_;
void update_bus();
PortHandler *port_handler_ = nullptr;

View File

@ -8,27 +8,23 @@
#include "AudioToggle.hpp"
#include <algorithm>
using namespace Audio;
Audio::Toggle::Toggle(Concurrency::AsyncTaskQueue<false> &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;
});
}

View File

@ -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,15 @@ 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<Toggle, false> {
public:
Toggle(Concurrency::AsyncTaskQueue<false> &audio_queue);
void get_samples(std::size_t number_of_samples, std::int16_t *target);
template <Outputs::Speaker::Action action>
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) {
Outputs::Speaker::fill<action>(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);
void set_output(bool enabled);
bool get_output() const;
@ -34,6 +36,7 @@ class Toggle: public Outputs::Speaker::SampleSource {
// Accessed on the audio thread.
int16_t level_ = 0, volume_ = 0;
bool level_active_ = false;
};
}

View File

@ -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 <Outputs::Speaker::Action action>
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<action>(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<action>(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<action>(target[c], transient_output_level_);
c++;
master_divider_++;
}
}
}
template void SCC::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *);
template void SCC::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *);
template void SCC::apply_samples<Outputs::Speaker::Action::Ignore>(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;
}

View File

@ -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<SCC, false> {
public:
/// Creates a new SCC.
SCC(Concurrency::AsyncTaskQueue<false> &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 <Outputs::Speaker::Action action>
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;

View File

@ -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 <typename Child> class OPLBase: public ::Outputs::Speaker::SampleSource {
template <typename Child, bool stereo> class OPLBase: public ::Outputs::Speaker::BufferSource<Child, stereo> {
public:
void write(uint16_t address, uint8_t value) {
if(address & 1) {

View File

@ -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 <Outputs::Speaker::Action action>
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<action>(*target, output_levels_[audio_offset_ / channel_output_period]);
++target;
audio_offset_ = (audio_offset_ + 1) % update_period;
}
}
template void OPLL::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *);
template void OPLL::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *);
template void OPLL::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *);
void OPLL::update_all_channels() {
oscillator_.update();

View File

@ -19,24 +19,25 @@
namespace Yamaha::OPL {
class OPLL: public OPLBase<OPLL> {
class OPLL: public OPLBase<OPLL, false> {
public:
/// Creates a new OPLL or VRC7.
OPLL(Concurrency::AsyncTaskQueue<false> &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 <Outputs::Speaker::Action action>
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target);
void set_sample_volume_range(std::int16_t range);
// 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<OPLL>;
friend OPLBase<OPLL, false>;
void write_register(uint8_t address, uint8_t value);
int audio_divider_ = 0;

View File

@ -99,10 +99,11 @@ void SN76489::evaluate_output_volume() {
);
}
void SN76489::get_samples(std::size_t number_of_samples, std::int16_t *target) {
template <Outputs::Speaker::Action action>
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<action>(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<action>(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<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *);
template void SN76489::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *);
template void SN76489::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *);

View File

@ -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<SN76489, false> {
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 <Outputs::Speaker::Action action>
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;

View File

@ -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.

View File

@ -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<uint32_t>();
}
void GLU::get_samples(std::size_t number_of_samples, std::int16_t *target) {
template <Outputs::Speaker::Action action>
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<action>(number_of_samples, target);
}
template void GLU::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *);
template void GLU::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *);
template void GLU::apply_samples<Outputs::Speaker::Action::Ignore>(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 <Outputs::Speaker::Action action>
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<action>(
target[sample],
Outputs::Speaker::MonoSample(
(output * output_range_) >> 20
)
);
// Apply any RAM writes that interleave here.
++pending_store_read_time_;

View File

@ -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<GLU, false> { // TODO: isn't this stereo?
public:
GLU(Concurrency::AsyncTaskQueue<false> &audio_queue);
@ -34,9 +34,9 @@ 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 <Outputs::Speaker::Action action>
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);
private:
Concurrency::AsyncTaskQueue<false> &audio_queue_;
@ -94,7 +94,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 <Outputs::Speaker::Action action>
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.

View File

@ -8,6 +8,8 @@
#include "Audio.hpp"
#include <algorithm>
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 <Outputs::Speaker::Action action>
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<action>(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<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *);
template void Audio::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *);
template void Audio::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *);

View File

@ -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 <array>
#include <atomic>
@ -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<Audio, false> {
public:
Audio(Concurrency::AsyncTaskQueue<false> &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 <Outputs::Speaker::Action action>
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<false> &task_queue_;

View File

@ -28,7 +28,7 @@ class DriveSpeedAccumulator {
Sets the delegate to receive drive speed changes.
*/
void set_delegate(Delegate *delegate) {
delegate_ = delegate;;
delegate_ = delegate;
}
private:

View File

@ -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 <Outputs::Speaker::Action action>
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<action>(target[c], output);
}
}
template void Atari2600::TIASound::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *);
template void Atari2600::TIASound::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *);
template void Atari2600::TIASound::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *);
void Atari2600::TIASound::set_sample_volume_range(std::int16_t range) {
per_channel_volume_ = range / 2;

View File

@ -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<TIASound, false> {
public:
TIASound(Concurrency::AsyncTaskQueue<false> &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 <Outputs::Speaker::Action action>
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<false> &audio_queue_;

View File

@ -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 <Outputs::Speaker::Action action>
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<action>(*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<action>(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<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *);
template void SoundGenerator::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *);
template void SoundGenerator::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *);
void SoundGenerator::set_divider(uint8_t divider) {
audio_queue_.enqueue([this, divider]() {

View File

@ -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<SoundGenerator, false> {
public:
SoundGenerator(Concurrency::AsyncTaskQueue<false> &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 <Outputs::Speaker::Action action>
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<false> &audio_queue_;

View File

@ -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<Frame *>(target);
template <Outputs::Speaker::Action action>
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<action>(target[c], output_level);
++c;
}
global_divider_ = global_divider_reload_;
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<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::StereoSample *);
template void Audio::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::StereoSample *);
template void Audio::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::StereoSample *);
// MARK: - Interrupt source

View File

@ -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<Audio, true> {
public:
Audio(Concurrency::AsyncTaskQueue<false> &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 <Outputs::Speaker::Action action>
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::StereoSample *target);
private:
Concurrency::AsyncTaskQueue<false> &audio_queue_;

View File

@ -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"

View File

@ -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"

View File

@ -1533,7 +1533,7 @@
4B680CE123A5553100451D43 /* 68000ComparativeTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = 68000ComparativeTests.mm; sourceTree = "<group>"; };
4B680CE323A555CA00451D43 /* 68000 Comparative Tests */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "68000 Comparative Tests"; sourceTree = "<group>"; };
4B683B002727BE6F0043E541 /* Amiga Blitter Tests */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "Amiga Blitter Tests"; sourceTree = "<group>"; };
4B698D1A1FE768A100696C91 /* SampleSource.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = SampleSource.hpp; sourceTree = "<group>"; };
4B698D1A1FE768A100696C91 /* BufferSource.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = BufferSource.hpp; sourceTree = "<group>"; };
4B69DEB52AB79E4F0055B217 /* Instruction.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Instruction.cpp; sourceTree = "<group>"; };
4B69FB3B1C4D908A00B5F0AA /* Tape.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Tape.cpp; sourceTree = "<group>"; };
4B69FB3C1C4D908A00B5F0AA /* Tape.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Tape.hpp; sourceTree = "<group>"; };
@ -4004,7 +4004,7 @@
isa = PBXGroup;
children = (
4B8EF6071FE5AF830076CCDD /* LowpassSpeaker.hpp */,
4B698D1A1FE768A100696C91 /* SampleSource.hpp */,
4B698D1A1FE768A100696C91 /* BufferSource.hpp */,
4B770A961FE9EE770026DC70 /* CompoundSource.hpp */,
);
path = Implementation;

View File

@ -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();

View File

@ -0,0 +1,153 @@
//
// SampleSource.hpp
// Clock Signal
//
// Created by Thomas Harte on 17/12/2017.
// Copyright 2017 Thomas Harte. All rights reserved.
//
#pragma once
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#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 <Action action, typename SampleT> 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 <Action action, typename IteratorT, typename SampleT> void fill(IteratorT begin, IteratorT end, SampleT value) {
switch(action) {
case Action::Mix:
while(begin != end) {
apply<action>(*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 <typename SourceT, bool stereo>
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 <Action action>
void apply_samples(std::size_t number_of_samples, typename SampleT<stereo>::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 <typename SourceT, bool stereo, int divider = 1>
struct SampleSource: public BufferSource<SourceT, stereo> {
public:
template <bool mix>
void apply_samples(std::size_t number_of_samples, typename SampleT<stereo>::type *target) {
const auto &source = *static_cast<SourceT *>(this);
if constexpr (divider == 1) {
while(number_of_samples--) {
apply<mix>(*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<mix>(target[c], level);
++c;
++master_divider_;
}
source.advance();
// Provide all full levels.
int whole_steps = (number_of_samples - c) / divider;
while(whole_steps--) {
fill<mix>(&target[c], &target[c + divider], source.level());
c += divider;
source.advance();
}
// Provide the head of a further partial capture.
level = source.level();
master_divider_ = number_of_samples - c;
fill<mix>(&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.
auto level() const {
return typename SampleT<stereo>::type();
}
void advance() {}
private:
int master_divider_{};
};
}

View File

@ -8,21 +8,126 @@
#pragma once
#include "SampleSource.hpp"
#include "BufferSource.hpp"
#include <algorithm>
#include <cassert>
#include <cstring>
#include <atomic>
namespace Outputs::Speaker {
/// @returns @c true if any of the templated sources is stereo; @c false otherwise.
template <typename... S> 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 <typename... S> 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 <typename... T> class CompoundSource:
public Outputs::Speaker::SampleSource {
public Outputs::Speaker::BufferSource<CompoundSource<T...>, ::Outputs::Speaker::is_stereo<T...>()> {
private:
template <typename... S> 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 <typename S, typename... R> class CompoundSourceHolder<S, R...> {
public:
CompoundSourceHolder(S &source, R &...next) : source_(source), next_source_(next...) {}
static constexpr bool is_stereo = S::is_stereo || CompoundSourceHolder<R...>::is_stereo;
template <Outputs::Speaker::Action action, bool output_stereo>
void apply_samples(std::size_t number_of_samples, typename ::Outputs::Speaker::SampleT<output_stereo>::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<Action::Store, false>(number_of_samples, conversion_source_.data());
// Map up and return.
for(std::size_t c = 0; c < number_of_samples; c++) {
Outputs::Speaker::apply<action>(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<action, output_stereo>(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<action>(target, target + number_of_samples, typename SampleT<output_stereo>::type());
}
return;
}
// Add in this component's output.
source_.template apply_samples<is_final_source ? Action::Store : Action::Mix>(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<R...>::size();
}
double total_scale(double *volumes) const {
return (volumes[0] / source_.average_output_peak()) + next_source_.total_scale(&volumes[1]);
}
private:
S &source_;
CompoundSourceHolder<R...> next_source_;
std::vector<MonoSample> conversion_source_;
};
public:
using Sample = typename SampleT<::Outputs::Speaker::is_stereo<T...>()>::type;
// To ensure at most one mono to stereo conversion, require appropriate source ordering.
static_assert(are_properly_ordered<T...>(), "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 <typename... T> class CompoundSource:
}
}
void get_samples(std::size_t number_of_samples, std::int16_t *target) {
source_holder_.template get_samples<get_is_stereo()>(number_of_samples, target);
}
void skip_samples(const std::size_t number_of_samples) {
source_holder_.skip_samples(number_of_samples);
template <Outputs::Speaker::Action action>
void apply_samples(std::size_t number_of_samples, Sample *target) {
source_holder_.template apply_samples<action, ::Outputs::Speaker::is_stereo<T...>()>(number_of_samples, target);
}
/*!
@ -59,16 +161,11 @@ template <typename... T> 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<T...>::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 <typename... T> class CompoundSource:
source_holder_.set_scaled_volume_range(volume_range_, volumes_.data(), scale);
}
template <typename... S> class CompoundSourceHolder: public Outputs::Speaker::SampleSource {
public:
template <bool output_stereo> 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 <typename S, typename... R> class CompoundSourceHolder<S, R...> {
public:
CompoundSourceHolder(S &source, R &...next) : source_(source), next_source_(next...) {}
template <bool output_stereo> void get_samples(std::size_t number_of_samples, std::int16_t *target) {
// Get the rest of the output.
next_source_.template get_samples<output_stereo>(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<R...>::size();
}
static constexpr bool get_is_stereo() {
return S::get_is_stereo() || CompoundSourceHolder<R...>::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<R...> next_source_;
};
CompoundSourceHolder<T...> source_holder_;
std::vector<double> volumes_;
int16_t volume_range_ = 0;

View File

@ -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 <bool is_stereo> class PushLowpass: public LowpassBase<PushLowpass<is_s
source of a high-frequency stream of audio which it filters down to a
lower-frequency output.
*/
template <typename SampleSource> class PullLowpass: public LowpassBase<PullLowpass<SampleSource>, SampleSource::get_is_stereo()> {
template <typename SampleSource> class PullLowpass: public LowpassBase<PullLowpass<SampleSource>, SampleSource::is_stereo> {
public:
PullLowpass(SampleSource &sample_source) : sample_source_(sample_source) {
// Propagate an initial volume level.
@ -362,7 +363,7 @@ template <typename SampleSource> class PullLowpass: public LowpassBase<PullLowpa
}
bool get_is_stereo() final {
return SampleSource::get_is_stereo();
return SampleSource::is_stereo;
}
/*!
@ -381,7 +382,7 @@ template <typename SampleSource> class PullLowpass: public LowpassBase<PullLowpa
}
private:
using BaseT = LowpassBase<PullLowpass<SampleSource>, SampleSource::get_is_stereo()>;
using BaseT = LowpassBase<PullLowpass<SampleSource>, SampleSource::is_stereo>;
friend BaseT;
using BaseT::process;
@ -396,15 +397,20 @@ template <typename SampleSource> class PullLowpass: public LowpassBase<PullLowpa
SampleSource &sample_source_;
void skip_samples(size_t count) {
sample_source_.skip_samples(count);
sample_source_.template apply_samples<Action::Ignore>(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<StereoSample *>(target);
sample_source_.template apply_samples<Action::Store>(length, stereo_target);
} else {
sample_source_.template apply_samples<Action::Store>(length, target);
}
}
};

View File

@ -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 <cstddef>
#include <cstdint>
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; }
};
}

View File

@ -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 <bool stereo> struct SampleT {
using type = std::conditional_t<stereo, StereoSample, MonoSample>;
};
/*!
Provides a communication point for sound; machines that have a speaker provide an
audio output.

View File

@ -793,7 +793,7 @@ template <typename BusHandler, bool uses_ready_line> void Processor<BusHandler,
assert(data_buffer_.size == 2 - m_flag());
++data_buffer_.value;
registers_.flags.set_nz(uint16_t(data_buffer_.value), registers_.m_shift);
break;;
break;
case DEC:
assert(data_buffer_.size == 2 - m_flag());

View File

@ -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;

View File

@ -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));