From 6bda4034c67effb49b591b63e8150cb8c0d0ee5a Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 9 Feb 2020 19:14:25 -0500 Subject: [PATCH] Ensures no input data is dropped when changing output rates. I think this 'completely' deals with the problem. At least until someone wants dynamic output buffer sizes or something like that. We'll see. --- .../Speaker/Implementation/LowpassSpeaker.hpp | 146 +++++++++++------- 1 file changed, 92 insertions(+), 54 deletions(-) diff --git a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp index afde67693..da1cc692b 100644 --- a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp +++ b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp @@ -109,6 +109,12 @@ template class LowpassSpeaker: public Speaker { } private: + enum class Conversion { + ResampleSmaller, + Copy, + ResampleLarger + } conversion_ = Conversion::Copy; + /*! Advances by the number of cycles specified, obtaining data from the sample source supplied at construction, filtering it and passing it on to the speaker's delegate if there is one. @@ -131,69 +137,41 @@ template class LowpassSpeaker: public Speaker { delegate_->speaker_did_change_input_clock(this); } - // If input and output rates exactly match, and no additional cut-off has been specified, - // just accumulate results and pass on. - if( filter_parameters.input_cycles_per_second == filter_parameters.output_cycles_per_second && - filter_parameters.high_frequency_cutoff < 0.0) { - while(cycles_remaining) { - const auto cycles_to_read = std::min(output_buffer_.size() - output_buffer_pointer_, cycles_remaining); + switch(conversion_) { + case Conversion::Copy: + while(cycles_remaining) { + const auto cycles_to_read = std::min(output_buffer_.size() - output_buffer_pointer_, cycles_remaining); - sample_source_.get_samples(cycles_to_read, &output_buffer_[output_buffer_pointer_]); - output_buffer_pointer_ += cycles_to_read; + sample_source_.get_samples(cycles_to_read, &output_buffer_[output_buffer_pointer_]); + output_buffer_pointer_ += cycles_to_read; - // announce to delegate if full - if(output_buffer_pointer_ == output_buffer_.size()) { - output_buffer_pointer_ = 0; - did_complete_samples(this, output_buffer_); - } - - cycles_remaining -= cycles_to_read; - } - - return; - } - - // If the output rate is less than the input rate, or an additional cut-off has been specified, use the filter. - if( filter_parameters.input_cycles_per_second > filter_parameters.output_cycles_per_second || - (filter_parameters.input_cycles_per_second == filter_parameters.output_cycles_per_second && filter_parameters.high_frequency_cutoff >= 0.0)) { - while(cycles_remaining) { - const auto cycles_to_read = std::min(cycles_remaining, input_buffer_.size() - input_buffer_depth_); - sample_source_.get_samples(cycles_to_read, &input_buffer_[input_buffer_depth_]); - cycles_remaining -= cycles_to_read; - input_buffer_depth_ += cycles_to_read; - - if(input_buffer_depth_ == input_buffer_.size()) { - output_buffer_[output_buffer_pointer_] = filter_->apply(input_buffer_.data()); - output_buffer_pointer_++; - - // Announce to delegate if full. + // announce to delegate if full if(output_buffer_pointer_ == output_buffer_.size()) { output_buffer_pointer_ = 0; did_complete_samples(this, output_buffer_); } - // If the next loop around is going to reuse some of the samples just collected, use a memmove to - // preserve them in the correct locations (TODO: use a longer buffer to fix that) and don't skip - // anything. Otherwise skip as required to get to the next sample batch and don't expect to reuse. - const auto steps = stepper_->step(); - if(steps < input_buffer_.size()) { - auto *const input_buffer = input_buffer_.data(); - std::memmove( input_buffer, - &input_buffer[steps], - sizeof(int16_t) * (input_buffer_.size() - steps)); - input_buffer_depth_ -= steps; - } else { - if(steps > input_buffer_.size()) - sample_source_.skip_samples(steps - input_buffer_.size()); - input_buffer_depth_ = 0; + cycles_remaining -= cycles_to_read; + } + break; + + case Conversion::ResampleSmaller: + while(cycles_remaining) { + const auto cycles_to_read = std::min(cycles_remaining, input_buffer_.size() - input_buffer_depth_); + sample_source_.get_samples(cycles_to_read, &input_buffer_[input_buffer_depth_]); + cycles_remaining -= cycles_to_read; + input_buffer_depth_ += cycles_to_read; + + if(input_buffer_depth_ == input_buffer_.size()) { + resample_input_buffer(); } } - } + break; - return; + case Conversion::ResampleLarger: + // TODO: input rate is less than output rate. + break; } - - // TODO: input rate is less than output rate } T &sample_source_; @@ -239,8 +217,68 @@ template class LowpassSpeaker: public Speaker { high_pass_frequency, SignalProcessing::FIRFilter::DefaultAttenuation); - input_buffer_.resize(std::size_t(number_of_taps)); - input_buffer_depth_ = 0; + + // Pick the new conversion function. + if( filter_parameters.input_cycles_per_second == filter_parameters.output_cycles_per_second && + filter_parameters.high_frequency_cutoff < 0.0) { + // If input and output rates exactly match, and no additional cut-off has been specified, + // just accumulate results and pass on. + conversion_ = Conversion::Copy; + } else if( filter_parameters.input_cycles_per_second > filter_parameters.output_cycles_per_second || + (filter_parameters.input_cycles_per_second == filter_parameters.output_cycles_per_second && filter_parameters.high_frequency_cutoff >= 0.0)) { + // If the output rate is less than the input rate, or an additional cut-off has been specified, use the filter. + conversion_ = Conversion::ResampleSmaller; + } else { + conversion_ = Conversion::ResampleLarger; + } + + // Do something sensible with any dangling input, if necessary. + 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, + // that means nothing to do. + default: break; + + case Conversion::ResampleSmaller: + // Reize the input buffer only if absolutely necessary; if sizing downward + // such that a sample would otherwise be lost then output it now. Keep anything + // currently in the input buffer that hasn't yet been processed. + if(input_buffer_.size() != size_t(number_of_taps)) { + if(input_buffer_depth_ >= size_t(number_of_taps)) { + resample_input_buffer(); + input_buffer_depth_ %= size_t(number_of_taps); + } + input_buffer_.resize(size_t(number_of_taps)); + } + break; + } + } + + inline void resample_input_buffer() { + output_buffer_[output_buffer_pointer_] = filter_->apply(input_buffer_.data()); + output_buffer_pointer_++; + + // Announce to delegate if full. + if(output_buffer_pointer_ == output_buffer_.size()) { + output_buffer_pointer_ = 0; + did_complete_samples(this, output_buffer_); + } + + // If the next loop around is going to reuse some of the samples just collected, use a memmove to + // preserve them in the correct locations (TODO: use a longer buffer to fix that?) and don't skip + // anything. Otherwise skip as required to get to the next sample batch and don't expect to reuse. + const auto steps = stepper_->step(); + if(steps < input_buffer_.size()) { + auto *const input_buffer = input_buffer_.data(); + std::memmove( input_buffer, + &input_buffer[steps], + sizeof(int16_t) * (input_buffer_.size() - steps)); + input_buffer_depth_ -= steps; + } else { + if(steps > input_buffer_.size()) + sample_source_.skip_samples(steps - input_buffer_.size()); + input_buffer_depth_ = 0; + } } };