1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-01-11 08:30:55 +00:00

Introduces concept of 'average peak volume' in order better to normalise audio sources like the OPLL.

This commit is contained in:
Thomas Harte 2020-05-09 17:57:21 -04:00
parent 8f541602c1
commit eed357abb4
12 changed files with 84 additions and 26 deletions

View File

@ -234,7 +234,7 @@ template <bool is_stereo> void AY38910<is_stereo>::evaluate_output_volume() {
} }
} }
template <bool is_stereo> bool AY38910<is_stereo>::is_zero_level() { template <bool is_stereo> bool AY38910<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;
} }

View File

@ -107,7 +107,7 @@ template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource
// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter. // to satisfy ::Outputs::Speaker (included via ::Outputs::Filter.
void get_samples(std::size_t number_of_samples, int16_t *target); void get_samples(std::size_t number_of_samples, int16_t *target);
bool is_zero_level(); bool is_zero_level() const;
void set_sample_volume_range(std::int16_t range); void set_sample_volume_range(std::int16_t range);
static constexpr bool get_is_stereo() { return is_stereo; } static constexpr bool get_is_stereo() { return is_stereo; }

View File

@ -15,7 +15,7 @@ using namespace Konami;
SCC::SCC(Concurrency::DeferringAsyncTaskQueue &task_queue) : SCC::SCC(Concurrency::DeferringAsyncTaskQueue &task_queue) :
task_queue_(task_queue) {} task_queue_(task_queue) {}
bool SCC::is_zero_level() { bool SCC::is_zero_level() const {
return !(channel_enable_ & 0x1f); return !(channel_enable_ & 0x1f);
} }

View File

@ -27,7 +27,7 @@ class SCC: public ::Outputs::Speaker::SampleSource {
SCC(Concurrency::DeferringAsyncTaskQueue &task_queue); SCC(Concurrency::DeferringAsyncTaskQueue &task_queue);
/// As per ::SampleSource; provides a broadphase test for silence. /// As per ::SampleSource; provides a broadphase test for silence.
bool is_zero_level(); bool is_zero_level() const;
/// As per ::SampleSource; provides audio output. /// As per ::SampleSource; provides audio output.
void get_samples(std::size_t number_of_samples, std::int16_t *target); void get_samples(std::size_t number_of_samples, std::int16_t *target);

View File

@ -30,6 +30,10 @@ class OPLL: public OPLBase<OPLL> {
void get_samples(std::size_t number_of_samples, std::int16_t *target); void get_samples(std::size_t number_of_samples, std::int16_t *target);
void set_sample_volume_range(std::int16_t range); void set_sample_volume_range(std::int16_t range);
// 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.
double get_average_output_peak() const { return 0.5; }
/// Reads from the OPL. /// Reads from the OPL.
uint8_t read(uint16_t address); uint8_t read(uint16_t address);

View File

@ -86,7 +86,7 @@ void SN76489::write(uint8_t value) {
}); });
} }
bool SN76489::is_zero_level() { bool SN76489::is_zero_level() const {
return channels_[0].volume == 0xf && channels_[1].volume == 0xf && channels_[2].volume == 0xf && channels_[3].volume == 0xf; return channels_[0].volume == 0xf && channels_[1].volume == 0xf && channels_[2].volume == 0xf && channels_[3].volume == 0xf;
} }

View File

@ -30,7 +30,7 @@ class SN76489: public Outputs::Speaker::SampleSource {
// As per SampleSource. // As per SampleSource.
void get_samples(std::size_t number_of_samples, std::int16_t *target); void get_samples(std::size_t number_of_samples, std::int16_t *target);
bool is_zero_level(); bool is_zero_level() const;
void set_sample_volume_range(std::int16_t range); void set_sample_volume_range(std::int16_t range);
static constexpr bool get_is_stereo() { return false; } static constexpr bool get_is_stereo() { return false; }

View File

@ -55,7 +55,7 @@ void Audio::set_enabled(bool on) {
// MARK: - Output generation // MARK: - Output generation
bool Audio::is_zero_level() { bool Audio::is_zero_level() const {
return !volume_ || !enabled_mask_; return !volume_ || !enabled_mask_;
} }

View File

@ -53,7 +53,7 @@ class Audio: public ::Outputs::Speaker::SampleSource {
// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter. // to satisfy ::Outputs::Speaker (included via ::Outputs::Filter.
void get_samples(std::size_t number_of_samples, int16_t *target); void get_samples(std::size_t number_of_samples, int16_t *target);
bool is_zero_level(); bool is_zero_level() const;
void set_sample_volume_range(std::int16_t range); void set_sample_volume_range(std::int16_t range);
constexpr static bool get_is_stereo() { return false; } constexpr static bool get_is_stereo() { return false; }

View File

@ -13,6 +13,7 @@
#include <cassert> #include <cassert>
#include <cstring> #include <cstring>
#include <atomic>
namespace Outputs { namespace Outputs {
namespace Speaker { namespace Speaker {
@ -26,7 +27,7 @@ template <typename... T> class CompoundSource:
public: public:
CompoundSource(T &... sources) : source_holder_(sources...) { CompoundSource(T &... sources) : source_holder_(sources...) {
// Default: give all sources equal volume. // Default: give all sources equal volume.
const float volume = 1.0f / static_cast<float>(source_holder_.size()); const auto volume = 1.0 / double(source_holder_.size());
for(std::size_t c = 0; c < source_holder_.size(); ++c) { for(std::size_t c = 0; c < source_holder_.size(); ++c) {
volumes_.push_back(volume); volumes_.push_back(volume);
} }
@ -40,10 +41,12 @@ template <typename... T> class CompoundSource:
source_holder_.skip_samples(number_of_samples); source_holder_.skip_samples(number_of_samples);
} }
/*!
Sets the total output volume of this CompoundSource.
*/
void set_sample_volume_range(int16_t range) { void set_sample_volume_range(int16_t range) {
volume_range_ = range; volume_range_ = range;
push_volumes(); push_volumes();
source_holder_.set_scaled_volume_range(range, volumes_.data());
} }
/*! /*!
@ -51,17 +54,30 @@ template <typename... T> class CompoundSource:
compound. The caller should ensure that the number of items supplied compound. The caller should ensure that the number of items supplied
matches the number of sources and that the values in it sum to 1.0. matches the number of sources and that the values in it sum to 1.0.
*/ */
void set_relative_volumes(const std::vector<float> &volumes) { void set_relative_volumes(const std::vector<double> &volumes) {
assert(volumes.size() == source_holder_.size()); assert(volumes.size() == source_holder_.size());
volumes_ = volumes; volumes_ = volumes;
push_volumes(); push_volumes();
average_output_peak_ = 1.0 / source_holder_.total_scale(volumes_.data());
} }
/*!
@returns true if any of the sources owned by this CompoundSource is stereo.
*/
static constexpr bool get_is_stereo() { return CompoundSourceHolder<T...>::get_is_stereo(); } static constexpr bool get_is_stereo() { return CompoundSourceHolder<T...>::get_is_stereo(); }
/*!
@returns the average output peak given the sources owned by this CompoundSource and the
current relative volumes.
*/
double get_average_output_peak() const {
return average_output_peak_;
}
private: private:
void push_volumes() { void push_volumes() {
source_holder_.set_scaled_volume_range(volume_range_, volumes_.data()); const double scale = source_holder_.total_scale(volumes_.data());
source_holder_.set_scaled_volume_range(volume_range_, volumes_.data(), scale);
} }
template <typename... S> class CompoundSourceHolder: public Outputs::Speaker::SampleSource { template <typename... S> class CompoundSourceHolder: public Outputs::Speaker::SampleSource {
@ -70,15 +86,19 @@ template <typename... T> class CompoundSource:
std::memset(target, 0, sizeof(std::int16_t) * number_of_samples); std::memset(target, 0, sizeof(std::int16_t) * number_of_samples);
} }
void set_scaled_volume_range(int16_t range, float *volumes) {} void set_scaled_volume_range(int16_t range, double *volumes, double scale) {}
std::size_t size() { static constexpr std::size_t size() {
return 0; return 0;
} }
static constexpr bool get_is_stereo() { static constexpr bool get_is_stereo() {
return false; return false;
} }
double total_scale(double *) const {
return 0.0;
}
}; };
template <typename S, typename... R> class CompoundSourceHolder<S, R...> { template <typename S, typename... R> class CompoundSourceHolder<S, R...> {
@ -125,27 +145,33 @@ template <typename... T> class CompoundSource:
next_source_.skip_samples(number_of_samples); next_source_.skip_samples(number_of_samples);
} }
void set_scaled_volume_range(int16_t range, float *volumes) { void set_scaled_volume_range(int16_t range, double *volumes, double scale) {
source_.set_sample_volume_range(static_cast<int16_t>(static_cast<float>(range * volumes[0]))); const auto scaled_range = volumes[0] / double(source_.get_average_output_peak()) * double(range) / scale;
next_source_.set_scaled_volume_range(range, &volumes[1]); source_.set_sample_volume_range(int16_t(scaled_range));
next_source_.set_scaled_volume_range(range, &volumes[1], scale);
} }
std::size_t size() { static constexpr std::size_t size() {
return 1+next_source_.size(); return 1 + CompoundSourceHolder<R...>::size();
} }
static constexpr bool get_is_stereo() { static constexpr bool get_is_stereo() {
return S::get_is_stereo() || CompoundSourceHolder<R...>::get_is_stereo(); return S::get_is_stereo() || CompoundSourceHolder<R...>::get_is_stereo();
} }
double total_scale(double *volumes) const {
return (volumes[0] / source_.get_average_output_peak()) + next_source_.total_scale(&volumes[1]);
}
private: private:
S &source_; S &source_;
CompoundSourceHolder<R...> next_source_; CompoundSourceHolder<R...> next_source_;
}; };
CompoundSourceHolder<T...> source_holder_; CompoundSourceHolder<T...> source_holder_;
std::vector<float> volumes_; std::vector<double> volumes_;
int16_t volume_range_ = 0; int16_t volume_range_ = 0;
std::atomic<double> average_output_peak_;
}; };
} }

View File

@ -134,6 +134,8 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker {
const auto delegate = delegate_.load(); const auto delegate = delegate_.load();
if(!delegate) return; if(!delegate) return;
const int scale = get_scale();
std::size_t cycles_remaining = size_t(cycles.as_integral()); std::size_t cycles_remaining = size_t(cycles.as_integral());
if(!cycles_remaining) return; if(!cycles_remaining) return;
@ -156,6 +158,8 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker {
sample_source_.get_samples(cycles_to_read, &output_buffer_[output_buffer_pointer_ ]); sample_source_.get_samples(cycles_to_read, &output_buffer_[output_buffer_pointer_ ]);
output_buffer_pointer_ += cycles_to_read * (SampleSource::get_is_stereo() ? 2 : 1); output_buffer_pointer_ += cycles_to_read * (SampleSource::get_is_stereo() ? 2 : 1);
// TODO: apply scale.
// Announce to delegate if full. // Announce to delegate if full.
if(output_buffer_pointer_ == output_buffer_.size()) { if(output_buffer_pointer_ == output_buffer_.size()) {
output_buffer_pointer_ = 0; output_buffer_pointer_ = 0;
@ -174,7 +178,7 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker {
input_buffer_depth_ += cycles_to_read * (SampleSource::get_is_stereo() ? 2 : 1); input_buffer_depth_ += cycles_to_read * (SampleSource::get_is_stereo() ? 2 : 1);
if(input_buffer_depth_ == input_buffer_.size()) { if(input_buffer_depth_ == input_buffer_.size()) {
resample_input_buffer(); resample_input_buffer(scale);
} }
cycles_remaining -= cycles_to_read; cycles_remaining -= cycles_to_read;
@ -246,6 +250,7 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker {
} }
// Do something sensible with any dangling input, if necessary. // Do something sensible with any dangling input, if necessary.
const int scale = get_scale();
switch(conversion_) { switch(conversion_) {
// Neither direct copying nor resampling larger currently use any temporary input. // Neither direct copying nor resampling larger currently use any temporary input.
// Although in the latter case that's just because it's unimplemented. But, regardless, // Although in the latter case that's just because it's unimplemented. But, regardless,
@ -259,7 +264,7 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker {
const size_t required_buffer_size = size_t(number_of_taps) * (SampleSource::get_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_.size() != required_buffer_size) {
if(input_buffer_depth_ >= required_buffer_size) { if(input_buffer_depth_ >= required_buffer_size) {
resample_input_buffer(); resample_input_buffer(scale);
input_buffer_depth_ %= required_buffer_size; input_buffer_depth_ %= required_buffer_size;
} }
input_buffer_.resize(required_buffer_size); input_buffer_.resize(required_buffer_size);
@ -268,7 +273,7 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker {
} }
} }
inline void resample_input_buffer() { inline void resample_input_buffer(int scale) {
if constexpr (SampleSource::get_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_ + 0] = filter_->apply(input_buffer_.data(), 2);
output_buffer_[output_buffer_pointer_ + 1] = filter_->apply(input_buffer_.data() + 1, 2); output_buffer_[output_buffer_pointer_ + 1] = filter_->apply(input_buffer_.data() + 1, 2);
@ -278,6 +283,18 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker {
output_buffer_pointer_++; output_buffer_pointer_++;
} }
// Apply scale, if supplied, clamping appropriately.
if(scale != 65536) {
#define SCALE(x) x = int16_t(std::max(std::min((int(x) * scale) >> 16, 32767), -32768))
if constexpr (SampleSource::get_is_stereo()) {
SCALE(output_buffer_[output_buffer_pointer_ - 2]);
SCALE(output_buffer_[output_buffer_pointer_ - 1]);
} else {
SCALE(output_buffer_[output_buffer_pointer_ - 1]);
}
#undef SCALE
}
// Announce to delegate if full. // Announce to delegate if full.
if(output_buffer_pointer_ == output_buffer_.size()) { if(output_buffer_pointer_ == output_buffer_.size()) {
output_buffer_pointer_ = 0; output_buffer_pointer_ = 0;
@ -301,6 +318,10 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker {
input_buffer_depth_ = 0; input_buffer_depth_ = 0;
} }
} }
int get_scale() {
return int(65536.0 / sample_source_.get_average_output_peak());
};
}; };
} }

View File

@ -43,7 +43,7 @@ class SampleSource {
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() { bool is_zero_level() const {
return false; return false;
} }
@ -51,13 +51,20 @@ class SampleSource {
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) {}
}
/*! /*!
Indicates whether this component will write stereo samples. Indicates whether this component will write stereo samples.
*/ */
static constexpr bool get_is_stereo() { return false; } static constexpr bool get_is_stereo() { return false; }
/*!
Permits a sample source to declare that, averaged over time, it will use only
a certain proportion of the allocated volume range. This commonly happens
in sample sources that use a time-multiplexed sound output for example, if
one were to output only every other sample then it would return 0.5.
*/
double get_average_output_peak() const { return 1.0; }
}; };
} }