mirror of
https://github.com/TomHarte/CLK.git
synced 2024-12-23 20:29:42 +00:00
Introduces concept of 'average peak volume' in order better to normalise audio sources like the OPLL.
This commit is contained in:
parent
8f541602c1
commit
eed357abb4
@ -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;
|
||||
}
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -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_;
|
||||
}
|
||||
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -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_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -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());
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -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; }
|
||||
};
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user