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

Merge pull request #1330 from TomHarte/SampleProducingAY

Convert AY to a SampleSource.
This commit is contained in:
Thomas Harte 2024-02-13 14:45:31 -05:00 committed by GitHub
commit d43f050922
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 115 additions and 107 deletions

View File

@ -10,17 +10,24 @@
#include "AY38910.hpp" #include "AY38910.hpp"
//namespace GI {
//namespace AY38910 {
using namespace GI::AY38910; using namespace GI::AY38910;
// Note on dividers: 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.
// Therefore this class implements a divider of 4 and doubles the tone
// and noise periods. The envelope ticks along at the divide-by-four rate,
// but if this is an AY rather than a YM then its lowest bit is forced to 1,
// matching the YM datasheet's depiction of envelope level 31 as equal to
// programmatic volume 15, envelope level 29 as equal to programmatic 14, etc.
template <bool is_stereo> template <bool is_stereo>
AY38910<is_stereo>::AY38910(Personality personality, Concurrency::AsyncTaskQueue<false> &task_queue) : task_queue_(task_queue) { AY38910SampleSource<is_stereo>::AY38910SampleSource(Personality personality, Concurrency::AsyncTaskQueue<false> &task_queue) : task_queue_(task_queue) {
// Don't use the low bit of the envelope position if this is an AY. // Don't use the low bit of the envelope position if this is an AY.
envelope_position_mask_ |= personality == Personality::AY38910; envelope_position_mask_ |= personality == Personality::AY38910;
// Set up envelope lookup tables. // Set up envelope lookup tables; these are based on 32 volume levels as used by the YM2149F.
// The AY38910 will just use only even table entries, and therefore only even volumes.
for(int c = 0; c < 16; c++) { for(int c = 0; c < 16; c++) {
for(int p = 0; p < 64; p++) { for(int p = 0; p < 64; p++) {
switch(c) { switch(c) {
@ -74,7 +81,8 @@ AY38910<is_stereo>::AY38910(Personality personality, Concurrency::AsyncTaskQueue
set_sample_volume_range(0); set_sample_volume_range(0);
} }
template <bool is_stereo> void AY38910<is_stereo>::set_sample_volume_range(std::int16_t range) { template <bool is_stereo>
void AY38910SampleSource<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 // 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 // from the YM's datasheet, showing a clear power curve, and fitting that to observed
// values reported elsewhere. // values reported elsewhere.
@ -92,7 +100,8 @@ template <bool is_stereo> void AY38910<is_stereo>::set_sample_volume_range(std::
evaluate_output_volume(); evaluate_output_volume();
} }
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) { template <bool is_stereo>
void AY38910SampleSource<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); a_left_ = uint8_t(a_left * 255.0f);
b_left_ = uint8_t(b_left * 255.0f); b_left_ = uint8_t(b_left * 255.0f);
c_left_ = uint8_t(c_left * 255.0f); c_left_ = uint8_t(c_left * 255.0f);
@ -102,81 +111,49 @@ template <bool is_stereo> void AY38910<is_stereo>::set_output_mixing(float a_lef
} }
template <bool is_stereo> template <bool is_stereo>
template <Outputs::Speaker::Action action> void AY38910SampleSource<is_stereo>::advance() {
void AY38910<is_stereo>::apply_samples( const auto step_channel = [&](int c) {
std::size_t number_of_samples, if(tone_counters_[c]) --tone_counters_[c];
typename Outputs::Speaker::SampleT<is_stereo>::type *target
) {
// Note on structure below: the real AY has a built-in divider of 8
// prior to applying its tone and noise dividers. But the YM fills the
// same total periods for noise and tone with double-precision envelopes.
// Therefore this class implements a divider of 4 and doubles the tone
// and noise periods. The envelope ticks along at the divide-by-four rate,
// but if this is an AY rather than a YM then its lowest bit is forced to 1,
// matching the YM datasheet's depiction of envelope level 31 as equal to
// programmatic volume 15, envelope level 29 as equal to programmatic 14, etc.
std::size_t c = 0;
while((master_divider_&3) && c < number_of_samples) {
Outputs::Speaker::apply<action>(target[c], output_volume_);
master_divider_++;
c++;
}
while(c < number_of_samples) {
#define step_channel(c) \
if(tone_counters_[c]) tone_counters_[c]--;\
else {\
tone_outputs_[c] ^= 1;\
tone_counters_[c] = tone_periods_[c] << 1;\
}
// Update the tone channels.
step_channel(0);
step_channel(1);
step_channel(2);
#undef step_channel
// Update the noise generator. This recomputes the new bit repeatedly but harmlessly, only shifting
// it into the official 17 upon divider underflow.
if(noise_counter_) noise_counter_--;
else { else {
noise_counter_ = noise_period_ << 1; // To cover the double resolution of envelopes. tone_outputs_[c] ^= 1;
noise_output_ ^= noise_shift_register_&1; tone_counters_[c] = tone_periods_[c] << 1;
noise_shift_register_ |= ((noise_shift_register_ ^ (noise_shift_register_ >> 3))&1) << 17;
noise_shift_register_ >>= 1;
} }
};
// Update the envelope generator. Table based for pattern lookup, with a 'refill' step: a way of // Update the tone channels.
// implementing non-repeating patterns by locking them to the final table position. step_channel(0);
if(envelope_divider_) envelope_divider_--; step_channel(1);
else { step_channel(2);
envelope_divider_ = envelope_period_;
envelope_position_ ++;
if(envelope_position_ == 64) envelope_position_ = envelope_overflow_masks_[output_registers_[13]];
}
evaluate_output_volume(); // Update the noise generator. This recomputes the new bit repeatedly but harmlessly, only shifting
// it into the official 17 upon divider underflow.
for(int ic = 0; ic < 4 && c < number_of_samples; ic++) { if(noise_counter_) noise_counter_--;
Outputs::Speaker::apply<action>(target[c], output_volume_); else {
c++; noise_counter_ = noise_period_ << 1; // To cover the double resolution of envelopes.
master_divider_++; noise_output_ ^= noise_shift_register_&1;
} noise_shift_register_ |= ((noise_shift_register_ ^ (noise_shift_register_ >> 3))&1) << 17;
noise_shift_register_ >>= 1;
} }
master_divider_ &= 3; // Update the envelope generator. Table based for pattern lookup, with a 'refill' step: a way of
// implementing non-repeating patterns by locking them to the final table position.
if(envelope_divider_) envelope_divider_--;
else {
envelope_divider_ = envelope_period_;
envelope_position_ ++;
if(envelope_position_ == 64) envelope_position_ = envelope_overflow_masks_[output_registers_[13]];
}
evaluate_output_volume();
} }
template void AY38910<false>::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, typename Outputs::Speaker::SampleT<false>::type *); template <bool is_stereo>
template void AY38910<false>::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, typename Outputs::Speaker::SampleT<false>::type *); typename Outputs::Speaker::SampleT<is_stereo>::type AY38910SampleSource<is_stereo>::level() const {
template void AY38910<false>::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, typename Outputs::Speaker::SampleT<false>::type *); return output_volume_;
template void AY38910<true>::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, typename Outputs::Speaker::SampleT<true>::type *); }
template void AY38910<true>::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, typename Outputs::Speaker::SampleT<true>::type *);
template void AY38910<true>::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, typename Outputs::Speaker::SampleT<true>::type *);
template <bool is_stereo> void AY38910<is_stereo>::evaluate_output_volume() { template <bool is_stereo>
void AY38910SampleSource<is_stereo>::evaluate_output_volume() {
int envelope_volume = envelope_shapes_[output_registers_[13]][envelope_position_ | envelope_position_mask_]; int envelope_volume = envelope_shapes_[output_registers_[13]][envelope_position_ | envelope_position_mask_];
// The output level for a channel is: // The output level for a channel is:
@ -237,18 +214,21 @@ template <bool is_stereo> void AY38910<is_stereo>::evaluate_output_volume() {
} }
} }
template <bool is_stereo> bool AY38910<is_stereo>::is_zero_level() const { template <bool is_stereo>
bool AY38910SampleSource<is_stereo>::is_zero_level() const {
// Confirm that the AY is trivially at the zero level if all three volume controls are set to fixed zero. // 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; return output_registers_[0x8] == 0 && output_registers_[0x9] == 0 && output_registers_[0xa] == 0;
} }
// MARK: - Register manipulation // MARK: - Register manipulation
template <bool is_stereo> void AY38910<is_stereo>::select_register(uint8_t r) { template <bool is_stereo>
void AY38910SampleSource<is_stereo>::select_register(uint8_t r) {
selected_register_ = r; selected_register_ = r;
} }
template <bool is_stereo> void AY38910<is_stereo>::set_register_value(uint8_t value) { template <bool is_stereo>
void AY38910SampleSource<is_stereo>::set_register_value(uint8_t value) {
// There are only 16 registers. // There are only 16 registers.
if(selected_register_ > 15) return; if(selected_register_ > 15) return;
@ -317,7 +297,8 @@ template <bool is_stereo> void AY38910<is_stereo>::set_register_value(uint8_t va
if(update_port_a) set_port_output(false); if(update_port_a) set_port_output(false);
} }
template <bool is_stereo> uint8_t AY38910<is_stereo>::get_register_value() { template <bool is_stereo>
uint8_t AY38910SampleSource<is_stereo>::get_register_value() {
// This table ensures that bits that aren't defined within the AY are returned as 0s // This table ensures that bits that aren't defined within the AY are returned as 0s
// when read, conforming to CPC-sourced unit tests. // when read, conforming to CPC-sourced unit tests.
const uint8_t register_masks[16] = { const uint8_t register_masks[16] = {
@ -331,24 +312,28 @@ template <bool is_stereo> uint8_t AY38910<is_stereo>::get_register_value() {
// MARK: - Port querying // MARK: - Port querying
template <bool is_stereo> uint8_t AY38910<is_stereo>::get_port_output(bool port_b) { template <bool is_stereo>
uint8_t AY38910SampleSource<is_stereo>::get_port_output(bool port_b) {
return registers_[port_b ? 15 : 14]; return registers_[port_b ? 15 : 14];
} }
// MARK: - Bus handling // MARK: - Bus handling
template <bool is_stereo> void AY38910<is_stereo>::set_port_handler(PortHandler *handler) { template <bool is_stereo>
void AY38910SampleSource<is_stereo>::set_port_handler(PortHandler *handler) {
port_handler_ = handler; port_handler_ = handler;
set_port_output(true); set_port_output(true);
set_port_output(false); set_port_output(false);
} }
template <bool is_stereo> void AY38910<is_stereo>::set_data_input(uint8_t r) { template <bool is_stereo>
void AY38910SampleSource<is_stereo>::set_data_input(uint8_t r) {
data_input_ = r; data_input_ = r;
update_bus(); update_bus();
} }
template <bool is_stereo> void AY38910<is_stereo>::set_port_output(bool port_b) { template <bool is_stereo>
void AY38910SampleSource<is_stereo>::set_port_output(bool port_b) {
// Per the data sheet: "each [IO] pin is provided with an on-chip pull-up resistor, // 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, // 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. // report programmer selection of input mode as creating an output of 0xff.
@ -358,7 +343,8 @@ template <bool is_stereo> void AY38910<is_stereo>::set_port_output(bool port_b)
} }
} }
template <bool is_stereo> uint8_t AY38910<is_stereo>::get_data_output() { template <bool is_stereo>
uint8_t AY38910SampleSource<is_stereo>::get_data_output() {
if(control_state_ == Read && selected_register_ >= 14 && selected_register_ < 16) { 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 // 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. // value returned to the CPU when reading it is the and of the output value and any input.
@ -374,7 +360,8 @@ template <bool is_stereo> uint8_t AY38910<is_stereo>::get_data_output() {
return data_output_; return data_output_;
} }
template <bool is_stereo> void AY38910<is_stereo>::set_control_lines(ControlLines control_lines) { template <bool is_stereo>
void AY38910SampleSource<is_stereo>::set_control_lines(ControlLines control_lines) {
switch(int(control_lines)) { switch(int(control_lines)) {
default: control_state_ = Inactive; break; default: control_state_ = Inactive; break;
@ -389,7 +376,8 @@ template <bool is_stereo> void AY38910<is_stereo>::set_control_lines(ControlLine
update_bus(); update_bus();
} }
template <bool is_stereo> void AY38910<is_stereo>::update_bus() { template <bool is_stereo>
void AY38910SampleSource<is_stereo>::update_bus() {
// Assume no output, unless this turns out to be a read. // Assume no output, unless this turns out to be a read.
data_output_ = 0xff; data_output_ = 0xff;
switch(control_state_) { switch(control_state_) {
@ -401,5 +389,10 @@ template <bool is_stereo> void AY38910<is_stereo>::update_bus() {
} }
// Ensure both mono and stereo versions of the AY are built. // Ensure both mono and stereo versions of the AY are built.
template class GI::AY38910::AY38910<true>; template class GI::AY38910::AY38910SampleSource<true>;
template class GI::AY38910::AY38910<false>; template class GI::AY38910::AY38910SampleSource<false>;
// Perform an explicit instantiation of the BufferSource to hope for
// appropriate inlining of advance() and level().
template struct GI::AY38910::AY38910<true>;
template struct GI::AY38910::AY38910<false>;

View File

@ -66,10 +66,10 @@ enum class Personality {
This AY has an attached mono or stereo mixer. This AY has an attached mono or stereo mixer.
*/ */
template <bool stereo> class AY38910: public ::Outputs::Speaker::BufferSource<AY38910<stereo>, stereo> { template <bool stereo> class AY38910SampleSource {
public: public:
/// Creates a new AY38910. /// Creates a new AY38910.
AY38910(Personality, Concurrency::AsyncTaskQueue<false> &); AY38910SampleSource(Personality, Concurrency::AsyncTaskQueue<false> &);
/// Sets the value the AY would read from its data lines if it were not outputting. /// Sets the value the AY would read from its data lines if it were not outputting.
void set_data_input(uint8_t r); void set_data_input(uint8_t r);
@ -105,9 +105,9 @@ template <bool stereo> class AY38910: public ::Outputs::Speaker::BufferSource<AY
*/ */
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); 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);
// Buffer generation. // Sample generation.
template <Outputs::Speaker::Action action> typename Outputs::Speaker::SampleT<stereo>::type level() const;
void apply_samples(std::size_t number_of_samples, typename Outputs::Speaker::SampleT<stereo>::type *target); void advance();
bool is_zero_level() const; bool is_zero_level() const;
void set_sample_volume_range(std::int16_t range); void set_sample_volume_range(std::int16_t range);
@ -118,8 +118,6 @@ template <bool stereo> class AY38910: public ::Outputs::Speaker::BufferSource<AY
uint8_t registers_[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; uint8_t registers_[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
uint8_t output_registers_[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; uint8_t output_registers_[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
int master_divider_ = 0;
int tone_periods_[3] = {0, 0, 0}; int tone_periods_[3] = {0, 0, 0};
int tone_counters_[3] = {0, 0, 0}; int tone_counters_[3] = {0, 0, 0};
int tone_outputs_[3] = {0, 0, 0}; int tone_outputs_[3] = {0, 0, 0};
@ -166,6 +164,20 @@ template <bool stereo> class AY38910: public ::Outputs::Speaker::BufferSource<AY
friend struct State; friend struct State;
}; };
/// Defines a default AY to be the sample source with a master divider of 4;
/// real AYs have a divide-by-8 step built in but YMs apply only a divide by 4.
///
/// The implementation of AY38910SampleSource combines those two worlds
/// by always applying a divide by four and scaling other things as appropriate.
template <bool stereo> struct AY38910:
public AY38910SampleSource<stereo>,
public Outputs::Speaker::SampleSource<AY38910<stereo>, stereo, 4> {
// Use the same constructor as `AY38910SampleSource` (along with inheriting
// the rest of its interface).
using AY38910SampleSource<stereo>::AY38910SampleSource;
};
/*! /*!
Provides helper code, to provide something closer to the interface exposed by many Provides helper code, to provide something closer to the interface exposed by many
AY-deploying machines of the era. AY-deploying machines of the era.

View File

@ -25,6 +25,9 @@ class Toggle: public Outputs::Speaker::BufferSource<Toggle, false> {
Outputs::Speaker::fill<action>(target, target + number_of_samples, level_); Outputs::Speaker::fill<action>(target, target + number_of_samples, level_);
} }
void set_sample_volume_range(std::int16_t range); void set_sample_volume_range(std::int16_t range);
bool is_zero_level() const {
return !level_;
}
void set_output(bool enabled); void set_output(bool enabled);
bool get_output() const; bool get_output() const;

View File

@ -28,6 +28,7 @@ class OPLL: public OPLBase<OPLL, false> {
template <Outputs::Speaker::Action action> template <Outputs::Speaker::Action action>
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target); void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target);
void set_sample_volume_range(std::int16_t range); void set_sample_volume_range(std::int16_t range);
bool is_zero_level() const { return false; } // TODO.
// The OPLL is generally 'half' as loud as it's told to be. This won't strictly be true in // The OPLL is generally 'half' as loud as it's told to be. This won't strictly be true in
// rhythm mode, but it's correct for melodic output. // rhythm mode, but it's correct for melodic output.

View File

@ -37,6 +37,7 @@ class GLU: public Outputs::Speaker::BufferSource<GLU, false> { // TODO: isn't th
template <Outputs::Speaker::Action action> template <Outputs::Speaker::Action action>
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target); void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target);
void set_sample_volume_range(std::int16_t range); void set_sample_volume_range(std::int16_t range);
bool is_zero_level() const { return false; } // TODO.
private: private:
Concurrency::AsyncTaskQueue<false> &audio_queue_; Concurrency::AsyncTaskQueue<false> &audio_queue_;

View File

@ -76,13 +76,13 @@ class BufferSource {
fill the target with zeroes; @c false if a call might return all zeroes or fill the target with zeroes; @c false if a call might return all zeroes or
might not. might not.
*/ */
bool is_zero_level() const { return false; } // bool is_zero_level() const { return false; }
/*! /*!
Sets the proper output range for this sample source; it should write values Sets the proper output range for this sample source; it should write values
between 0 and volume. between 0 and volume.
*/ */
void set_sample_volume_range(std::int16_t volume); // void set_sample_volume_range(std::int16_t volume);
/*! /*!
Permits a sample source to declare that, averaged over time, it will use only Permits a sample source to declare that, averaged over time, it will use only
@ -101,13 +101,13 @@ class BufferSource {
template <typename SourceT, bool stereo, int divider = 1> template <typename SourceT, bool stereo, int divider = 1>
struct SampleSource: public BufferSource<SourceT, stereo> { struct SampleSource: public BufferSource<SourceT, stereo> {
public: public:
template <bool mix> template <Action action>
void apply_samples(std::size_t number_of_samples, typename SampleT<stereo>::type *target) { void apply_samples(std::size_t number_of_samples, typename SampleT<stereo>::type *target) {
const auto &source = *static_cast<SourceT *>(this); auto &source = *static_cast<SourceT *>(this);
if constexpr (divider == 1) { if constexpr (divider == 1) {
while(number_of_samples--) { while(number_of_samples--) {
apply<mix>(*target, source.level()); apply<action>(*target, source.level());
++target; ++target;
source.advance(); source.advance();
} }
@ -117,34 +117,32 @@ struct SampleSource: public BufferSource<SourceT, stereo> {
// Fill in the tail of any partially-captured level. // Fill in the tail of any partially-captured level.
auto level = source.level(); auto level = source.level();
while(c < number_of_samples && master_divider_ != divider) { while(c < number_of_samples && master_divider_ != divider) {
apply<mix>(target[c], level); apply<action>(target[c], level);
++c; ++c;
++master_divider_; ++master_divider_;
} }
source.advance(); source.advance();
// Provide all full levels. // Provide all full levels.
int whole_steps = (number_of_samples - c) / divider; auto whole_steps = static_cast<int>((number_of_samples - c) / divider);
while(whole_steps--) { while(whole_steps--) {
fill<mix>(&target[c], &target[c + divider], source.level()); fill<action>(&target[c], &target[c + divider], source.level());
c += divider; c += divider;
source.advance(); source.advance();
} }
// Provide the head of a further partial capture. // Provide the head of a further partial capture.
level = source.level(); level = source.level();
master_divider_ = number_of_samples - c; master_divider_ = static_cast<int>(number_of_samples - c);
fill<mix>(&target[c], &target[number_of_samples], source.level()); fill<action>(&target[c], &target[number_of_samples], source.level());
} }
} }
// TODO: use a concept here, when C++20 filters through. // TODO: use a concept here, when C++20 filters through.
// //
// Until then: sample sources should implement this. // Until then: sample sources should implement this.
auto level() const { // typename SampleT<stereo>::type level() const;
return typename SampleT<stereo>::type(); // void advance();
}
void advance() {}
private: private:
int master_divider_{}; int master_divider_{};