1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-06-25 18:30:07 +00:00

Fixed: individual audio generators now either are or are not stereo. The speaker acts accordingly.

This commit is contained in:
Thomas Harte 2020-02-16 18:28:03 -05:00
parent 5242362f31
commit 9835e800ec
8 changed files with 58 additions and 63 deletions

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_;
@ -433,7 +434,7 @@ template <class BusHandler> class MOS6560 {
Concurrency::DeferringAsyncTaskQueue audio_queue_;
AudioGenerator audio_generator_;
Outputs::Speaker::LowpassSpeaker<AudioGenerator, false> speaker_;
Outputs::Speaker::LowpassSpeaker<AudioGenerator> speaker_;
Cycles cycles_since_speaker_update_;
void update_audio() {

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.
@ -85,15 +89,10 @@ void AY38910::set_sample_volume_range(std::int16_t range) {
volumes_[v] -= volumes_[0];
}
if(is_stereo_) {
evaluate_output_volume<true>();
} else {
evaluate_output_volume<false>();
}
evaluate_output_volume();
}
void AY38910::set_output_mixing(bool is_stereo, float a_left, float b_left, float c_left, float a_right, float b_right, float c_right) {
is_stereo_ = is_stereo;
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);
@ -102,15 +101,7 @@ void AY38910::set_output_mixing(bool is_stereo, float a_left, float b_left, floa
c_right_ = uint8_t(c_right * 255.0f);
}
void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) {
if(is_stereo_) {
get_samples<true>(number_of_samples, target);
} else {
get_samples<false>(number_of_samples, target);
}
}
template <bool is_stereo> void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) {
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.
@ -165,7 +156,7 @@ template <bool is_stereo> void AY38910::get_samples(std::size_t number_of_sample
if(envelope_position_ == 64) envelope_position_ = envelope_overflow_masks_[output_registers_[13]];
}
evaluate_output_volume<is_stereo>();
evaluate_output_volume();
for(int ic = 0; ic < 4 && c < number_of_samples; ic++) {
if constexpr (is_stereo) {
@ -181,7 +172,7 @@ template <bool is_stereo> void AY38910::get_samples(std::size_t number_of_sample
master_divider_ &= 3;
}
template <bool is_stereo> 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:
@ -243,18 +234,18 @@ template <bool is_stereo> void AY38910::evaluate_output_volume() {
}
}
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;
@ -297,11 +288,7 @@ void AY38910::set_register_value(uint8_t value) {
// Store a copy of the current register within the storage used by the audio generation
// thread, and apply any changes to output volume.
output_registers_[selected_register] = masked_value;
if(is_stereo_) {
evaluate_output_volume<true>();
} else {
evaluate_output_volume<false>();
}
evaluate_output_volume();
});
}
@ -327,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] = {
@ -341,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.
@ -368,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.
@ -384,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;
@ -399,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_) {
@ -409,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

@ -66,7 +66,7 @@ enum class Personality {
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 &);
@ -103,12 +103,13 @@ class AY38910: public ::Outputs::Speaker::SampleSource {
a_left = 0.5, a_right = 0.5 will make A half volume on both outputs.
*/
void set_output_mixing(bool is_stereo, float a_left, float b_left, float c_left, float a_right, float b_right, float c_right);
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_;
@ -155,16 +156,15 @@ class AY38910: public ::Outputs::Speaker::SampleSource {
PortHandler *port_handler_ = nullptr;
void set_port_output(bool port_b);
template <bool is_stereo> void get_samples(std::size_t number_of_samples, int16_t *target);
template <bool is_stereo> void evaluate_output_volume();
void evaluate_output_volume();
// Output mixing control.
bool is_stereo_ = false;
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

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

@ -18,7 +18,7 @@ namespace Macintosh {
struct DeferredAudio {
Concurrency::DeferringAsyncTaskQueue queue;
Audio audio;
Outputs::Speaker::LowpassSpeaker<Audio, false> speaker;
Outputs::Speaker::LowpassSpeaker<Audio> speaker;
HalfCycles time_since_update;
DeferredAudio() : audio(queue), speaker(audio) {}

View File

@ -43,7 +43,8 @@
namespace Oric {
using DiskInterface = Analyser::Static::Oric::Target::DiskInterface;
using Speaker = Outputs::Speaker::LowpassSpeaker<GI::AY38910::AY38910, false>;
using AY = GI::AY38910::AY38910<false>;
using Speaker = Outputs::Speaker::LowpassSpeaker<AY>;
enum ROM {
BASIC10 = 0, BASIC11, Microdisc, Colour
@ -147,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, Speaker &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) {}
/*!
@ -210,7 +211,7 @@ class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
HalfCycles cycles_since_ay_update_;
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
GI::AY38910::AY38910 &ay8910_;
AY &ay8910_;
Speaker &speaker_;
TapePlayer &tape_player_;
Keyboard &keyboard_;
@ -692,7 +693,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
VideoOutput video_output_;
Concurrency::DeferringAsyncTaskQueue audio_queue_;
GI::AY38910::AY38910 ay8910_;
GI::AY38910::AY38910<false> ay8910_;
Speaker speaker_;
// Inputs

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, false> 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

@ -28,7 +28,7 @@ namespace Speaker {
source of a high-frequency stream of audio which it filters down to a
lower-frequency output.
*/
template <typename SampleSource, bool is_stereo> class LowpassSpeaker: public Speaker {
template <typename SampleSource> class LowpassSpeaker: public Speaker {
public:
LowpassSpeaker(SampleSource &sample_source) : sample_source_(sample_source) {
sample_source.set_sample_volume_range(32767);
@ -66,11 +66,11 @@ template <typename SampleSource, bool is_stereo> class LowpassSpeaker: public Sp
filter_parameters_.output_cycles_per_second = cycles_per_second;
filter_parameters_.parameters_are_dirty = true;
output_buffer_.resize(std::size_t(buffer_size) * (is_stereo ? 2 : 1));
output_buffer_.resize(std::size_t(buffer_size) * (SampleSource::get_is_stereo() ? 2 : 1));
}
bool get_is_stereo() final {
return is_stereo;
return SampleSource::get_is_stereo();
}
/*!
@ -144,14 +144,14 @@ template <typename SampleSource, bool is_stereo> class LowpassSpeaker: public Sp
switch(conversion_) {
case Conversion::Copy:
while(cycles_remaining) {
const auto cycles_to_read = std::min((output_buffer_.size() - output_buffer_pointer_) / (is_stereo ? 2 : 1), 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 * (is_stereo ? 2 : 1);
output_buffer_pointer_ += cycles_to_read * (SampleSource::get_is_stereo() ? 2 : 1);
// Announce to delegate if full.
if(output_buffer_pointer_ == output_buffer_.size()) {
output_buffer_pointer_ = 0;
did_complete_samples(this, output_buffer_, is_stereo);
did_complete_samples(this, output_buffer_, SampleSource::get_is_stereo());
}
cycles_remaining -= cycles_to_read;
@ -160,10 +160,10 @@ template <typename SampleSource, bool is_stereo> class LowpassSpeaker: public Sp
case Conversion::ResampleSmaller:
while(cycles_remaining) {
const auto cycles_to_read = std::min((input_buffer_.size() - input_buffer_depth_) / (is_stereo ? 2 : 1), cycles_remaining);
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_]);
input_buffer_depth_ += cycles_to_read * (is_stereo ? 2 : 1);
input_buffer_depth_ += cycles_to_read * (SampleSource::get_is_stereo() ? 2 : 1);
if(input_buffer_depth_ == input_buffer_.size()) {
resample_input_buffer();
@ -248,7 +248,7 @@ template <typename SampleSource, bool is_stereo> class LowpassSpeaker: public Sp
// 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.
const size_t required_buffer_size = size_t(number_of_taps) * (is_stereo ? 2 : 1);
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();
@ -261,7 +261,7 @@ template <typename SampleSource, bool is_stereo> class LowpassSpeaker: public Sp
}
inline void resample_input_buffer() {
if constexpr (is_stereo) {
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;
@ -273,13 +273,13 @@ template <typename SampleSource, bool is_stereo> class LowpassSpeaker: public Sp
// Announce to delegate if full.
if(output_buffer_pointer_ == output_buffer_.size()) {
output_buffer_pointer_ = 0;
did_complete_samples(this, output_buffer_, is_stereo);
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() * (is_stereo ? 2 : 1);
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,
@ -288,7 +288,7 @@ template <typename SampleSource, bool is_stereo> class LowpassSpeaker: public Sp
input_buffer_depth_ -= steps;
} else {
if(steps > input_buffer_.size()) {
sample_source_.skip_samples((steps - input_buffer_.size()) / (is_stereo ? 2 : 1));
sample_source_.skip_samples((steps - input_buffer_.size()) / (SampleSource::get_is_stereo() ? 2 : 1));
}
input_buffer_depth_ = 0;
}