1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-12-26 09:29:45 +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.
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.
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);
static constexpr bool get_is_stereo() { return is_stereo; }

View File

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

View File

@ -27,7 +27,7 @@ class SCC: public ::Outputs::Speaker::SampleSource {
SCC(Concurrency::DeferringAsyncTaskQueue &task_queue);
/// As per ::SampleSource; provides a broadphase test for silence.
bool is_zero_level();
bool is_zero_level() const;
/// As per ::SampleSource; provides audio output.
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 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.
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;
}

View File

@ -30,7 +30,7 @@ class SN76489: public Outputs::Speaker::SampleSource {
// As per SampleSource.
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);
static constexpr bool get_is_stereo() { return false; }

View File

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

View File

@ -53,7 +53,7 @@ class Audio: public ::Outputs::Speaker::SampleSource {
// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter.
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);
constexpr static bool get_is_stereo() { return false; }

View File

@ -13,6 +13,7 @@
#include <cassert>
#include <cstring>
#include <atomic>
namespace Outputs {
namespace Speaker {
@ -26,7 +27,7 @@ template <typename... T> class CompoundSource:
public:
CompoundSource(T &... sources) : source_holder_(sources...) {
// 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) {
volumes_.push_back(volume);
}
@ -40,10 +41,12 @@ template <typename... T> class CompoundSource:
source_holder_.skip_samples(number_of_samples);
}
/*!
Sets the total output volume of this CompoundSource.
*/
void set_sample_volume_range(int16_t range) {
volume_range_ = range;
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
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());
volumes_ = 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(); }
/*!
@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:
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 {
@ -70,15 +86,19 @@ template <typename... T> class CompoundSource:
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;
}
static constexpr bool get_is_stereo() {
return false;
}
double total_scale(double *) const {
return 0.0;
}
};
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);
}
void set_scaled_volume_range(int16_t range, float *volumes) {
source_.set_sample_volume_range(static_cast<int16_t>(static_cast<float>(range * volumes[0])));
next_source_.set_scaled_volume_range(range, &volumes[1]);
void set_scaled_volume_range(int16_t range, double *volumes, double scale) {
const auto scaled_range = volumes[0] / double(source_.get_average_output_peak()) * double(range) / scale;
source_.set_sample_volume_range(int16_t(scaled_range));
next_source_.set_scaled_volume_range(range, &volumes[1], scale);
}
std::size_t size() {
return 1+next_source_.size();
static constexpr std::size_t size() {
return 1 + CompoundSourceHolder<R...>::size();
}
static constexpr bool 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:
S &source_;
CompoundSourceHolder<R...> next_source_;
};
CompoundSourceHolder<T...> source_holder_;
std::vector<float> volumes_;
std::vector<double> volumes_;
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();
if(!delegate) return;
const int scale = get_scale();
std::size_t cycles_remaining = size_t(cycles.as_integral());
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_ ]);
output_buffer_pointer_ += cycles_to_read * (SampleSource::get_is_stereo() ? 2 : 1);
// TODO: apply scale.
// Announce to delegate if full.
if(output_buffer_pointer_ == output_buffer_.size()) {
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);
if(input_buffer_depth_ == input_buffer_.size()) {
resample_input_buffer();
resample_input_buffer(scale);
}
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.
const int scale = get_scale();
switch(conversion_) {
// 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,
@ -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);
if(input_buffer_.size() != 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_.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()) {
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);
@ -278,6 +283,18 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker {
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.
if(output_buffer_pointer_ == output_buffer_.size()) {
output_buffer_pointer_ = 0;
@ -301,6 +318,10 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker {
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
might not.
*/
bool is_zero_level() {
bool is_zero_level() const {
return false;
}
@ -51,13 +51,20 @@ class SampleSource {
Sets the proper output range for this sample source; it should write values
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.
*/
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; }
};
}