mirror of
https://github.com/TomHarte/CLK.git
synced 2024-12-25 03:32:01 +00:00
Merge pull request #756 from TomHarte/Stereo
Adds stereo sound processing.
This commit is contained in:
commit
aca41ac089
@ -37,12 +37,23 @@ float MultiSpeaker::get_ideal_clock_rate_in_range(float minimum, float maximum)
|
||||
return ideal / static_cast<float>(speakers_.size());
|
||||
}
|
||||
|
||||
void MultiSpeaker::set_computed_output_rate(float cycles_per_second, int buffer_size) {
|
||||
void MultiSpeaker::set_computed_output_rate(float cycles_per_second, int buffer_size, bool stereo) {
|
||||
stereo_output_ = stereo;
|
||||
for(const auto &speaker: speakers_) {
|
||||
speaker->set_computed_output_rate(cycles_per_second, buffer_size);
|
||||
speaker->set_computed_output_rate(cycles_per_second, buffer_size, stereo);
|
||||
}
|
||||
}
|
||||
|
||||
bool MultiSpeaker::get_is_stereo() {
|
||||
// Return as stereo if any subspeaker is stereo.
|
||||
for(const auto &speaker: speakers_) {
|
||||
if(speaker->get_is_stereo()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void MultiSpeaker::set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) {
|
||||
delegate_ = delegate;
|
||||
}
|
||||
@ -53,7 +64,7 @@ void MultiSpeaker::speaker_did_complete_samples(Speaker *speaker, const std::vec
|
||||
std::lock_guard<std::mutex> lock_guard(front_speaker_mutex_);
|
||||
if(speaker != front_speaker_) return;
|
||||
}
|
||||
did_complete_samples(this, buffer);
|
||||
did_complete_samples(this, buffer, stereo_output_);
|
||||
}
|
||||
|
||||
void MultiSpeaker::speaker_did_change_input_clock(Speaker *speaker) {
|
||||
|
@ -39,8 +39,9 @@ class MultiSpeaker: public Outputs::Speaker::Speaker, Outputs::Speaker::Speaker:
|
||||
|
||||
// Below is the standard Outputs::Speaker::Speaker interface; see there for documentation.
|
||||
float get_ideal_clock_rate_in_range(float minimum, float maximum) override;
|
||||
void set_computed_output_rate(float cycles_per_second, int buffer_size) override;
|
||||
void set_computed_output_rate(float cycles_per_second, int buffer_size, bool stereo) override;
|
||||
void set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) override;
|
||||
bool get_is_stereo() override;
|
||||
|
||||
private:
|
||||
void speaker_did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) final;
|
||||
@ -51,6 +52,8 @@ class MultiSpeaker: public Outputs::Speaker::Speaker, Outputs::Speaker::Speaker:
|
||||
Outputs::Speaker::Speaker *front_speaker_ = nullptr;
|
||||
Outputs::Speaker::Speaker::Delegate *delegate_ = nullptr;
|
||||
std::mutex front_speaker_mutex_;
|
||||
|
||||
bool stereo_output_ = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ class AudioGenerator: public ::Outputs::Speaker::SampleSource {
|
||||
void get_samples(std::size_t number_of_samples, int16_t *target);
|
||||
void skip_samples(std::size_t number_of_samples);
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
static constexpr bool get_is_stereo() { return false; }
|
||||
|
||||
private:
|
||||
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
|
||||
|
@ -6,13 +6,17 @@
|
||||
// Copyright 2016 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "AY38910.hpp"
|
||||
|
||||
#include <cmath>
|
||||
//namespace GI {
|
||||
//namespace AY38910 {
|
||||
|
||||
using namespace GI::AY38910;
|
||||
|
||||
AY38910::AY38910(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) {
|
||||
template <bool is_stereo>
|
||||
AY38910<is_stereo>::AY38910(Personality personality, Concurrency::DeferringAsyncTaskQueue &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;
|
||||
|
||||
@ -70,7 +74,7 @@ AY38910::AY38910(Personality personality, Concurrency::DeferringAsyncTaskQueue &
|
||||
set_sample_volume_range(0);
|
||||
}
|
||||
|
||||
void AY38910::set_sample_volume_range(std::int16_t range) {
|
||||
template <bool is_stereo> void AY38910<is_stereo>::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.
|
||||
@ -84,10 +88,20 @@ void AY38910::set_sample_volume_range(std::int16_t range) {
|
||||
for(int v = 31; v >= 0; --v) {
|
||||
volumes_[v] -= volumes_[0];
|
||||
}
|
||||
|
||||
evaluate_output_volume();
|
||||
}
|
||||
|
||||
void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) {
|
||||
template <bool is_stereo> void AY38910<is_stereo>::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);
|
||||
a_right_ = uint8_t(a_right * 255.0f);
|
||||
b_right_ = uint8_t(b_right * 255.0f);
|
||||
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) {
|
||||
// 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.
|
||||
@ -99,7 +113,11 @@ void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) {
|
||||
|
||||
std::size_t c = 0;
|
||||
while((master_divider_&3) && c < number_of_samples) {
|
||||
target[c] = output_volume_;
|
||||
if constexpr (is_stereo) {
|
||||
reinterpret_cast<uint32_t *>(target)[c] = output_volume_;
|
||||
} else {
|
||||
target[c] = int16_t(output_volume_);
|
||||
}
|
||||
master_divider_++;
|
||||
c++;
|
||||
}
|
||||
@ -141,7 +159,11 @@ void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) {
|
||||
evaluate_output_volume();
|
||||
|
||||
for(int ic = 0; ic < 4 && c < number_of_samples; ic++) {
|
||||
target[c] = output_volume_;
|
||||
if constexpr (is_stereo) {
|
||||
reinterpret_cast<uint32_t *>(target)[c] = output_volume_;
|
||||
} else {
|
||||
target[c] = int16_t(output_volume_);
|
||||
}
|
||||
c++;
|
||||
master_divider_++;
|
||||
}
|
||||
@ -150,7 +172,7 @@ void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) {
|
||||
master_divider_ &= 3;
|
||||
}
|
||||
|
||||
void AY38910::evaluate_output_volume() {
|
||||
template <bool is_stereo> void AY38910<is_stereo>::evaluate_output_volume() {
|
||||
int envelope_volume = envelope_shapes_[output_registers_[13]][envelope_position_ | envelope_position_mask_];
|
||||
|
||||
// The output level for a channel is:
|
||||
@ -190,26 +212,40 @@ void AY38910::evaluate_output_volume() {
|
||||
};
|
||||
#undef channel_volume
|
||||
|
||||
// Mix additively.
|
||||
output_volume_ = int16_t(
|
||||
volumes_[volumes[0]] * channel_levels[0] +
|
||||
volumes_[volumes[1]] * channel_levels[1] +
|
||||
volumes_[volumes[2]] * channel_levels[2]
|
||||
);
|
||||
// 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((
|
||||
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((
|
||||
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(
|
||||
volumes_[volumes[0]] * channel_levels[0] +
|
||||
volumes_[volumes[1]] * channel_levels[1] +
|
||||
volumes_[volumes[2]] * channel_levels[2]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool AY38910::is_zero_level() {
|
||||
template <bool is_stereo> bool AY38910<is_stereo>::is_zero_level() {
|
||||
// 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
|
||||
|
||||
void AY38910::select_register(uint8_t r) {
|
||||
template <bool is_stereo> void AY38910<is_stereo>::select_register(uint8_t r) {
|
||||
selected_register_ = r;
|
||||
}
|
||||
|
||||
void AY38910::set_register_value(uint8_t value) {
|
||||
template <bool is_stereo> void AY38910<is_stereo>::set_register_value(uint8_t value) {
|
||||
// There are only 16 registers.
|
||||
if(selected_register_ > 15) return;
|
||||
|
||||
@ -278,7 +314,7 @@ void AY38910::set_register_value(uint8_t value) {
|
||||
if(update_port_a) set_port_output(false);
|
||||
}
|
||||
|
||||
uint8_t AY38910::get_register_value() {
|
||||
template <bool is_stereo> uint8_t AY38910<is_stereo>::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] = {
|
||||
@ -292,24 +328,24 @@ uint8_t AY38910::get_register_value() {
|
||||
|
||||
// MARK: - Port querying
|
||||
|
||||
uint8_t AY38910::get_port_output(bool port_b) {
|
||||
template <bool is_stereo> uint8_t AY38910<is_stereo>::get_port_output(bool port_b) {
|
||||
return registers_[port_b ? 15 : 14];
|
||||
}
|
||||
|
||||
// MARK: - Bus handling
|
||||
|
||||
void AY38910::set_port_handler(PortHandler *handler) {
|
||||
template <bool is_stereo> void AY38910<is_stereo>::set_port_handler(PortHandler *handler) {
|
||||
port_handler_ = handler;
|
||||
set_port_output(true);
|
||||
set_port_output(false);
|
||||
}
|
||||
|
||||
void AY38910::set_data_input(uint8_t r) {
|
||||
template <bool is_stereo> void AY38910<is_stereo>::set_data_input(uint8_t r) {
|
||||
data_input_ = r;
|
||||
update_bus();
|
||||
}
|
||||
|
||||
void AY38910::set_port_output(bool port_b) {
|
||||
template <bool is_stereo> void AY38910<is_stereo>::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.
|
||||
@ -319,7 +355,7 @@ void AY38910::set_port_output(bool port_b) {
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t AY38910::get_data_output() {
|
||||
template <bool is_stereo> uint8_t AY38910<is_stereo>::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.
|
||||
@ -335,7 +371,7 @@ uint8_t AY38910::get_data_output() {
|
||||
return data_output_;
|
||||
}
|
||||
|
||||
void AY38910::set_control_lines(ControlLines control_lines) {
|
||||
template <bool is_stereo> void AY38910<is_stereo>::set_control_lines(ControlLines control_lines) {
|
||||
switch(int(control_lines)) {
|
||||
default: control_state_ = Inactive; break;
|
||||
|
||||
@ -350,7 +386,7 @@ void AY38910::set_control_lines(ControlLines control_lines) {
|
||||
update_bus();
|
||||
}
|
||||
|
||||
void AY38910::update_bus() {
|
||||
template <bool is_stereo> void AY38910<is_stereo>::update_bus() {
|
||||
// Assume no output, unless this turns out to be a read.
|
||||
data_output_ = 0xff;
|
||||
switch(control_state_) {
|
||||
@ -360,3 +396,7 @@ void AY38910::update_bus() {
|
||||
case Read: data_output_ = get_register_value(); break;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure both mono and stereo versions of the AY are built.
|
||||
template class GI::AY38910::AY38910<true>;
|
||||
template class GI::AY38910::AY38910<false>;
|
||||
|
@ -63,8 +63,10 @@ enum class Personality {
|
||||
Provides emulation of an AY-3-8910 / YM2149, which is a three-channel sound chip with a
|
||||
noise generator and a volume envelope generator, which also provides two bidirectional
|
||||
interface ports.
|
||||
|
||||
This AY has an attached mono or stereo mixer.
|
||||
*/
|
||||
class AY38910: public ::Outputs::Speaker::SampleSource {
|
||||
template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource {
|
||||
public:
|
||||
/// Creates a new AY38910.
|
||||
AY38910(Personality, Concurrency::DeferringAsyncTaskQueue &);
|
||||
@ -91,10 +93,23 @@ class AY38910: public ::Outputs::Speaker::SampleSource {
|
||||
*/
|
||||
void set_port_handler(PortHandler *);
|
||||
|
||||
/*!
|
||||
Enables or disables stereo output; if stereo output is enabled then also sets the weight of each of the AY's
|
||||
channels in each of the output channels.
|
||||
|
||||
If a_left_ = b_left = c_left = a_right = b_right = c_right = 1.0 then you'll get output that's effectively mono.
|
||||
|
||||
a_left = 0.0, a_right = 1.0 will make A full volume on the right output, and silent on the left.
|
||||
|
||||
a_left = 0.5, a_right = 0.5 will make A half volume on both outputs.
|
||||
*/
|
||||
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);
|
||||
bool is_zero_level();
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
static constexpr bool get_is_stereo() { return is_stereo; }
|
||||
|
||||
private:
|
||||
Concurrency::DeferringAsyncTaskQueue &task_queue_;
|
||||
@ -135,14 +150,21 @@ class AY38910: public ::Outputs::Speaker::SampleSource {
|
||||
|
||||
uint8_t data_input_, data_output_;
|
||||
|
||||
int16_t output_volume_;
|
||||
void evaluate_output_volume();
|
||||
uint32_t output_volume_;
|
||||
|
||||
void update_bus();
|
||||
PortHandler *port_handler_ = nullptr;
|
||||
void set_port_output(bool port_b);
|
||||
|
||||
void evaluate_output_volume();
|
||||
|
||||
// Output mixing control.
|
||||
uint8_t a_left_ = 255, a_right_ = 255;
|
||||
uint8_t b_left_ = 255, b_right_ = 255;
|
||||
uint8_t c_left_ = 255, c_right_ = 255;
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ class Toggle: public Outputs::Speaker::SampleSource {
|
||||
void get_samples(std::size_t number_of_samples, std::int16_t *target);
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
void skip_samples(const std::size_t number_of_samples);
|
||||
static constexpr bool get_is_stereo() { return false; }
|
||||
|
||||
void set_output(bool enabled);
|
||||
bool get_output();
|
||||
|
@ -32,6 +32,7 @@ class SCC: public ::Outputs::Speaker::SampleSource {
|
||||
/// As per ::SampleSource; provides audio output.
|
||||
void get_samples(std::size_t number_of_samples, std::int16_t *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);
|
||||
|
@ -32,6 +32,7 @@ class SN76489: public Outputs::Speaker::SampleSource {
|
||||
void get_samples(std::size_t number_of_samples, std::int16_t *target);
|
||||
bool is_zero_level();
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
static constexpr bool get_is_stereo() { return false; }
|
||||
|
||||
private:
|
||||
int master_divider_ = 0;
|
||||
|
@ -126,6 +126,9 @@ class AYDeferrer {
|
||||
/// Constructs a new AY instance and sets its clock rate.
|
||||
AYDeferrer() : ay_(GI::AY38910::Personality::AY38910, audio_queue_), speaker_(ay_) {
|
||||
speaker_.set_input_rate(1000000);
|
||||
// Per the CPC Wiki:
|
||||
// "A is output to the right, channel C is output left, and channel B is output to both left and right".
|
||||
ay_.set_output_mixing(0.0, 0.5, 1.0, 1.0, 0.5, 0.0);
|
||||
}
|
||||
|
||||
~AYDeferrer() {
|
||||
@ -153,14 +156,14 @@ class AYDeferrer {
|
||||
}
|
||||
|
||||
/// @returns the AY itself.
|
||||
GI::AY38910::AY38910 &ay() {
|
||||
GI::AY38910::AY38910<true> &ay() {
|
||||
return ay_;
|
||||
}
|
||||
|
||||
private:
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
GI::AY38910::AY38910 ay_;
|
||||
Outputs::Speaker::LowpassSpeaker<GI::AY38910::AY38910> speaker_;
|
||||
GI::AY38910::AY38910<true> ay_;
|
||||
Outputs::Speaker::LowpassSpeaker<GI::AY38910::AY38910<true>> speaker_;
|
||||
HalfCycles cycles_since_update_;
|
||||
};
|
||||
|
||||
|
@ -55,6 +55,7 @@ class Audio: public ::Outputs::Speaker::SampleSource {
|
||||
void get_samples(std::size_t number_of_samples, int16_t *target);
|
||||
bool is_zero_level();
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
constexpr static bool get_is_stereo() { return false; }
|
||||
|
||||
private:
|
||||
Concurrency::DeferringAsyncTaskQueue &task_queue_;
|
||||
|
@ -29,6 +29,7 @@ class TIASound: public Outputs::Speaker::SampleSource {
|
||||
// To satisfy ::SampleSource.
|
||||
void get_samples(std::size_t number_of_samples, int16_t *target);
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
static constexpr bool get_is_stereo() { return false; }
|
||||
|
||||
private:
|
||||
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
|
||||
|
@ -497,8 +497,8 @@ class ConcreteMachine:
|
||||
JustInTimeActor<Motorola::ACIA::ACIA, 16> midi_acia_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
GI::AY38910::AY38910 ay_;
|
||||
Outputs::Speaker::LowpassSpeaker<GI::AY38910::AY38910> speaker_;
|
||||
GI::AY38910::AY38910<false> ay_;
|
||||
Outputs::Speaker::LowpassSpeaker<GI::AY38910::AY38910<false>> speaker_;
|
||||
HalfCycles cycles_since_audio_update_;
|
||||
|
||||
JustInTimeActor<DMAController> dma_;
|
||||
|
@ -405,9 +405,9 @@ class ConcreteMachine:
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
TI::SN76489 sn76489_;
|
||||
GI::AY38910::AY38910 ay_;
|
||||
Outputs::Speaker::CompoundSource<TI::SN76489, GI::AY38910::AY38910> mixer_;
|
||||
Outputs::Speaker::LowpassSpeaker<Outputs::Speaker::CompoundSource<TI::SN76489, GI::AY38910::AY38910>> speaker_;
|
||||
GI::AY38910::AY38910<false> ay_;
|
||||
Outputs::Speaker::CompoundSource<TI::SN76489, GI::AY38910::AY38910<false>> mixer_;
|
||||
Outputs::Speaker::LowpassSpeaker<Outputs::Speaker::CompoundSource<TI::SN76489, GI::AY38910::AY38910<false>>> speaker_;
|
||||
|
||||
std::vector<uint8_t> bios_;
|
||||
std::vector<uint8_t> cartridge_;
|
||||
|
@ -28,6 +28,7 @@ class SoundGenerator: public ::Outputs::Speaker::SampleSource {
|
||||
void get_samples(std::size_t number_of_samples, int16_t *target);
|
||||
void skip_samples(std::size_t number_of_samples);
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
static constexpr bool get_is_stereo() { return false; }
|
||||
|
||||
private:
|
||||
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
|
||||
|
@ -760,11 +760,11 @@ class ConcreteMachine:
|
||||
Intel::i8255::i8255<i8255PortHandler> i8255_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
GI::AY38910::AY38910 ay_;
|
||||
GI::AY38910::AY38910<false> ay_;
|
||||
Audio::Toggle audio_toggle_;
|
||||
Konami::SCC scc_;
|
||||
Outputs::Speaker::CompoundSource<GI::AY38910::AY38910, Audio::Toggle, Konami::SCC> mixer_;
|
||||
Outputs::Speaker::LowpassSpeaker<Outputs::Speaker::CompoundSource<GI::AY38910::AY38910, Audio::Toggle, Konami::SCC>> speaker_;
|
||||
Outputs::Speaker::CompoundSource<GI::AY38910::AY38910<false>, Audio::Toggle, Konami::SCC> mixer_;
|
||||
Outputs::Speaker::LowpassSpeaker<Outputs::Speaker::CompoundSource<GI::AY38910::AY38910<false>, Audio::Toggle, Konami::SCC>> speaker_;
|
||||
|
||||
Storage::Tape::BinaryTapePlayer tape_player_;
|
||||
bool tape_player_is_sleeping_ = false;
|
||||
|
@ -43,6 +43,8 @@
|
||||
namespace Oric {
|
||||
|
||||
using DiskInterface = Analyser::Static::Oric::Target::DiskInterface;
|
||||
using AY = GI::AY38910::AY38910<false>;
|
||||
using Speaker = Outputs::Speaker::LowpassSpeaker<AY>;
|
||||
|
||||
enum ROM {
|
||||
BASIC10 = 0, BASIC11, Microdisc, Colour
|
||||
@ -146,7 +148,7 @@ class TapePlayer: public Storage::Tape::BinaryTapePlayer {
|
||||
*/
|
||||
class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
public:
|
||||
VIAPortHandler(Concurrency::DeferringAsyncTaskQueue &audio_queue, GI::AY38910::AY38910 &ay8910, Outputs::Speaker::LowpassSpeaker<GI::AY38910::AY38910> &speaker, TapePlayer &tape_player, Keyboard &keyboard) :
|
||||
VIAPortHandler(Concurrency::DeferringAsyncTaskQueue &audio_queue, AY &ay8910, Speaker &speaker, TapePlayer &tape_player, Keyboard &keyboard) :
|
||||
audio_queue_(audio_queue), ay8910_(ay8910), speaker_(speaker), tape_player_(tape_player), keyboard_(keyboard) {}
|
||||
|
||||
/*!
|
||||
@ -209,8 +211,8 @@ class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
HalfCycles cycles_since_ay_update_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
|
||||
GI::AY38910::AY38910 &ay8910_;
|
||||
Outputs::Speaker::LowpassSpeaker<GI::AY38910::AY38910> &speaker_;
|
||||
AY &ay8910_;
|
||||
Speaker &speaker_;
|
||||
TapePlayer &tape_player_;
|
||||
Keyboard &keyboard_;
|
||||
};
|
||||
@ -691,8 +693,8 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
|
||||
VideoOutput video_output_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
GI::AY38910::AY38910 ay8910_;
|
||||
Outputs::Speaker::LowpassSpeaker<GI::AY38910::AY38910> speaker_;
|
||||
GI::AY38910::AY38910<false> ay8910_;
|
||||
Speaker speaker_;
|
||||
|
||||
// Inputs
|
||||
Oric::KeyboardMapper keyboard_mapper_;
|
||||
|
@ -467,8 +467,9 @@ template<bool is_zx81> class ConcreteMachine:
|
||||
|
||||
// MARK: - Audio
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
GI::AY38910::AY38910 ay_;
|
||||
Outputs::Speaker::LowpassSpeaker<GI::AY38910::AY38910> speaker_;
|
||||
using AY = GI::AY38910::AY38910<false>;
|
||||
AY ay_;
|
||||
Outputs::Speaker::LowpassSpeaker<AY> speaker_;
|
||||
HalfCycles time_since_ay_update_;
|
||||
inline void ay_set_register(uint8_t value) {
|
||||
update_audio();
|
||||
|
@ -60,6 +60,10 @@
|
||||
argument = ""/Users/thomasharte/Library/Mobile Documents/com~apple~CloudDocs/Desktop/Soft/Master System/R-Type (NTSC).sms""
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = ""/Users/thomasharte/Library/Mobile Documents/com~apple~CloudDocs/Desktop/Soft/Amstrad CPC/Robocop.dsk""
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--speed=5"
|
||||
isEnabled = "NO">
|
||||
|
@ -25,10 +25,12 @@
|
||||
Creates an instance of CSAudioQueue.
|
||||
|
||||
@param samplingRate The output audio rate.
|
||||
@param isStereo @c YES if audio buffers will contain stereo audio, @c NO otherwise.
|
||||
|
||||
@returns An instance of CSAudioQueue if successful; @c nil otherwise.
|
||||
*/
|
||||
- (nonnull instancetype)initWithSamplingRate:(Float64)samplingRate;
|
||||
- (nonnull instancetype)initWithSamplingRate:(Float64)samplingRate isStereo:(BOOL)isStereo NS_DESIGNATED_INITIALIZER;
|
||||
- (nonnull instancetype)init __attribute((unavailable));
|
||||
|
||||
/*!
|
||||
Enqueues a buffer for playback.
|
||||
|
@ -73,7 +73,7 @@ static void audioOutputCallback(
|
||||
|
||||
#pragma mark - Standard object lifecycle
|
||||
|
||||
- (instancetype)initWithSamplingRate:(Float64)samplingRate {
|
||||
- (instancetype)initWithSamplingRate:(Float64)samplingRate isStereo:(BOOL)isStereo {
|
||||
self = [super init];
|
||||
|
||||
if(self) {
|
||||
@ -98,10 +98,11 @@ static void audioOutputCallback(
|
||||
outputDescription.mFormatID = kAudioFormatLinearPCM;
|
||||
outputDescription.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;
|
||||
|
||||
outputDescription.mBytesPerPacket = 2;
|
||||
|
||||
outputDescription.mChannelsPerFrame = isStereo ? 2 : 1;
|
||||
outputDescription.mFramesPerPacket = 1;
|
||||
outputDescription.mBytesPerFrame = 2;
|
||||
outputDescription.mChannelsPerFrame = 1;
|
||||
outputDescription.mBytesPerFrame = 2 * outputDescription.mChannelsPerFrame;
|
||||
outputDescription.mBytesPerPacket = outputDescription.mBytesPerFrame * outputDescription.mFramesPerPacket;
|
||||
outputDescription.mBitsPerChannel = 16;
|
||||
|
||||
outputDescription.mReserved = 0;
|
||||
@ -123,10 +124,6 @@ static void audioOutputCallback(
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
return [self initWithSamplingRate:[[self class] preferredSamplingRate]];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[CSAudioQueueDeallocLock lock];
|
||||
if(_audioQueue) {
|
||||
|
@ -228,11 +228,12 @@ class MachineDocument:
|
||||
// TODO: this needs to be threadsafe. FIX!
|
||||
let maximumSamplingRate = CSAudioQueue.preferredSamplingRate()
|
||||
let selectedSamplingRate = self.machine.idealSamplingRate(from: NSRange(location: 0, length: NSInteger(maximumSamplingRate)))
|
||||
let isStereo = self.machine.isStereo()
|
||||
if selectedSamplingRate > 0 {
|
||||
self.audioQueue = CSAudioQueue(samplingRate: Float64(selectedSamplingRate))
|
||||
self.audioQueue = CSAudioQueue(samplingRate: Float64(selectedSamplingRate), isStereo:isStereo)
|
||||
self.audioQueue.delegate = self
|
||||
self.machine.audioQueue = self.audioQueue
|
||||
self.machine.setAudioSamplingRate(selectedSamplingRate, bufferSize:audioQueue.preferredBufferSize)
|
||||
self.machine.setAudioSamplingRate(selectedSamplingRate, bufferSize:audioQueue.preferredBufferSize, stereo:isStereo)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,7 +58,8 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
|
||||
- (nullable instancetype)initWithAnalyser:(nonnull CSStaticAnalyser *)result missingROMs:(nullable inout NSMutableArray<CSMissingROM *> *)missingROMs NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
- (float)idealSamplingRateFromRange:(NSRange)range;
|
||||
- (void)setAudioSamplingRate:(float)samplingRate bufferSize:(NSUInteger)bufferSize;
|
||||
- (BOOL)isStereo;
|
||||
- (void)setAudioSamplingRate:(float)samplingRate bufferSize:(NSUInteger)bufferSize stereo:(BOOL)stereo;
|
||||
|
||||
- (void)setView:(nullable CSOpenGLView *)view aspectRatio:(float)aspectRatio;
|
||||
|
||||
|
@ -262,17 +262,27 @@ struct ActivityObserver: public Activity::Observer {
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setAudioSamplingRate:(float)samplingRate bufferSize:(NSUInteger)bufferSize {
|
||||
@synchronized(self) {
|
||||
[self setSpeakerDelegate:&_speakerDelegate sampleRate:samplingRate bufferSize:bufferSize];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)setSpeakerDelegate:(Outputs::Speaker::Speaker::Delegate *)delegate sampleRate:(float)sampleRate bufferSize:(NSUInteger)bufferSize {
|
||||
- (BOOL)isStereo {
|
||||
@synchronized(self) {
|
||||
Outputs::Speaker::Speaker *speaker = _machine->crt_machine()->get_speaker();
|
||||
if(speaker) {
|
||||
speaker->set_output_rate(sampleRate, (int)bufferSize);
|
||||
return speaker->get_is_stereo();
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setAudioSamplingRate:(float)samplingRate bufferSize:(NSUInteger)bufferSize stereo:(BOOL)stereo {
|
||||
@synchronized(self) {
|
||||
[self setSpeakerDelegate:&_speakerDelegate sampleRate:samplingRate bufferSize:bufferSize stereo:stereo];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)setSpeakerDelegate:(Outputs::Speaker::Speaker::Delegate *)delegate sampleRate:(float)sampleRate bufferSize:(NSUInteger)bufferSize stereo:(BOOL)stereo {
|
||||
@synchronized(self) {
|
||||
Outputs::Speaker::Speaker *speaker = _machine->crt_machine()->get_speaker();
|
||||
if(speaker) {
|
||||
speaker->set_output_rate(sampleRate, (int)bufferSize, stereo);
|
||||
speaker->set_delegate(delegate);
|
||||
return YES;
|
||||
}
|
||||
|
@ -168,10 +168,12 @@ struct MachineRunner {
|
||||
|
||||
struct SpeakerDelegate: public Outputs::Speaker::Speaker::Delegate {
|
||||
// This is empirically the best that I can seem to do with SDL's timer precision.
|
||||
static constexpr int buffer_size = 1024;
|
||||
static constexpr size_t buffered_samples = 1024;
|
||||
bool is_stereo = false;
|
||||
|
||||
void speaker_did_complete_samples(Outputs::Speaker::Speaker *speaker, const std::vector<int16_t> &buffer) final {
|
||||
std::lock_guard<std::mutex> lock_guard(audio_buffer_mutex_);
|
||||
const size_t buffer_size = buffered_samples * (is_stereo ? 2 : 1);
|
||||
if(audio_buffer_.size() > buffer_size) {
|
||||
audio_buffer_.erase(audio_buffer_.begin(), audio_buffer_.end() - buffer_size);
|
||||
}
|
||||
@ -181,9 +183,10 @@ struct SpeakerDelegate: public Outputs::Speaker::Speaker::Delegate {
|
||||
void audio_callback(Uint8 *stream, int len) {
|
||||
std::lock_guard<std::mutex> lock_guard(audio_buffer_mutex_);
|
||||
|
||||
std::size_t sample_length = static_cast<std::size_t>(len) / sizeof(int16_t);
|
||||
std::size_t copy_length = std::min(sample_length, audio_buffer_.size());
|
||||
int16_t *target = static_cast<int16_t *>(static_cast<void *>(stream));
|
||||
// SDL buffer length is in bytes, so there's no need to adjust for stereo/mono in here.
|
||||
const std::size_t sample_length = static_cast<std::size_t>(len) / sizeof(int16_t);
|
||||
const std::size_t copy_length = std::min(sample_length, audio_buffer_.size());
|
||||
int16_t *const target = static_cast<int16_t *>(static_cast<void *>(stream));
|
||||
|
||||
std::memcpy(stream, audio_buffer_.data(), copy_length * sizeof(int16_t));
|
||||
if(copy_length < sample_length) {
|
||||
@ -660,14 +663,15 @@ int main(int argc, char *argv[]) {
|
||||
SDL_zero(desired_audio_spec);
|
||||
desired_audio_spec.freq = 48000; // TODO: how can I get SDL to reveal the output rate of this machine?
|
||||
desired_audio_spec.format = AUDIO_S16;
|
||||
desired_audio_spec.channels = 1;
|
||||
desired_audio_spec.samples = SpeakerDelegate::buffer_size;
|
||||
desired_audio_spec.channels = 1 + int(speaker->get_is_stereo());
|
||||
desired_audio_spec.samples = Uint16(SpeakerDelegate::buffered_samples);
|
||||
desired_audio_spec.callback = SpeakerDelegate::SDL_audio_callback;
|
||||
desired_audio_spec.userdata = &speaker_delegate;
|
||||
|
||||
speaker_delegate.audio_device = SDL_OpenAudioDevice(nullptr, 0, &desired_audio_spec, &obtained_audio_spec, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE);
|
||||
|
||||
speaker->set_output_rate(obtained_audio_spec.freq, desired_audio_spec.samples);
|
||||
speaker->set_output_rate(obtained_audio_spec.freq, desired_audio_spec.samples, obtained_audio_spec.channels == 2);
|
||||
speaker_delegate.is_stereo = obtained_audio_spec.channels == 2;
|
||||
speaker->set_delegate(&speaker_delegate);
|
||||
SDL_PauseAudioDevice(speaker_delegate.audio_device, 0);
|
||||
}
|
||||
@ -865,23 +869,22 @@ int main(int argc, char *argv[]) {
|
||||
SDL_FreeSurface(surface);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Syphon off alt+enter (toggle full-screen) upon key up only; this was previously a key down action,
|
||||
// but the SDL_KEYDOWN announcement was found to be reposted after changing graphics mode on some
|
||||
// systems, causing a loop of changes, so key up is safer.
|
||||
if(event.type == SDL_KEYUP && event.key.keysym.sym == SDLK_RETURN && (SDL_GetModState()&KMOD_ALT)) {
|
||||
fullscreen_mode ^= SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
SDL_SetWindowFullscreen(window, fullscreen_mode);
|
||||
SDL_ShowCursor((fullscreen_mode&SDL_WINDOW_FULLSCREEN_DESKTOP) ? SDL_DISABLE : SDL_ENABLE);
|
||||
|
||||
// Syphon off alt+enter (toggle full-screen) upon key up only; this was previously a key down action,
|
||||
// but the SDL_KEYDOWN announcement was found to be reposted after changing graphics mode on some
|
||||
// systems so key up is safer.
|
||||
if(event.type == SDL_KEYUP && event.key.keysym.sym == SDLK_RETURN && (SDL_GetModState()&KMOD_ALT)) {
|
||||
fullscreen_mode ^= SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
SDL_SetWindowFullscreen(window, fullscreen_mode);
|
||||
SDL_ShowCursor((fullscreen_mode&SDL_WINDOW_FULLSCREEN_DESKTOP) ? SDL_DISABLE : SDL_ENABLE);
|
||||
|
||||
// Announce a potential discontinuity in keyboard input.
|
||||
const auto keyboard_machine = machine->keyboard_machine();
|
||||
if(keyboard_machine) {
|
||||
keyboard_machine->get_keyboard().reset_all_keys();
|
||||
}
|
||||
break;
|
||||
// Announce a potential discontinuity in keyboard input.
|
||||
const auto keyboard_machine = machine->keyboard_machine();
|
||||
if(keyboard_machine) {
|
||||
keyboard_machine->get_keyboard().reset_all_keys();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const bool is_pressed = event.type == SDL_KEYDOWN;
|
||||
|
@ -33,7 +33,7 @@ template <typename... T> class CompoundSource:
|
||||
}
|
||||
|
||||
void get_samples(std::size_t number_of_samples, std::int16_t *target) {
|
||||
source_holder_.get_samples(number_of_samples, target);
|
||||
source_holder_.template get_samples<get_is_stereo()>(number_of_samples, target);
|
||||
}
|
||||
|
||||
void skip_samples(const std::size_t number_of_samples) {
|
||||
@ -57,6 +57,8 @@ template <typename... T> class CompoundSource:
|
||||
push_volumes();
|
||||
}
|
||||
|
||||
static constexpr bool get_is_stereo() { return CompoundSourceHolder<T...>::get_is_stereo(); }
|
||||
|
||||
private:
|
||||
void push_volumes() {
|
||||
source_holder_.set_scaled_volume_range(volume_range_, volumes_.data());
|
||||
@ -64,7 +66,7 @@ template <typename... T> class CompoundSource:
|
||||
|
||||
template <typename... S> class CompoundSourceHolder: public Outputs::Speaker::SampleSource {
|
||||
public:
|
||||
void get_samples(std::size_t number_of_samples, std::int16_t *target) {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -73,24 +75,49 @@ template <typename... T> class CompoundSource:
|
||||
std::size_t size() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static constexpr bool get_is_stereo() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename S, typename... R> class CompoundSourceHolder<S, R...> {
|
||||
public:
|
||||
CompoundSourceHolder(S &source, R &...next) : source_(source), next_source_(next...) {}
|
||||
|
||||
void get_samples(std::size_t number_of_samples, std::int16_t *target) {
|
||||
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);
|
||||
next_source_.get_samples(number_of_samples, target);
|
||||
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 {
|
||||
int16_t next_samples[number_of_samples];
|
||||
next_source_.get_samples(number_of_samples, next_samples);
|
||||
source_.get_samples(number_of_samples, target);
|
||||
while(number_of_samples--) {
|
||||
target[number_of_samples] += next_samples[number_of_samples];
|
||||
// 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) {
|
||||
@ -107,6 +134,10 @@ template <typename... T> class CompoundSource:
|
||||
return 1+next_source_.size();
|
||||
}
|
||||
|
||||
static constexpr bool get_is_stereo() {
|
||||
return S::get_is_stereo() || CompoundSourceHolder<R...>::get_is_stereo();
|
||||
}
|
||||
|
||||
private:
|
||||
S &source_;
|
||||
CompoundSourceHolder<R...> next_source_;
|
||||
|
@ -28,9 +28,9 @@ namespace Speaker {
|
||||
source of a high-frequency stream of audio which it filters down to a
|
||||
lower-frequency output.
|
||||
*/
|
||||
template <typename T> class LowpassSpeaker: public Speaker {
|
||||
template <typename SampleSource> class LowpassSpeaker: public Speaker {
|
||||
public:
|
||||
LowpassSpeaker(T &sample_source) : sample_source_(sample_source) {
|
||||
LowpassSpeaker(SampleSource &sample_source) : sample_source_(sample_source) {
|
||||
sample_source.set_sample_volume_range(32767);
|
||||
}
|
||||
|
||||
@ -58,7 +58,7 @@ template <typename T> class LowpassSpeaker: public Speaker {
|
||||
}
|
||||
|
||||
// Implemented as per Speaker.
|
||||
void set_computed_output_rate(float cycles_per_second, int buffer_size) final {
|
||||
void set_computed_output_rate(float cycles_per_second, int buffer_size, bool stereo) final {
|
||||
std::lock_guard<std::mutex> lock_guard(filter_parameters_mutex_);
|
||||
if(filter_parameters_.output_cycles_per_second == cycles_per_second && size_t(buffer_size) == output_buffer_.size()) {
|
||||
return;
|
||||
@ -66,7 +66,11 @@ template <typename T> class LowpassSpeaker: public Speaker {
|
||||
|
||||
filter_parameters_.output_cycles_per_second = cycles_per_second;
|
||||
filter_parameters_.parameters_are_dirty = true;
|
||||
output_buffer_.resize(std::size_t(buffer_size));
|
||||
output_buffer_.resize(std::size_t(buffer_size) * (SampleSource::get_is_stereo() ? 2 : 1));
|
||||
}
|
||||
|
||||
bool get_is_stereo() final {
|
||||
return SampleSource::get_is_stereo();
|
||||
}
|
||||
|
||||
/*!
|
||||
@ -140,15 +144,14 @@ template <typename T> class LowpassSpeaker: public Speaker {
|
||||
switch(conversion_) {
|
||||
case Conversion::Copy:
|
||||
while(cycles_remaining) {
|
||||
const auto cycles_to_read = std::min(output_buffer_.size() - output_buffer_pointer_, cycles_remaining);
|
||||
const auto cycles_to_read = std::min((output_buffer_.size() - output_buffer_pointer_) / (SampleSource::get_is_stereo() ? 2 : 1), cycles_remaining);
|
||||
sample_source_.get_samples(cycles_to_read, &output_buffer_[output_buffer_pointer_ ]);
|
||||
output_buffer_pointer_ += cycles_to_read * (SampleSource::get_is_stereo() ? 2 : 1);
|
||||
|
||||
sample_source_.get_samples(cycles_to_read, &output_buffer_[output_buffer_pointer_]);
|
||||
output_buffer_pointer_ += cycles_to_read;
|
||||
|
||||
// announce to delegate if full
|
||||
// Announce to delegate if full.
|
||||
if(output_buffer_pointer_ == output_buffer_.size()) {
|
||||
output_buffer_pointer_ = 0;
|
||||
did_complete_samples(this, output_buffer_);
|
||||
did_complete_samples(this, output_buffer_, SampleSource::get_is_stereo());
|
||||
}
|
||||
|
||||
cycles_remaining -= cycles_to_read;
|
||||
@ -157,14 +160,16 @@ template <typename T> class LowpassSpeaker: public Speaker {
|
||||
|
||||
case Conversion::ResampleSmaller:
|
||||
while(cycles_remaining) {
|
||||
const auto cycles_to_read = std::min(cycles_remaining, input_buffer_.size() - input_buffer_depth_);
|
||||
const auto cycles_to_read = std::min((input_buffer_.size() - input_buffer_depth_) / (SampleSource::get_is_stereo() ? 2 : 1), cycles_remaining);
|
||||
|
||||
sample_source_.get_samples(cycles_to_read, &input_buffer_[input_buffer_depth_]);
|
||||
cycles_remaining -= cycles_to_read;
|
||||
input_buffer_depth_ += cycles_to_read;
|
||||
input_buffer_depth_ += cycles_to_read * (SampleSource::get_is_stereo() ? 2 : 1);
|
||||
|
||||
if(input_buffer_depth_ == input_buffer_.size()) {
|
||||
resample_input_buffer();
|
||||
}
|
||||
|
||||
cycles_remaining -= cycles_to_read;
|
||||
}
|
||||
break;
|
||||
|
||||
@ -174,7 +179,7 @@ template <typename T> class LowpassSpeaker: public Speaker {
|
||||
}
|
||||
}
|
||||
|
||||
T &sample_source_;
|
||||
SampleSource &sample_source_;
|
||||
|
||||
std::size_t output_buffer_pointer_ = 0;
|
||||
std::size_t input_buffer_depth_ = 0;
|
||||
@ -239,35 +244,42 @@ template <typename T> class LowpassSpeaker: public Speaker {
|
||||
// that means nothing to do.
|
||||
default: break;
|
||||
|
||||
case Conversion::ResampleSmaller:
|
||||
case Conversion::ResampleSmaller: {
|
||||
// Reize the input buffer only if absolutely necessary; if sizing downward
|
||||
// such that a sample would otherwise be lost then output it now. Keep anything
|
||||
// currently in the input buffer that hasn't yet been processed.
|
||||
if(input_buffer_.size() != size_t(number_of_taps)) {
|
||||
if(input_buffer_depth_ >= size_t(number_of_taps)) {
|
||||
const size_t required_buffer_size = size_t(number_of_taps) * (SampleSource::get_is_stereo() ? 2 : 1);
|
||||
if(input_buffer_.size() != required_buffer_size) {
|
||||
if(input_buffer_depth_ >= required_buffer_size) {
|
||||
resample_input_buffer();
|
||||
input_buffer_depth_ %= size_t(number_of_taps);
|
||||
input_buffer_depth_ %= required_buffer_size;
|
||||
}
|
||||
input_buffer_.resize(size_t(number_of_taps));
|
||||
input_buffer_.resize(required_buffer_size);
|
||||
}
|
||||
break;
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
inline void resample_input_buffer() {
|
||||
output_buffer_[output_buffer_pointer_] = filter_->apply(input_buffer_.data());
|
||||
output_buffer_pointer_++;
|
||||
if constexpr (SampleSource::get_is_stereo()) {
|
||||
output_buffer_[output_buffer_pointer_ + 0] = filter_->apply(input_buffer_.data(), 2);
|
||||
output_buffer_[output_buffer_pointer_ + 1] = filter_->apply(input_buffer_.data() + 1, 2);
|
||||
output_buffer_pointer_+= 2;
|
||||
} else {
|
||||
output_buffer_[output_buffer_pointer_] = filter_->apply(input_buffer_.data());
|
||||
output_buffer_pointer_++;
|
||||
}
|
||||
|
||||
// Announce to delegate if full.
|
||||
if(output_buffer_pointer_ == output_buffer_.size()) {
|
||||
output_buffer_pointer_ = 0;
|
||||
did_complete_samples(this, output_buffer_);
|
||||
did_complete_samples(this, output_buffer_, SampleSource::get_is_stereo());
|
||||
}
|
||||
|
||||
// If the next loop around is going to reuse some of the samples just collected, use a memmove to
|
||||
// preserve them in the correct locations (TODO: use a longer buffer to fix that?) and don't skip
|
||||
// anything. Otherwise skip as required to get to the next sample batch and don't expect to reuse.
|
||||
const auto steps = stepper_->step();
|
||||
const auto steps = stepper_->step() * (SampleSource::get_is_stereo() ? 2 : 1);
|
||||
if(steps < input_buffer_.size()) {
|
||||
auto *const input_buffer = input_buffer_.data();
|
||||
std::memmove( input_buffer,
|
||||
@ -275,8 +287,9 @@ template <typename T> class LowpassSpeaker: public Speaker {
|
||||
sizeof(int16_t) * (input_buffer_.size() - steps));
|
||||
input_buffer_depth_ -= steps;
|
||||
} else {
|
||||
if(steps > input_buffer_.size())
|
||||
sample_source_.skip_samples(steps - input_buffer_.size());
|
||||
if(steps > input_buffer_.size()) {
|
||||
sample_source_.skip_samples((steps - input_buffer_.size()) / (SampleSource::get_is_stereo() ? 2 : 1));
|
||||
}
|
||||
input_buffer_depth_ = 0;
|
||||
}
|
||||
}
|
||||
|
@ -23,33 +23,90 @@ class Speaker {
|
||||
public:
|
||||
virtual ~Speaker() {}
|
||||
|
||||
/*!
|
||||
@returns The best output clock rate for the audio being supplied to this speaker, from the range given.
|
||||
*/
|
||||
virtual float get_ideal_clock_rate_in_range(float minimum, float maximum) = 0;
|
||||
void set_output_rate(float cycles_per_second, int buffer_size) {
|
||||
|
||||
/*!
|
||||
@returns @c true if the device would most ideally output stereo sound; @c false otherwise.
|
||||
*/
|
||||
virtual bool get_is_stereo() = 0;
|
||||
|
||||
/*!
|
||||
Sets the actual output rate; packets provided to the delegate will conform to these
|
||||
specifications regardless of the input.
|
||||
*/
|
||||
void set_output_rate(float cycles_per_second, int buffer_size, bool stereo) {
|
||||
output_cycles_per_second_ = cycles_per_second;
|
||||
output_buffer_size_ = buffer_size;
|
||||
stereo_output_ = stereo;
|
||||
compute_output_rate();
|
||||
}
|
||||
|
||||
/*!
|
||||
Speeds a speed multiplier for this machine, e.g. that it is currently being run at 2.0x its normal rate.
|
||||
This will affect the number of input samples that are combined to produce one output sample.
|
||||
*/
|
||||
void set_input_rate_multiplier(float multiplier) {
|
||||
input_rate_multiplier_ = multiplier;
|
||||
compute_output_rate();
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns The number of sample sets so far delivered to the delegate.
|
||||
*/
|
||||
int completed_sample_sets() const { return completed_sample_sets_; }
|
||||
|
||||
/*!
|
||||
Defines a receiver for audio packets.
|
||||
*/
|
||||
struct Delegate {
|
||||
/*!
|
||||
Indicates that a new audio packet is ready. If the output is stereo, samples will be interleaved with the first
|
||||
being left, the second being right, etc.
|
||||
*/
|
||||
virtual void speaker_did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) = 0;
|
||||
|
||||
/*!
|
||||
Provides the delegate with a hint that the input clock rate has changed, which provides an opportunity to
|
||||
renegotiate the ideal clock rate, if desired.
|
||||
*/
|
||||
virtual void speaker_did_change_input_clock(Speaker *speaker) {}
|
||||
};
|
||||
virtual void set_delegate(Delegate *delegate) {
|
||||
delegate_ = delegate;
|
||||
}
|
||||
|
||||
virtual void set_computed_output_rate(float cycles_per_second, int buffer_size) = 0;
|
||||
virtual void set_computed_output_rate(float cycles_per_second, int buffer_size, bool stereo) = 0;
|
||||
|
||||
protected:
|
||||
void did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) {
|
||||
void did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer, bool is_stereo) {
|
||||
++completed_sample_sets_;
|
||||
delegate_->speaker_did_complete_samples(this, buffer);
|
||||
|
||||
// Hope for the fast path first: producer and consumer agree about
|
||||
// number of channels.
|
||||
if(is_stereo == stereo_output_) {
|
||||
delegate_->speaker_did_complete_samples(this, buffer);
|
||||
return;
|
||||
}
|
||||
|
||||
// Producer and consumer don't agree, so mix two channels to one, or double out one to two.
|
||||
if(is_stereo) {
|
||||
// Mix down.
|
||||
mix_buffer_.resize(buffer.size() / 2);
|
||||
for(size_t c = 0; c < mix_buffer_.size(); ++c) {
|
||||
mix_buffer_[c] = (buffer[(c << 1) + 0] + buffer[(c << 1) + 1]) >> 1;
|
||||
// TODO: is there an Accelerate framework solution to this?
|
||||
}
|
||||
} else {
|
||||
// Double up.
|
||||
mix_buffer_.resize(buffer.size() * 2);
|
||||
for(size_t c = 0; c < buffer.size(); ++c) {
|
||||
mix_buffer_[(c << 1) + 0] = mix_buffer_[(c << 1) + 1] = buffer[c];
|
||||
}
|
||||
}
|
||||
delegate_->speaker_did_complete_samples(this, mix_buffer_);
|
||||
}
|
||||
Delegate *delegate_ = nullptr;
|
||||
|
||||
@ -57,13 +114,15 @@ class Speaker {
|
||||
void compute_output_rate() {
|
||||
// The input rate multiplier is actually used as an output rate divider,
|
||||
// to confirm to the public interface of a generic speaker being output-centric.
|
||||
set_computed_output_rate(output_cycles_per_second_ / input_rate_multiplier_, output_buffer_size_);
|
||||
set_computed_output_rate(output_cycles_per_second_ / input_rate_multiplier_, output_buffer_size_, stereo_output_);
|
||||
}
|
||||
|
||||
int completed_sample_sets_ = 0;
|
||||
float input_rate_multiplier_ = 1.0f;
|
||||
float output_cycles_per_second_ = 1.0f;
|
||||
int output_buffer_size_ = 1;
|
||||
bool stereo_output_ = false;
|
||||
std::vector<int16_t> mix_buffer_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <Accelerate/Accelerate.h>
|
||||
#define USE_ACCELERATE
|
||||
#endif
|
||||
|
||||
#include <vector>
|
||||
@ -49,15 +50,15 @@ class FIRFilter {
|
||||
@param src The source buffer to apply the filter to.
|
||||
@returns The result of applying the filter.
|
||||
*/
|
||||
inline short apply(const short *src) const {
|
||||
#ifdef __APPLE__
|
||||
inline short apply(const short *src, size_t stride = 1) const {
|
||||
#ifdef USE_ACCELERATE
|
||||
short result;
|
||||
vDSP_dotpr_s1_15(filter_coefficients_.data(), 1, src, 1, &result, filter_coefficients_.size());
|
||||
vDSP_dotpr_s1_15(filter_coefficients_.data(), 1, src, vDSP_Stride(stride), &result, filter_coefficients_.size());
|
||||
return result;
|
||||
#else
|
||||
int outputValue = 0;
|
||||
for(std::size_t c = 0; c < filter_coefficients_.size(); ++c) {
|
||||
outputValue += filter_coefficients_[c] * src[c];
|
||||
outputValue += filter_coefficients_[c] * src[c * stride];
|
||||
}
|
||||
return static_cast<short>(outputValue >> FixedShift);
|
||||
#endif
|
||||
|
Loading…
Reference in New Issue
Block a user