1
0
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:
Thomas Harte 2020-02-16 19:20:21 -05:00 committed by GitHub
commit aca41ac089
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 345 additions and 134 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -60,6 +60,10 @@
argument = "&quot;/Users/thomasharte/Library/Mobile Documents/com~apple~CloudDocs/Desktop/Soft/Master System/R-Type (NTSC).sms&quot;"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "&quot;/Users/thomasharte/Library/Mobile Documents/com~apple~CloudDocs/Desktop/Soft/Amstrad CPC/Robocop.dsk&quot;"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--speed=5"
isEnabled = "NO">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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