From: <sv...@op...> - 2025-03-23 18:48:52
|
Author: sagamusix Date: Sun Mar 23 19:48:34 2025 New Revision: 23077 URL: https://source.openmpt.org/browse/openmpt/?op=revision&rev=23077 Log: [Var] Signalmmith Stretch: Update to commit ffa45981be0b75079b43d1a7a142234f06869254 (2025-02-12). Modified: trunk/OpenMPT/include/SignalsmithStretch/OpenMPT.txt trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/README.md trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/dsp/README.md trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/dsp/common.h trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/dsp/fft.h trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/dsp/spectral.h trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/signalsmith-stretch.h Modified: trunk/OpenMPT/include/SignalsmithStretch/OpenMPT.txt ============================================================================== --- trunk/OpenMPT/include/SignalsmithStretch/OpenMPT.txt Sun Mar 23 17:20:10 2025 (r23076) +++ trunk/OpenMPT/include/SignalsmithStretch/OpenMPT.txt Sun Mar 23 19:48:34 2025 (r23077) @@ -1,3 +1,3 @@ -Signalsmith Stretch: C++ pitch/time library, commit a9d78f033150e9e2421decdf1c83011a316fcfb1 (2024-02-19) +Signalsmith Stretch: C++ pitch/time library, commit ffa45981be0b75079b43d1a7a142234f06869254 (2025-02-12) https://github.com/Signalsmith-Audio/signalsmith-stretch -No local changes have been made. \ No newline at end of file +Unneeded files (cmd and web folder) have been removed. \ No newline at end of file Modified: trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/README.md ============================================================================== --- trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/README.md Sun Mar 23 17:20:10 2025 (r23076) +++ trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/README.md Sun Mar 23 19:48:34 2025 (r23077) @@ -6,6 +6,8 @@ ## How to use it +Just include `signalsmith-stretch.h` where needed: + ```cpp #include "signalsmith-stretch.h" @@ -92,21 +94,35 @@ To follow pitch/time automation accurately, you should give it automation values from the current processing time (`.outputLatency()` samples ahead of the output), and feed it input from `.inputLatency()` samples ahead of the current processing time. -#### Starting and ending +### Seeking and starting + +You can use `.seek()` which lets you move around the input audio, by providing a bunch of input samples. You should ideally provide at least (one block-length + one interval) of input data: + +```cpp +stretch.seek(inputBuffers, inputSamples, playbackRateHint); +``` -After initialisation/reset to zero, the current processing time is `.inputLatency()` samples *before* t=0 in the input. This means you'll get `stretch.outputLatency() + stretch.inputLatency()*stretchFactor` samples of pre-roll output in total. +At the very start of playback (or after a `.reset()`), the current processing time is `.inputLatency()` samples *before* the first input samples you give it. You therefore might want to call `.seek()` to provide the first `inputSamples = stretch.inputLatency()` samples of input, so that the processing time matches the start of the input (meaning your pre-roll output is only `.outputLatency()` samples long). -If you're processing a fixed-length sound (instead of an infinite stream), you'll end up providing `.inputLatency()` samples of extra (zero) input at the end, to get the processing time to the right place. You'll then want to give it another `.outputLatency()` samples of (zero) input to fully clear the buffer, producing a correspondly-stretched amount of output. +### Ending + +If you're processing a fixed-length sound (instead of an infinite stream), you'll reach the end of your input, but still have some pending output. You should first make sure the processing time gets to the end, by passing an additional `.inputLatency()` samples of silence to `.process()` (similar to using `.seek()` at the beginning). + +You can then read the final part of the output using `.flush()`. It's recommended to read at least `.outputLatency()` samples of output: + +```cpp +stretch.flush(outputBuffers, outputSamples); +``` -What you do with this extra start/end output is up to you. Personally, I'd try inverting the phase and reversing them in time, and then adding them to the start/end of the result. (Wrapping this up in a helper function is on the TODO list.) +Using `.seek()`/`.flush()` like this, you can perform an exact time-stretch on a fixed-length sound, and your result will have `.outputLatency()` of pre-roll. ## Compiling -⚠️ This has mostly been tested with Clang. If you're using another compiler and have any problems, please get in touch. +⚠️ This has mostly been tested with Clang. If you're using another compiler and have any problems, please get in touch. -Just include `signalsmith-stretch.h` where needed. +🚨 It's generally be OK to enable `-ffast-math`, however there's a bug in Apple Clang 16.0.0 which can generate incorrect SIMD code. If you _have_ to use this version, we advise you don't use `-ffast-math`. -It's much slower (about 10x) if optimisation is disabled though, so you might want to enable optimisation where it's used, even in debug builds. +It's much slower (about 10x) if optimisation is disabled altogether, so you might want to enable optimisation where it's used, even in Debug builds. ### DSP Library @@ -116,4 +132,21 @@ ## License -[MIT License](LICENSE.txt) for now - get in touch if you need anything else. +Released under the [MIT License](LICENSE.txt) - get in touch if you need anything else. + +## Other environments / languages + +There's a Web Audio wrapper in `web/` (using WASM/AudioWorklet). This will remain in-sync with the C++ library. + +There's a [Python binding](https://pypi.org/project/python-stretch/) written/published by [Gregorio Andrea Giudici](https://github.com/gregogiudici/python-stretch), and a [Rust wrapper](https://crates.io/crates/signalsmith-stretch) by [Colin Marc](https://github.com/colinmarc/signalsmith-stretch-rs). + +## Thanks + +We'd like to particularly thank the following people who sponsored specific features or improvements: + +* **Metronaut**: web audio (JS/WASM) release +* **Daniel L Bowling** and the Stanford School of Medicine: web audio improvements + +We're also grateful for the following community contributions: + +* **Steve MacKinnon** for finding/resolving a bug in `.reset()` Modified: trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/dsp/README.md ============================================================================== --- trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/dsp/README.md Sun Mar 23 17:20:10 2025 (r23076) +++ trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/dsp/README.md Sun Mar 23 19:48:34 2025 (r23077) @@ -22,7 +22,7 @@ You can add a compile-time version-check to make sure you have a compatible version of the library: ```cpp #include "dsp/envelopes.h" -SIGNALSMITH_DSP_VERSION_CHECK(1, 6, 0) +SIGNALSMITH_DSP_VERSION_CHECK(1, 6, 1) ``` ### Development / contributing Modified: trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/dsp/common.h ============================================================================== --- trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/dsp/common.h Sun Mar 23 17:20:10 2025 (r23076) +++ trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/dsp/common.h Sun Mar 23 19:48:34 2025 (r23077) @@ -1,6 +1,10 @@ #ifndef SIGNALSMITH_DSP_COMMON_H #define SIGNALSMITH_DSP_COMMON_H +#if defined(__FAST_MATH__) && (__apple_build_version__ >= 16000000) && (__apple_build_version__ <= 16000099) +# error Apple Clang 16.0.0 generates incorrect SIMD for ARM. If you HAVE to use this version of Clang, turn off -ffast-math. +#endif + #ifndef M_PI #define M_PI 3.14159265358979323846264338327950288 #endif @@ -15,8 +19,8 @@ #define SIGNALSMITH_DSP_VERSION_MAJOR 1 #define SIGNALSMITH_DSP_VERSION_MINOR 6 -#define SIGNALSMITH_DSP_VERSION_PATCH 0 -#define SIGNALSMITH_DSP_VERSION_STRING "1.6.0" +#define SIGNALSMITH_DSP_VERSION_PATCH 1 +#define SIGNALSMITH_DSP_VERSION_STRING "1.6.1" /** Version compatability check. \code{.cpp} @@ -39,5 +43,5 @@ } // signalsmith:: #else // If we've already included it, check it's the same version -static_assert(SIGNALSMITH_DSP_VERSION_MAJOR == 1 && SIGNALSMITH_DSP_VERSION_MINOR == 6 && SIGNALSMITH_DSP_VERSION_PATCH == 0, "multiple versions of the Signalsmith DSP library"); +static_assert(SIGNALSMITH_DSP_VERSION_MAJOR == 1 && SIGNALSMITH_DSP_VERSION_MINOR == 6 && SIGNALSMITH_DSP_VERSION_PATCH == 1, "multiple versions of the Signalsmith DSP library"); #endif // include guard Modified: trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/dsp/fft.h ============================================================================== --- trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/dsp/fft.h Sun Mar 23 17:20:10 2025 (r23076) +++ trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/dsp/fft.h Sun Mar 23 19:48:34 2025 (r23077) @@ -162,8 +162,10 @@ plan.resize(0); twiddleVector.resize(0); addPlanSteps(0, 0, _size, 1); + twiddleVector.shrink_to_fit(); permutation.resize(0); + permutation.reserve(_size); permutation.push_back(PermutationPair{0, 0}); size_t indexLow = 0, indexHigh = factors.size(); size_t inputStepLow = _size, outputStepLow = 1; Modified: trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/dsp/spectral.h ============================================================================== --- trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/dsp/spectral.h Sun Mar 23 17:20:10 2025 (r23076) +++ trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/dsp/spectral.h Sun Mar 23 19:48:34 2025 (r23077) @@ -89,16 +89,17 @@ return mrfft.size(); } - /// Performs an FFT (with windowing) - template<class Input, class Output> + /// Performs an FFT, with windowing and rotation (if enabled) + template<bool withWindow=true, bool withScaling=false, class Input, class Output> void fft(Input &&input, Output &&output) { int fftSize = size(); + const Sample norm = (withScaling ? 1/(Sample)fftSize : 1); for (int i = 0; i < offsetSamples; ++i) { // Inverted polarity since we're using the MRFFT - timeBuffer[i + fftSize - offsetSamples] = -input[i]*fftWindow[i]; + timeBuffer[i + fftSize - offsetSamples] = -input[i]*norm*(withWindow ? fftWindow[i] : Sample(1)); } for (int i = offsetSamples; i < fftSize; ++i) { - timeBuffer[i - offsetSamples] = input[i]*fftWindow[i]; + timeBuffer[i - offsetSamples] = input[i]*norm*(withWindow ? fftWindow[i] : Sample(1)); } mrfft.fft(timeBuffer, output); } @@ -108,22 +109,22 @@ mrfft.fft(input, output); } - /// Inverse FFT, with windowing and 1/N scaling - template<class Input, class Output> + /// Inverse FFT, with windowing, 1/N scaling and rotation (if enabled) + template<bool withWindow=true, bool withScaling=true, class Input, class Output> void ifft(Input &&input, Output &&output) { mrfft.ifft(input, timeBuffer); int fftSize = mrfft.size(); - Sample norm = 1/(Sample)fftSize; + const Sample norm = (withScaling ? 1/(Sample)fftSize : 1); for (int i = 0; i < offsetSamples; ++i) { // Inverted polarity since we're using the MRFFT - output[i] = -timeBuffer[i + fftSize - offsetSamples]*norm*fftWindow[i]; + output[i] = -timeBuffer[i + fftSize - offsetSamples]*norm*(withWindow ? fftWindow[i] : Sample(1)); } for (int i = offsetSamples; i < fftSize; ++i) { - output[i] = timeBuffer[i - offsetSamples]*norm*fftWindow[i]; + output[i] = timeBuffer[i - offsetSamples]*norm*(withWindow ? fftWindow[i] : Sample(1)); } } - /// Performs an IFFT (no windowing or rotation) + /// Performs an IFFT (no windowing, scaling or rotation) template<class Input, class Output> void ifftRaw(Input &&input, Output &&output) { mrfft.ifft(input, output); @@ -169,6 +170,7 @@ */ template<typename Sample> class STFT : public signalsmith::delay::MultiBuffer<Sample> { + using Super = signalsmith::delay::MultiBuffer<Sample>; using Complex = std::complex<Sample>; @@ -206,6 +208,7 @@ }; std::vector<Sample> timeBuffer; + bool rotate = false; void resizeInternal(int newChannels, int windowSize, int newInterval, int historyLength, int zeroPadding) { Super::resize(newChannels, windowSize /* for output summing */ @@ -213,14 +216,14 @@ + historyLength); int fftSize = fft.fastSizeAbove(windowSize + zeroPadding); - + this->channels = newChannels; _windowSize = windowSize; this->_fftSize = fftSize; this->_interval = newInterval; validUntilIndex = -1; - setWindow(windowShape); + setWindow(windowShape, rotate); spectrum.resize(channels, fftSize/2); timeBuffer.resize(fftSize); @@ -242,6 +245,7 @@ // TODO: these should both be set before resize() void setWindow(Window shape, bool rotateToZero=false) { windowShape = shape; + rotate = rotateToZero; auto &window = fft.setSizeWindow(_fftSize, rotateToZero ? _windowSize/2 : 0); if (windowShape == Window::kaiser) { Modified: trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/signalsmith-stretch.h ============================================================================== --- trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/signalsmith-stretch.h Sun Mar 23 17:20:10 2025 (r23076) +++ trunk/OpenMPT/include/SignalsmithStretch/SignalsmithStretch/signalsmith-stretch.h Sun Mar 23 19:48:34 2025 (r23077) @@ -12,8 +12,9 @@ namespace signalsmith { namespace stretch { -template<typename Sample=float> +template<typename Sample=float, class RandomEngine=std::default_random_engine> struct SignalsmithStretch { + static constexpr size_t version[3] = {1, 1, 1}; SignalsmithStretch() : randomEngine(std::random_device{}()) {} SignalsmithStretch(long seed) : randomEngine(seed) {} @@ -36,7 +37,7 @@ inputBuffer.reset(); prevInputOffset = -1; channelBands.assign(channelBands.size(), Band()); - silenceCounter = 2*stft.windowSize(); + silenceCounter = 0; didSeek = false; flushed = true; } @@ -52,18 +53,14 @@ // Manual setup void configure(int nChannels, int blockSamples, int intervalSamples) { channels = nChannels; + stft.setWindow(stft.kaiser, true); stft.resize(channels, blockSamples, intervalSamples); bands = stft.bands(); inputBuffer.resize(channels, blockSamples + intervalSamples + 1); timeBuffer.assign(stft.fftSize(), 0); channelBands.assign(bands*channels, Band()); - // Various phase rotations - rotCentreSpectrum.resize(bands); - rotPrevInterval.assign(bands, 0); - timeShiftPhases(blockSamples*Sample(-0.5), rotCentreSpectrum); - timeShiftPhases(-intervalSamples, rotPrevInterval); - peaks.reserve(bands); + peaks.reserve(bands/2); energy.resize(bands); smoothedEnergy.resize(bands); outputMap.resize(bands); @@ -93,14 +90,21 @@ template<class Inputs> void seek(Inputs &&inputs, int inputSamples, double playbackRate) { inputBuffer.reset(); + Sample totalEnergy = 0; for (int c = 0; c < channels; ++c) { auto &&inputChannel = inputs[c]; auto &&bufferChannel = inputBuffer[c]; int startIndex = std::max<int>(0, inputSamples - stft.windowSize() - stft.interval()); for (int i = startIndex; i < inputSamples; ++i) { - bufferChannel[i] = inputChannel[i]; + Sample s = inputChannel[i]; + totalEnergy += s*s; + bufferChannel[i] = s; } } + if (totalEnergy >= noiseFloor) { + silenceCounter = 0; + silenceFirst = true; + } inputBuffer += inputSamples; didSeek = true; seekTimeFactor = (playbackRate*stft.interval() > 1) ? 1/playbackRate : stft.interval(); @@ -121,7 +125,7 @@ if (silenceFirst) { silenceFirst = false; for (auto &b : channelBands) { - b.input = b.prevInput = b.output = b.prevOutput = 0; + b.input = b.prevInput = b.output = 0; b.inputEnergy = 0; } } @@ -190,7 +194,7 @@ auto channelBands = bandsForChannel(c); auto &&spectrumBands = stft.spectrum[c]; for (int b = 0; b < bands; ++b) { - channelBands[b].input = signalsmith::perf::mul(spectrumBands[b], rotCentreSpectrum[b]); + channelBands[b].input = spectrumBands[b]; } } @@ -213,7 +217,7 @@ auto channelBands = bandsForChannel(c); auto &&spectrumBands = stft.spectrum[c]; for (int b = 0; b < bands; ++b) { - channelBands[b].prevInput = signalsmith::perf::mul(spectrumBands[b], rotCentreSpectrum[b]); + channelBands[b].prevInput = spectrumBands[b]; } } } @@ -227,7 +231,7 @@ auto channelBands = bandsForChannel(c); auto &&spectrumBands = stft.spectrum[c]; for (int b = 0; b < bands; ++b) { - spectrumBands[b] = signalsmith::perf::mul<true>(channelBands[b].output, rotCentreSpectrum[b]); + spectrumBands[b] = channelBands[b].output; } } }); @@ -278,7 +282,7 @@ for (int c = 0; c < channels; ++c) { auto channelBands = bandsForChannel(c); for (int b = 0; b < bands; ++b) { - channelBands[b].prevInput = channelBands[b].prevOutput = 0; + channelBands[b].prevInput = channelBands[b].output = 0; } } flushed = true; @@ -301,23 +305,16 @@ bool didSeek = false, flushed = true; Sample seekTimeFactor = 1; - std::vector<Complex> rotCentreSpectrum, rotPrevInterval; Sample bandToFreq(Sample b) const { return (b + Sample(0.5))/stft.fftSize(); } Sample freqToBand(Sample f) const { return f*stft.fftSize() - Sample(0.5); } - void timeShiftPhases(Sample shiftSamples, std::vector<Complex> &output) const { - for (int b = 0; b < bands; ++b) { - Sample phase = bandToFreq(b)*shiftSamples*Sample(-2*M_PI); - output[b] = {std::cos(phase), std::sin(phase)}; - } - } struct Band { Complex input, prevInput{0}; - Complex output, prevOutput{0}; + Complex output{0}; Sample inputEnergy; }; std::vector<Band> channelBands; @@ -372,7 +369,6 @@ struct Prediction { Sample energy = 0; Complex input; - Complex shortVerticalTwist, longVerticalTwist; Complex makeOutput(Complex phase) { Sample phaseNorm = std::norm(phase); @@ -387,8 +383,8 @@ Prediction * predictionsForChannel(int c) { return channelPredictions.data() + c*bands; } - - std::default_random_engine randomEngine; + + RandomEngine randomEngine; void processSpectrum(bool newSpectrum, Sample timeFactor) { timeFactor = std::max<Sample>(timeFactor, 1/maxCleanStretch); @@ -398,10 +394,16 @@ if (newSpectrum) { for (int c = 0; c < channels; ++c) { auto bins = bandsForChannel(c); + + Complex rot = std::polar(Sample(1), bandToFreq(0)*stft.interval()*Sample(2*M_PI)); + Sample freqStep = bandToFreq(1) - bandToFreq(0); + Complex rotStep = std::polar(Sample(1), freqStep*stft.interval()*Sample(2*M_PI)); + for (int b = 0; b < bands; ++b) { auto &bin = bins[b]; - bin.prevOutput = signalsmith::perf::mul(bin.prevOutput, rotPrevInterval[b]); - bin.prevInput = signalsmith::perf::mul(bin.prevInput, rotPrevInterval[b]); + bin.output = signalsmith::perf::mul(bin.output, rot); + bin.prevInput = signalsmith::perf::mul(bin.prevInput, rot); + rot = signalsmith::perf::mul(rot, rotStep); } } } @@ -441,22 +443,8 @@ auto &outputBin = bins[b]; Complex prevInput = getFractional<&Band::prevInput>(c, lowIndex, fracIndex); Complex freqTwist = signalsmith::perf::mul<true>(prediction.input, prevInput); - Complex phase = signalsmith::perf::mul(outputBin.prevOutput, freqTwist); + Complex phase = signalsmith::perf::mul(outputBin.output, freqTwist); outputBin.output = phase/(std::max(prevEnergy, prediction.energy) + noiseFloor); - - if (b > 0) { - Sample binTimeFactor = randomTimeFactor ? timeFactorDist(randomEngine) : timeFactor; - Complex downInput = getFractional<&Band::input>(c, mapPoint.inputBin - binTimeFactor); - prediction.shortVerticalTwist = signalsmith::perf::mul<true>(prediction.input, downInput); - if (b >= longVerticalStep) { - Complex longDownInput = getFractional<&Band::input>(c, mapPoint.inputBin - longVerticalStep*binTimeFactor); - prediction.longVerticalTwist = signalsmith::perf::mul<true>(prediction.input, longDownInput); - } else { - prediction.longVerticalTwist = 0; - } - } else { - prediction.shortVerticalTwist = prediction.longVerticalTwist = 0; - } } } @@ -479,27 +467,46 @@ auto &outputBin = bins[b]; Complex phase = 0; + auto mapPoint = outputMap[b]; // Upwards vertical steps if (b > 0) { + Sample binTimeFactor = randomTimeFactor ? timeFactorDist(randomEngine) : timeFactor; + Complex downInput = getFractional<&Band::input>(maxChannel, mapPoint.inputBin - binTimeFactor); + Complex shortVerticalTwist = signalsmith::perf::mul<true>(prediction.input, downInput); + auto &downBin = bins[b - 1]; - phase += signalsmith::perf::mul(downBin.output, prediction.shortVerticalTwist); + phase += signalsmith::perf::mul(downBin.output, shortVerticalTwist); if (b >= longVerticalStep) { + Complex longDownInput = getFractional<&Band::input>(maxChannel, mapPoint.inputBin - longVerticalStep*binTimeFactor); + Complex longVerticalTwist = signalsmith::perf::mul<true>(prediction.input, longDownInput); + auto &longDownBin = bins[b - longVerticalStep]; - phase += signalsmith::perf::mul(longDownBin.output, prediction.longVerticalTwist); + phase += signalsmith::perf::mul(longDownBin.output, longVerticalTwist); } } // Downwards vertical steps if (b < bands - 1) { auto &upPrediction = predictions[b + 1]; + auto &upMapPoint = outputMap[b + 1]; + + Sample binTimeFactor = randomTimeFactor ? timeFactorDist(randomEngine) : timeFactor; + Complex downInput = getFractional<&Band::input>(maxChannel, upMapPoint.inputBin - binTimeFactor); + Complex shortVerticalTwist = signalsmith::perf::mul<true>(upPrediction.input, downInput); + auto &upBin = bins[b + 1]; - phase += signalsmith::perf::mul<true>(upBin.output, upPrediction.shortVerticalTwist); + phase += signalsmith::perf::mul<true>(upBin.output, shortVerticalTwist); if (b < bands - longVerticalStep) { auto &longUpPrediction = predictions[b + longVerticalStep]; + auto &longUpMapPoint = outputMap[b + longVerticalStep]; + + Complex longDownInput = getFractional<&Band::input>(maxChannel, longUpMapPoint.inputBin - longVerticalStep*binTimeFactor); + Complex longVerticalTwist = signalsmith::perf::mul<true>(longUpPrediction.input, longDownInput); + auto &longUpBin = bins[b + longVerticalStep]; - phase += signalsmith::perf::mul<true>(longUpBin.output, longUpPrediction.longVerticalTwist); + phase += signalsmith::perf::mul<true>(longUpBin.output, longVerticalTwist); } } @@ -520,11 +527,8 @@ if (newSpectrum) { for (auto &bin : channelBands) { - bin.prevOutput = bin.output; bin.prevInput = bin.input; } - } else { - for (auto &bin : channelBands) bin.prevOutput = bin.output; } } |