From eed357abb4aed1c57111c74785f542699e8cce73 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 9 May 2020 17:57:21 -0400 Subject: [PATCH] Introduces concept of 'average peak volume' in order better to normalise audio sources like the OPLL. --- Components/AY38910/AY38910.cpp | 2 +- Components/AY38910/AY38910.hpp | 2 +- Components/KonamiSCC/KonamiSCC.cpp | 2 +- Components/KonamiSCC/KonamiSCC.hpp | 2 +- Components/OPL2/OPLL.hpp | 4 ++ Components/SN76489/SN76489.cpp | 2 +- Components/SN76489/SN76489.hpp | 2 +- Machines/Apple/Macintosh/Audio.cpp | 2 +- Machines/Apple/Macintosh/Audio.hpp | 2 +- .../Speaker/Implementation/CompoundSource.hpp | 50 ++++++++++++++----- .../Speaker/Implementation/LowpassSpeaker.hpp | 27 ++++++++-- .../Speaker/Implementation/SampleSource.hpp | 13 +++-- 12 files changed, 84 insertions(+), 26 deletions(-) diff --git a/Components/AY38910/AY38910.cpp b/Components/AY38910/AY38910.cpp index 28292bcc8..31a2e7ad6 100644 --- a/Components/AY38910/AY38910.cpp +++ b/Components/AY38910/AY38910.cpp @@ -234,7 +234,7 @@ template void AY38910::evaluate_output_volume() { } } -template bool AY38910::is_zero_level() { +template bool AY38910::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; } diff --git a/Components/AY38910/AY38910.hpp b/Components/AY38910/AY38910.hpp index bd9afbefd..5e04d91f5 100644 --- a/Components/AY38910/AY38910.hpp +++ b/Components/AY38910/AY38910.hpp @@ -107,7 +107,7 @@ template 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; } diff --git a/Components/KonamiSCC/KonamiSCC.cpp b/Components/KonamiSCC/KonamiSCC.cpp index 9d2fb6ddf..48563c434 100644 --- a/Components/KonamiSCC/KonamiSCC.cpp +++ b/Components/KonamiSCC/KonamiSCC.cpp @@ -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); } diff --git a/Components/KonamiSCC/KonamiSCC.hpp b/Components/KonamiSCC/KonamiSCC.hpp index e524614d4..edb3882eb 100644 --- a/Components/KonamiSCC/KonamiSCC.hpp +++ b/Components/KonamiSCC/KonamiSCC.hpp @@ -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); diff --git a/Components/OPL2/OPLL.hpp b/Components/OPL2/OPLL.hpp index 086d8b1b5..beaa816d8 100644 --- a/Components/OPL2/OPLL.hpp +++ b/Components/OPL2/OPLL.hpp @@ -30,6 +30,10 @@ class OPLL: public OPLBase { 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); diff --git a/Components/SN76489/SN76489.cpp b/Components/SN76489/SN76489.cpp index 852dbf37f..7da1459d1 100644 --- a/Components/SN76489/SN76489.cpp +++ b/Components/SN76489/SN76489.cpp @@ -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; } diff --git a/Components/SN76489/SN76489.hpp b/Components/SN76489/SN76489.hpp index eafac818a..6f5b0c151 100644 --- a/Components/SN76489/SN76489.hpp +++ b/Components/SN76489/SN76489.hpp @@ -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; } diff --git a/Machines/Apple/Macintosh/Audio.cpp b/Machines/Apple/Macintosh/Audio.cpp index 0ac016213..a6dac5cd6 100644 --- a/Machines/Apple/Macintosh/Audio.cpp +++ b/Machines/Apple/Macintosh/Audio.cpp @@ -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_; } diff --git a/Machines/Apple/Macintosh/Audio.hpp b/Machines/Apple/Macintosh/Audio.hpp index 39eb774a5..3d8f5835d 100644 --- a/Machines/Apple/Macintosh/Audio.hpp +++ b/Machines/Apple/Macintosh/Audio.hpp @@ -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; } diff --git a/Outputs/Speaker/Implementation/CompoundSource.hpp b/Outputs/Speaker/Implementation/CompoundSource.hpp index 8a6c8f1d9..21be56290 100644 --- a/Outputs/Speaker/Implementation/CompoundSource.hpp +++ b/Outputs/Speaker/Implementation/CompoundSource.hpp @@ -13,6 +13,7 @@ #include #include +#include namespace Outputs { namespace Speaker { @@ -26,7 +27,7 @@ template class CompoundSource: public: CompoundSource(T &... sources) : source_holder_(sources...) { // Default: give all sources equal volume. - const float volume = 1.0f / static_cast(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 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 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 &volumes) { + void set_relative_volumes(const std::vector &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::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 class CompoundSourceHolder: public Outputs::Speaker::SampleSource { @@ -70,15 +86,19 @@ template 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 class CompoundSourceHolder { @@ -125,27 +145,33 @@ template 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(static_cast(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::size(); } static constexpr bool get_is_stereo() { return S::get_is_stereo() || CompoundSourceHolder::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 next_source_; }; CompoundSourceHolder source_holder_; - std::vector volumes_; + std::vector volumes_; int16_t volume_range_ = 0; + std::atomic average_output_peak_; }; } diff --git a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp index 89ab38268..009e9db1d 100644 --- a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp +++ b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp @@ -134,6 +134,8 @@ template 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 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 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 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 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 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 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 class LowpassSpeaker: public Speaker { input_buffer_depth_ = 0; } } + + int get_scale() { + return int(65536.0 / sample_source_.get_average_output_peak()); + }; }; } diff --git a/Outputs/Speaker/Implementation/SampleSource.hpp b/Outputs/Speaker/Implementation/SampleSource.hpp index 04d567bc0..c0742e0e5 100644 --- a/Outputs/Speaker/Implementation/SampleSource.hpp +++ b/Outputs/Speaker/Implementation/SampleSource.hpp @@ -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; } }; }