mirror of
https://github.com/TomHarte/CLK.git
synced 2025-04-06 10:38:16 +00:00
Fixed: individual audio generators now either are or are not stereo. The speaker acts accordingly.
This commit is contained in:
parent
5242362f31
commit
9835e800ec
@ -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() {
|
||||
|
@ -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>;
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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_;
|
||||
|
@ -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) {}
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user