diff --git a/src/audio/frame.h b/src/audio/frame.h index 1136196d6ac..cdd7d932eac 100644 --- a/src/audio/frame.h +++ b/src/audio/frame.h @@ -35,16 +35,19 @@ class FramePos final { /// Return a `FramePos` from a given engine sample position. To catch /// "invalid" positions (e.g. when parsing values from control objects), /// use `FramePos::fromEngineSamplePosMaybeInvalid` instead. - static constexpr FramePos fromEngineSamplePos(double engineSamplePos) { - return FramePos(engineSamplePos / mixxx::kEngineChannelCount); + static constexpr FramePos fromEngineSamplePos(double engineSamplePos, + mixxx::audio::ChannelCount channelCount = + mixxx::kEngineChannelCount) { + return FramePos(engineSamplePos / channelCount); } /// Return an engine sample position. The `FramePos` is expected to be /// valid. If invalid positions are possible (e.g. for control object /// values), use `FramePos::toEngineSamplePosMaybeInvalid` instead. - double toEngineSamplePos() const { + double toEngineSamplePos(mixxx::audio::ChannelCount channelCount = + mixxx::kEngineChannelCount) const { DEBUG_ASSERT(isValid()); - double engineSamplePos = value() * mixxx::kEngineChannelCount; + double engineSamplePos = value() * channelCount; // In the rare but possible instance that the position is valid but // the engine sample position is exactly -1.0, we nudge the position // because otherwise fromEngineSamplePosMaybeInvalid() will think @@ -63,11 +66,14 @@ class FramePos final { /// for compatibility with our control objects and legacy parts of the code /// base. Using a different code path based on the output of `isValid()` is /// preferable. - static constexpr FramePos fromEngineSamplePosMaybeInvalid(double engineSamplePos) { + static constexpr FramePos fromEngineSamplePosMaybeInvalid( + double engineSamplePos, + mixxx::audio::ChannelCount channelCount = + mixxx::kEngineChannelCount) { if (engineSamplePos == kLegacyInvalidEnginePosition) { return {}; } - return fromEngineSamplePos(engineSamplePos); + return fromEngineSamplePos(engineSamplePos, channelCount); } /// Return an engine sample position. If the `FramePos` is invalid, @@ -77,11 +83,13 @@ class FramePos final { /// for compatibility with our control objects and legacy parts of the code /// base. Using a different code path based on the output of `isValid()` is /// preferable. - double toEngineSamplePosMaybeInvalid() const { + double toEngineSamplePosMaybeInvalid( + mixxx::audio::ChannelCount channelCount = + mixxx::kEngineChannelCount) const { if (!isValid()) { return kLegacyInvalidEnginePosition; } - return toEngineSamplePos(); + return toEngineSamplePos(channelCount); } /// Return true if the frame position is valid. Any finite value is diff --git a/src/audio/types.h b/src/audio/types.h index 2e4254afe96..c3c2de58d1e 100644 --- a/src/audio/types.h +++ b/src/audio/types.h @@ -80,6 +80,14 @@ class ChannelCount { return ChannelCount(valueFromInt(value)); } + static ChannelCount fromDouble(double value) { + const auto channelCount = ChannelCount(static_cast(value)); + // The channel count should always be an integer value + // and this conversion is supposed to be lossless. + DEBUG_ASSERT(channelCount.toDouble() == value); + return channelCount; + } + static constexpr ChannelCount mono() { return ChannelCount(static_cast(1)); } @@ -88,6 +96,10 @@ class ChannelCount { return ChannelCount(static_cast(2)); } + static constexpr ChannelCount stem() { + return ChannelCount(static_cast(8)); // 4 stereo channels + } + explicit constexpr ChannelCount( value_t value = kValueDefault) : m_value(value) { @@ -115,6 +127,11 @@ class ChannelCount { return value(); } + // Helper cast for COs + constexpr double toDouble() const { + return static_cast(value()); + } + private: value_t m_value; }; diff --git a/src/engine/bufferscalers/enginebufferscale.cpp b/src/engine/bufferscalers/enginebufferscale.cpp index ee9ef873ab4..adf203ad5e6 100644 --- a/src/engine/bufferscalers/enginebufferscale.cpp +++ b/src/engine/bufferscalers/enginebufferscale.cpp @@ -16,12 +16,21 @@ EngineBufferScale::EngineBufferScale() DEBUG_ASSERT(!m_outputSignal.isValid()); } -void EngineBufferScale::setSampleRate( - mixxx::audio::SampleRate sampleRate) { +void EngineBufferScale::setOutputSignal( + mixxx::audio::SampleRate sampleRate, + mixxx::audio::ChannelCount channelCount) { DEBUG_ASSERT(sampleRate.isValid()); + bool changed = false; if (sampleRate != m_outputSignal.getSampleRate()) { m_outputSignal.setSampleRate(sampleRate); - onSampleRateChanged(); + changed = true; + } + if (channelCount != m_outputSignal.getChannelCount()) { + m_outputSignal.setChannelCount(channelCount); + changed = true; + } + if (changed) { + onOutputSignalChanged(); } DEBUG_ASSERT(m_outputSignal.isValid()); } diff --git a/src/engine/bufferscalers/enginebufferscale.h b/src/engine/bufferscalers/enginebufferscale.h index 5480f83312a..4d058f172f7 100644 --- a/src/engine/bufferscalers/enginebufferscale.h +++ b/src/engine/bufferscalers/enginebufferscale.h @@ -42,9 +42,10 @@ class EngineBufferScale : public QObject { m_dPitchRatio = *pPitchRatio; } - // Set the desired output sample rate. - void setSampleRate( - mixxx::audio::SampleRate sampleRate); + // Set the desired output signal. + void setOutputSignal( + mixxx::audio::SampleRate sampleRate, + mixxx::audio::ChannelCount channelCout); const mixxx::audio::SignalInfo& getOutputSignal() const { return m_outputSignal; @@ -66,7 +67,7 @@ class EngineBufferScale : public QObject { private: mixxx::audio::SignalInfo m_outputSignal; - virtual void onSampleRateChanged() = 0; + virtual void onOutputSignalChanged() = 0; protected: double m_dBaseRate; diff --git a/src/engine/bufferscalers/enginebufferscalelinear.cpp b/src/engine/bufferscalers/enginebufferscalelinear.cpp index a461dd21cb6..ef478007125 100644 --- a/src/engine/bufferscalers/enginebufferscalelinear.cpp +++ b/src/engine/bufferscalers/enginebufferscalelinear.cpp @@ -17,8 +17,7 @@ EngineBufferScaleLinear::EngineBufferScaleLinear(ReadAheadManager *pReadAheadMan m_dOldRate(1.0), m_dCurrentFrame(0.0), m_dNextFrame(0.0) { - m_floorSampleOld[0] = 0.0; - m_floorSampleOld[1] = 0.0; + onOutputSignalChanged(); SampleUtil::clear(m_bufferInt, kiLinearScaleReadAheadLength); } @@ -26,6 +25,11 @@ EngineBufferScaleLinear::~EngineBufferScaleLinear() { SampleUtil::free(m_bufferInt); } +void EngineBufferScaleLinear::onOutputSignalChanged() { + m_floorSampleOld.resize(getOutputSignal().getChannelCount()); + std::fill(m_floorSampleOld.begin(), m_floorSampleOld.end(), 0.0); +} + void EngineBufferScaleLinear::setScaleParameters(double base_rate, double* pTempoRatio, double* pPitchRatio) { @@ -40,8 +44,7 @@ void EngineBufferScaleLinear::clear() { // Clear out buffer and saved sample data m_bufferIntSize = 0; m_dNextFrame = 0; - m_floorSampleOld[0] = 0; - m_floorSampleOld[1] = 0; + onOutputSignalChanged(); } // laurent de soras - punked from musicdsp.org (mad props) @@ -85,9 +88,11 @@ double EngineBufferScaleLinear::scaleBuffer( // reset m_floorSampleOld in a way as we were coming from // the other direction SINT iNextSample = getOutputSignal().frames2samples(static_cast(ceil(m_dNextFrame))); - if (iNextSample + 1 < m_bufferIntSize) { - m_floorSampleOld[0] = m_bufferInt[iNextSample]; - m_floorSampleOld[1] = m_bufferInt[iNextSample + 1]; + int chCount = getOutputSignal().getChannelCount(); + if (iNextSample + chCount <= m_bufferIntSize) { + for (int c = 0; c < chCount; c++) { + m_floorSampleOld[c] = m_bufferInt[iNextSample + c]; + } } // if the buffer has extra samples, do a read so RAMAN ends up back where @@ -103,7 +108,7 @@ double EngineBufferScaleLinear::scaleBuffer( //qDebug() << "extra samples" << extra_samples; SINT next_samples_read = m_pReadAheadManager->getNextSamples( - rate_add_new, m_bufferInt, extra_samples); + rate_add_new, m_bufferInt, extra_samples, getOutputSignal().getChannelCount()); frames_read += getOutputSignal().samples2frames(next_samples_read); } // force a buffer read: @@ -145,8 +150,10 @@ SINT EngineBufferScaleLinear::do_copy(CSAMPLE* buf, SINT buf_size) { // to call getNextSamples until you receive the number of samples you // wanted. while (samples_needed > 0) { - SINT read_size = m_pReadAheadManager->getNextSamples(m_dRate, write_buf, - samples_needed); + SINT read_size = m_pReadAheadManager->getNextSamples(m_dRate, + write_buf, + samples_needed, + getOutputSignal().getChannelCount()); if (read_size == 0) { if (++read_failed_count > 1) { break; @@ -168,9 +175,11 @@ SINT EngineBufferScaleLinear::do_copy(CSAMPLE* buf, SINT buf_size) { // blow away the fractional sample position here m_bufferIntSize = 0; // force buffer read m_dNextFrame = 0; - if (read_samples > 1) { - m_floorSampleOld[0] = buf[read_samples - 2]; - m_floorSampleOld[1] = buf[read_samples - 1]; + int chCount = getOutputSignal().getChannelCount(); + if (read_samples > chCount - 1) { + for (int c = 0; c < chCount; c++) { + m_floorSampleOld[c] = buf[read_samples - chCount + c]; + } } return read_samples; } @@ -219,13 +228,12 @@ double EngineBufferScaleLinear::do_scale(CSAMPLE* buf, SINT buf_size) { SINT unscaled_frames_needed = static_cast(frames + m_dNextFrame - floor(m_dNextFrame)); - CSAMPLE floor_sample[2]; - CSAMPLE ceil_sample[2]; + int chCount = getOutputSignal().getChannelCount(); + std::vector floor_sample(chCount); + std::vector ceil_sample(chCount); - floor_sample[0] = 0; - floor_sample[1] = 0; - ceil_sample[0] = 0; - ceil_sample[1] = 0; + std::fill(floor_sample.begin(), floor_sample.end(), 0.0); + std::fill(ceil_sample.begin(), ceil_sample.end(), 0.0); double startFrame = m_dNextFrame; SINT i = 0; @@ -248,27 +256,29 @@ double EngineBufferScaleLinear::do_scale(CSAMPLE* buf, SINT buf_size) { SINT currentFrameFloor = static_cast(floor(m_dCurrentFrame)); + int sampleCount = getOutputSignal().frames2samples(currentFrameFloor); if (currentFrameFloor < 0) { // we have advanced to a new buffer in the previous run, // but the floor still points to the old buffer // so take the cached sample, this happens on slow rates - floor_sample[0] = m_floorSampleOld[0]; - floor_sample[1] = m_floorSampleOld[1]; - ceil_sample[0] = m_bufferInt[0]; - ceil_sample[1] = m_bufferInt[1]; - } else if (getOutputSignal().frames2samples(currentFrameFloor) + 3 < m_bufferIntSize) { + for (int c = 0; c < chCount; c++) { + floor_sample[c] = m_floorSampleOld[c]; + ceil_sample[c] = m_bufferInt[c]; + } + } else if (sampleCount + 2 * chCount - 1 < m_bufferIntSize) { // take floor_sample form the buffer of the previous run - floor_sample[0] = m_bufferInt[getOutputSignal().frames2samples(currentFrameFloor)]; - floor_sample[1] = m_bufferInt[getOutputSignal().frames2samples(currentFrameFloor) + 1]; - ceil_sample[0] = m_bufferInt[getOutputSignal().frames2samples(currentFrameFloor) + 2]; - ceil_sample[1] = m_bufferInt[getOutputSignal().frames2samples(currentFrameFloor) + 3]; + for (int c = 0; c < chCount; c++) { + floor_sample[c] = m_bufferInt[sampleCount + c]; + ceil_sample[c] = m_bufferInt[sampleCount + chCount + c]; + } } else { // if we don't have the ceil_sample in buffer, load some more - if (getOutputSignal().frames2samples(currentFrameFloor) + 1 < m_bufferIntSize) { + if (sampleCount + chCount - 1 < m_bufferIntSize) { // take floor_sample form the buffer of the previous run - floor_sample[0] = m_bufferInt[getOutputSignal().frames2samples(currentFrameFloor)]; - floor_sample[1] = m_bufferInt[getOutputSignal().frames2samples(currentFrameFloor) + 1]; + for (int c = 0; c < chCount; c++) { + floor_sample[c] = m_bufferInt[sampleCount + c]; + } } do { @@ -285,7 +295,9 @@ double EngineBufferScaleLinear::do_scale(CSAMPLE* buf, SINT buf_size) { m_bufferIntSize = m_pReadAheadManager->getNextSamples( rate_new == 0 ? rate_old : rate_new, - m_bufferInt, samples_to_read); + m_bufferInt, + samples_to_read, + getOutputSignal().getChannelCount()); // Note we may get 0 samples once if we just hit a loop trigger, // e.g. when reloop_toggle jumps back to loop_in, or when // moving a loop causes the play position to be moved along. @@ -297,17 +309,20 @@ double EngineBufferScaleLinear::do_scale(CSAMPLE* buf, SINT buf_size) { startFrame -= oldBufferFrames; currentFrameFloor -= oldBufferFrames; - } while (getOutputSignal().frames2samples(currentFrameFloor) + 3 >= m_bufferIntSize); + sampleCount = getOutputSignal().frames2samples(currentFrameFloor); + } while (sampleCount + 2 * chCount - 1 >= m_bufferIntSize); // Now that the buffer is up to date, we can get the value of the sample // at the floor of our position. if (currentFrameFloor >= 0) { // the previous position is in the new buffer - floor_sample[0] = m_bufferInt[getOutputSignal().frames2samples(currentFrameFloor)]; - floor_sample[1] = m_bufferInt[getOutputSignal().frames2samples(currentFrameFloor) + 1]; + for (int c = 0; c < chCount; c++) { + floor_sample[c] = m_bufferInt[sampleCount + c]; + } + } + for (int c = 0; c < chCount; c++) { + ceil_sample[c] = m_bufferInt[sampleCount + chCount + c]; } - ceil_sample[0] = m_bufferInt[getOutputSignal().frames2samples(currentFrameFloor) + 2]; - ceil_sample[1] = m_bufferInt[getOutputSignal().frames2samples(currentFrameFloor) + 3]; } // For the current index, what percentage is it @@ -315,11 +330,11 @@ double EngineBufferScaleLinear::do_scale(CSAMPLE* buf, SINT buf_size) { CSAMPLE frac = static_cast(m_dCurrentFrame) - currentFrameFloor; // Perform linear interpolation - buf[i] = floor_sample[0] + frac * (ceil_sample[0] - floor_sample[0]); - buf[i + 1] = floor_sample[1] + frac * (ceil_sample[1] - floor_sample[1]); + for (int c = 0; c < chCount; c++) { + buf[i + c] = floor_sample[c] + frac * (ceil_sample[c] - floor_sample[c]); + } - m_floorSampleOld[0] = floor_sample[0]; - m_floorSampleOld[1] = floor_sample[1]; + m_floorSampleOld = floor_sample; // increment the index for the next loop m_dNextFrame = m_dCurrentFrame + rate_add; @@ -328,7 +343,7 @@ double EngineBufferScaleLinear::do_scale(CSAMPLE* buf, SINT buf_size) { // samples. This prevents the change from being discontinuous and helps // improve sound quality. rate_add += rate_delta_abs; - i += getOutputSignal().getChannelCount(); + i += chCount; } SampleUtil::clear(&buf[i], buf_size - i); diff --git a/src/engine/bufferscalers/enginebufferscalelinear.h b/src/engine/bufferscalers/enginebufferscalelinear.h index 362bad58898..7775d012bee 100644 --- a/src/engine/bufferscalers/enginebufferscalelinear.h +++ b/src/engine/bufferscalers/enginebufferscalelinear.h @@ -24,7 +24,7 @@ class EngineBufferScaleLinear : public EngineBufferScale { double* pPitchRatio) override; private: - void onSampleRateChanged() override {} + void onOutputSignalChanged() override; double do_scale(CSAMPLE* buf, SINT buf_size); SINT do_copy(CSAMPLE* buf, SINT buf_size); @@ -36,7 +36,7 @@ class EngineBufferScaleLinear : public EngineBufferScale { CSAMPLE* m_bufferInt; SINT m_bufferIntSize; - CSAMPLE m_floorSampleOld[2]; + std::vector m_floorSampleOld; bool m_bClear; double m_dRate; diff --git a/src/engine/bufferscalers/enginebufferscalerubberband.cpp b/src/engine/bufferscalers/enginebufferscalerubberband.cpp index 5e61b4ede74..2f11d75c12e 100644 --- a/src/engine/bufferscalers/enginebufferscalerubberband.cpp +++ b/src/engine/bufferscalers/enginebufferscalerubberband.cpp @@ -1,5 +1,6 @@ #include "engine/bufferscalers/enginebufferscalerubberband.h" +#include #include #include "engine/readaheadmanager.h" @@ -8,6 +9,7 @@ #include "util/defs.h" #include "util/math.h" #include "util/sample.h" +#include "util/timer.h" using RubberBand::RubberBandStretcher; @@ -16,14 +18,14 @@ using RubberBand::RubberBandStretcher; EngineBufferScaleRubberBand::EngineBufferScaleRubberBand( ReadAheadManager* pReadAheadManager) : m_pReadAheadManager(pReadAheadManager), - m_buffers{mixxx::SampleBuffer(MAX_BUFFER_LEN), mixxx::SampleBuffer(MAX_BUFFER_LEN)}, - m_bufferPtrs{m_buffers[0].data(), m_buffers[1].data()}, + m_buffers(), + m_bufferPtrs(), m_interleavedReadBuffer(MAX_BUFFER_LEN), m_bBackwards(false), m_useEngineFiner(false) { // Initialize the internal buffers to prevent re-allocations // in the real-time thread. - onSampleRateChanged(); + onOutputSignalChanged(); } void EngineBufferScaleRubberBand::setScaleParameters(double base_rate, @@ -90,14 +92,34 @@ void EngineBufferScaleRubberBand::setScaleParameters(double base_rate, m_dPitchRatio = *pPitchRatio; } -void EngineBufferScaleRubberBand::onSampleRateChanged() { +void EngineBufferScaleRubberBand::onOutputSignalChanged() { // TODO: Resetting the sample rate will cause internal // memory allocations that may block the real-time thread. // When is this function actually invoked?? - if (!getOutputSignal().isValid()) { + VERIFY_OR_DEBUG_ASSERT(getOutputSignal().isValid()) { m_pRubberBand.reset(); return; } + + uint8_t channelCount = getOutputSignal().getChannelCount(); + if (m_buffers.size() != channelCount) { + m_buffers.resize(channelCount); + } + + if (m_bufferPtrs.size() != channelCount) { + m_bufferPtrs.resize(channelCount); + } + + m_pRubberBand.reset(); + + for (int c = 0; c < channelCount; c++) { + if (m_buffers[c].size() == MAX_BUFFER_LEN) { + continue; + } + m_buffers[c] = mixxx::SampleBuffer(MAX_BUFFER_LEN); + m_bufferPtrs[c] = m_buffers[c].data(); + } + RubberBandStretcher::Options rubberbandOptions = RubberBandStretcher::OptionProcessRealTime; #if RUBBERBANDV3 @@ -131,6 +153,9 @@ void EngineBufferScaleRubberBand::clear() { SINT EngineBufferScaleRubberBand::retrieveAndDeinterleave( CSAMPLE* pBuffer, SINT frames) { + VERIFY_OR_DEBUG_ASSERT(m_pRubberBand) { + return 0; + } const SINT frames_available = m_pRubberBand->available(); // NOTE: If we still need to throw away padding, then we can also // immediately read those frames in addition to the frames we actually @@ -153,10 +178,36 @@ SINT EngineBufferScaleRubberBand::retrieveAndDeinterleave( } DEBUG_ASSERT(received_frames <= frames); - SampleUtil::interleaveBuffer(pBuffer, - m_buffers[0].data(frame_offset), - m_buffers[1].data(frame_offset), - received_frames); + + switch (getOutputSignal().getChannelCount()) { + case mixxx::audio::ChannelCount::stereo(): + SampleUtil::interleaveBuffer(pBuffer, + m_buffers[0].data(frame_offset), + m_buffers[1].data(frame_offset), + received_frames); + break; + case mixxx::audio::ChannelCount::stem(): + SampleUtil::interleaveBuffer(pBuffer, + m_buffers[0].data(frame_offset), + m_buffers[1].data(frame_offset), + m_buffers[2].data(frame_offset), + m_buffers[3].data(frame_offset), + m_buffers[4].data(frame_offset), + m_buffers[5].data(frame_offset), + m_buffers[6].data(frame_offset), + m_buffers[7].data(frame_offset), + received_frames); + break; + default: { + int chCount = getOutputSignal().getChannelCount(); + for (SINT i = 0; i < frames; ++i) { + for (int channel = 0; channel < chCount; channel++) { + m_buffers[channel].data()[i] = + pBuffer[i * chCount + channel]; + } + } + } break; + } return received_frames; } @@ -164,13 +215,42 @@ SINT EngineBufferScaleRubberBand::retrieveAndDeinterleave( void EngineBufferScaleRubberBand::deinterleaveAndProcess( const CSAMPLE* pBuffer, SINT frames) { + VERIFY_OR_DEBUG_ASSERT(m_pRubberBand) { + return; + } DEBUG_ASSERT(frames <= static_cast(m_buffers[0].size())); - SampleUtil::deinterleaveBuffer( - m_buffers[0].data(), - m_buffers[1].data(), - pBuffer, - frames); + switch (getOutputSignal().getChannelCount()) { + case mixxx::audio::ChannelCount::stereo(): + SampleUtil::deinterleaveBuffer( + m_buffers[0].data(), + m_buffers[1].data(), + pBuffer, + frames); + break; + case mixxx::audio::ChannelCount::stem(): + SampleUtil::deinterleaveBuffer( + m_buffers[0].data(), + m_buffers[1].data(), + m_buffers[2].data(), + m_buffers[3].data(), + m_buffers[4].data(), + m_buffers[5].data(), + m_buffers[6].data(), + m_buffers[7].data(), + pBuffer, + frames); + break; + default: { + int chCount = getOutputSignal().getChannelCount(); + for (SINT i = 0; i < frames; ++i) { + for (int channel = 0; channel < chCount; channel++) { + m_buffers[channel].data()[i] = + pBuffer[i * chCount + channel]; + } + } + } break; + } m_pRubberBand->process(m_bufferPtrs.data(), frames, @@ -180,6 +260,9 @@ void EngineBufferScaleRubberBand::deinterleaveAndProcess( double EngineBufferScaleRubberBand::scaleBuffer( CSAMPLE* pOutputBuffer, SINT iOutputBufferSize) { + VERIFY_OR_DEBUG_ASSERT(m_pRubberBand) { + return 0.0; + } if (m_dBaseRate == 0.0 || m_dTempoRatio == 0.0) { SampleUtil::clear(pOutputBuffer, iOutputBufferSize); // No actual samples/frames have been read from the @@ -215,7 +298,8 @@ double EngineBufferScaleRubberBand::scaleBuffer( // are going forward or backward. (m_bBackwards ? -1.0 : 1.0) * m_dBaseRate * m_dTempoRatio, m_interleavedReadBuffer.data(), - getOutputSignal().frames2samples(next_block_frames_required)); + getOutputSignal().frames2samples(next_block_frames_required), + getOutputSignal().getChannelCount()); const SINT available_frames = getOutputSignal().samples2frames(available_samples); if (available_frames > 0) { @@ -262,7 +346,7 @@ bool EngineBufferScaleRubberBand::isEngineFinerAvailable() { void EngineBufferScaleRubberBand::useEngineFiner(bool enable) { if (isEngineFinerAvailable()) { m_useEngineFiner = enable; - onSampleRateChanged(); + onOutputSignalChanged(); } } @@ -315,8 +399,9 @@ void EngineBufferScaleRubberBand::reset() { // for more information. size_t remaining_padding = getPreferredStartPad(); const size_t block_size = std::min(remaining_padding, m_buffers[0].size()); - std::fill_n(m_buffers[0].span().begin(), block_size, 0.0f); - std::fill_n(m_buffers[1].span().begin(), block_size, 0.0f); + for (auto& buffer : m_buffers) { + std::fill_n(buffer.span().begin(), block_size, 0.0f); + } while (remaining_padding > 0) { const size_t pad_samples = std::min(remaining_padding, block_size); m_pRubberBand->process(m_bufferPtrs.data(), pad_samples, false); diff --git a/src/engine/bufferscalers/enginebufferscalerubberband.h b/src/engine/bufferscalers/enginebufferscalerubberband.h index bfd42bc58a1..538aa124ee4 100644 --- a/src/engine/bufferscalers/enginebufferscalerubberband.h +++ b/src/engine/bufferscalers/enginebufferscalerubberband.h @@ -42,7 +42,7 @@ class EngineBufferScaleRubberBand final : public EngineBufferScale { private: // Reset RubberBand library with new audio signal - void onSampleRateChanged() override; + void onOutputSignalChanged() override; /// Calls `m_pRubberBand->getPreferredStartPad()`, with backwards /// compatibility for older librubberband versions. @@ -67,10 +67,10 @@ class EngineBufferScaleRubberBand final : public EngineBufferScale { /// The audio buffers samples used to send audio to Rubber Band and to /// receive processed audio from Rubber Band. This is needed because Mixxx /// uses interleaved buffers in most other places. - std::array m_buffers; + std::vector m_buffers; /// These point to the buffers in `m_buffers`. They can be defined here /// since this object cannot be moved or copied. - std::array m_bufferPtrs; + std::vector m_bufferPtrs; /// Contains interleaved samples read from `m_pReadAheadManager`. These need /// to be deinterleaved before they can be passed to Rubber Band. diff --git a/src/engine/bufferscalers/enginebufferscalest.cpp b/src/engine/bufferscalers/enginebufferscalest.cpp index 1d46edbf173..76d2af6939e 100644 --- a/src/engine/bufferscalers/enginebufferscalest.cpp +++ b/src/engine/bufferscalers/enginebufferscalest.cpp @@ -24,7 +24,7 @@ constexpr SINT kSeekOffsetFramesV20101 = 429; // TODO() Compensate that. This is probably cause by the delayed adoption of pitch changes due // to the SoundTouch chunk size. -constexpr SINT kBackBufferSize = 1024; +constexpr SINT kBackBufferSize = 512 * mixxx::audio::ChannelCount::stem(); } // namespace @@ -33,13 +33,12 @@ EngineBufferScaleST::EngineBufferScaleST(ReadAheadManager* pReadAheadManager) m_pSoundTouch(std::make_unique()), m_bufferBack(kBackBufferSize), m_bBackwards(false) { - m_pSoundTouch->setChannels(getOutputSignal().getChannelCount()); m_pSoundTouch->setRate(m_dBaseRate); m_pSoundTouch->setPitch(1.0); m_pSoundTouch->setSetting(SETTING_USE_QUICKSEEK, 1); // Initialize the internal buffers to prevent re-allocations // in the real-time thread. - onSampleRateChanged(); + onOutputSignalChanged(); } EngineBufferScaleST::~EngineBufferScaleST() { @@ -91,12 +90,13 @@ void EngineBufferScaleST::setScaleParameters(double base_rate, // changed direction. I removed it because this is handled by EngineBuffer. } -void EngineBufferScaleST::onSampleRateChanged() { +void EngineBufferScaleST::onOutputSignalChanged() { m_bufferBack.clear(); if (!getOutputSignal().isValid()) { return; } m_pSoundTouch->setSampleRate(getOutputSignal().getSampleRate()); + m_pSoundTouch->setChannels(getOutputSignal().getChannelCount()); // Setting the tempo to a very low value will force SoundTouch // to preallocate buffers large enough to (almost certainly) @@ -149,7 +149,8 @@ double EngineBufferScaleST::scaleBuffer( // are going forward or backward. (m_bBackwards ? -1.0 : 1.0) * m_effectiveRate, m_bufferBack.data(), - m_bufferBack.size()); + m_bufferBack.size(), + getOutputSignal().getChannelCount()); SINT iAvailFrames = getOutputSignal().samples2frames(iAvailSamples); if (iAvailFrames > 0) { diff --git a/src/engine/bufferscalers/enginebufferscalest.h b/src/engine/bufferscalers/enginebufferscalest.h index db3fb773b0a..66e5543c681 100644 --- a/src/engine/bufferscalers/enginebufferscalest.h +++ b/src/engine/bufferscalers/enginebufferscalest.h @@ -31,7 +31,7 @@ class EngineBufferScaleST : public EngineBufferScale { void clear() override; private: - void onSampleRateChanged() override; + void onOutputSignalChanged() override; // The read-ahead manager that we use to fetch samples ReadAheadManager* m_pReadAheadManager; diff --git a/src/engine/cachingreader/cachingreader.cpp b/src/engine/cachingreader/cachingreader.cpp index 45a83cf525a..8d3f10c096f 100644 --- a/src/engine/cachingreader/cachingreader.cpp +++ b/src/engine/cachingreader/cachingreader.cpp @@ -19,7 +19,7 @@ mixxx::Logger kLogger("CachingReader"); constexpr SINT kDefaultHintFrames = 1024; // With CachingReaderChunk::kFrames = 8192 each chunk consumes -// 8192 frames * 2 channels/frame * 4-bytes per sample = 65 kB. +// 8192 frames * 2 channels/frame * 4-bytes per sample = 65 kB for stereo frame. // // 80 chunks -> 5120 KB = 5 MB // @@ -37,8 +37,7 @@ constexpr SINT kNumberOfCachedChunksInMemory = 80; } // anonymous namespace -CachingReader::CachingReader(const QString& group, - UserSettingsPointer config) +CachingReader::CachingReader(const QString& group, UserSettingsPointer config) : m_pConfig(config), // Limit the number of in-flight requests to the worker. This should // prevent to overload the worker when it is not able to fetch those @@ -287,17 +286,21 @@ void CachingReader::process() { } } -CachingReader::ReadResult CachingReader::read(SINT startSample, SINT numSamples, bool reverse, CSAMPLE* buffer) { +CachingReader::ReadResult CachingReader::read(SINT startSample, + SINT numSamples, + bool reverse, + CSAMPLE* buffer, + mixxx::audio::ChannelCount channelCount) { // Check for bad inputs // Refuse to read from an invalid position - VERIFY_OR_DEBUG_ASSERT(startSample % CachingReaderChunk::kChannels == 0) { + VERIFY_OR_DEBUG_ASSERT(startSample % channelCount == 0) { kLogger.critical() << "Invalid arguments for read():" << "startSample =" << startSample; return ReadResult::UNAVAILABLE; } // Refuse to read from an invalid number of samples - VERIFY_OR_DEBUG_ASSERT(numSamples % CachingReaderChunk::kChannels == 0) { + VERIFY_OR_DEBUG_ASSERT(numSamples % channelCount == 0) { kLogger.critical() << "Invalid arguments for read():" << "numSamples =" << numSamples; @@ -344,8 +347,8 @@ CachingReader::ReadResult CachingReader::read(SINT startSample, SINT numSamples, auto remainingFrameIndexRange = mixxx::IndexRange::forward( - CachingReaderChunk::samples2frames(sample), - CachingReaderChunk::samples2frames(numSamples)); + CachingReaderChunk::samples2frames(sample, channelCount), + CachingReaderChunk::samples2frames(numSamples, channelCount)); DEBUG_ASSERT(!remainingFrameIndexRange.empty()); auto result = ReadResult::AVAILABLE; @@ -370,7 +373,8 @@ CachingReader::ReadResult CachingReader::read(SINT startSample, SINT numSamples, << m_readableFrameIndexRange.start(); } const SINT prerollFrames = prerollFrameIndexRange.length(); - const SINT prerollSamples = CachingReaderChunk::frames2samples(prerollFrames); + const SINT prerollSamples = CachingReaderChunk::frames2samples( + prerollFrames, channelCount); DEBUG_ASSERT(samplesRemaining >= prerollSamples); if (reverse) { SampleUtil::clear(&buffer[samplesRemaining - prerollSamples], prerollSamples); @@ -436,11 +440,13 @@ CachingReader::ReadResult CachingReader::read(SINT startSample, SINT numSamples, bufferedFrameIndexRange = pChunk->readBufferedSampleFramesReverse( &buffer[samplesRemaining], + channelCount, remainingFrameIndexRange); } else { bufferedFrameIndexRange = pChunk->readBufferedSampleFrames( buffer, + channelCount, remainingFrameIndexRange); } } else { @@ -482,7 +488,8 @@ CachingReader::ReadResult CachingReader::read(SINT startSample, SINT numSamples, << "Inserting" << paddingFrameIndexRange.length() << "frames of silence for unreadable audio data"; - SINT paddingSamples = CachingReaderChunk::frames2samples(paddingFrameIndexRange.length()); + SINT paddingSamples = CachingReaderChunk::frames2samples( + paddingFrameIndexRange.length(), channelCount); DEBUG_ASSERT(samplesRemaining >= paddingSamples); if (reverse) { SampleUtil::clear(&buffer[samplesRemaining - paddingSamples], paddingSamples); @@ -494,8 +501,8 @@ CachingReader::ReadResult CachingReader::read(SINT startSample, SINT numSamples, remainingFrameIndexRange.shrinkFront(paddingFrameIndexRange.length()); result = ReadResult::PARTIALLY_AVAILABLE; } - const SINT chunkSamples = - CachingReaderChunk::frames2samples(bufferedFrameIndexRange.length()); + const SINT chunkSamples = CachingReaderChunk::frames2samples( + bufferedFrameIndexRange.length(), channelCount); DEBUG_ASSERT(chunkSamples > 0); if (!reverse) { buffer += chunkSamples; diff --git a/src/engine/cachingreader/cachingreader.h b/src/engine/cachingreader/cachingreader.h index 6d18841fd77..bd9a6b70fe5 100644 --- a/src/engine/cachingreader/cachingreader.h +++ b/src/engine/cachingreader/cachingreader.h @@ -100,7 +100,11 @@ class CachingReader : public QObject { // buffer. It always writes numSamples to the buffer and otherwise // returns ReadResult::UNAVAILABLE. // It support reading stereo samples in reverse (backward) order. - virtual ReadResult read(SINT startSample, SINT numSamples, bool reverse, CSAMPLE* buffer); + virtual ReadResult read(SINT startSample, + SINT numSamples, + bool reverse, + CSAMPLE* buffer, + mixxx::audio::ChannelCount channelCount); // Issue a list of hints, but check whether any of the hints request a chunk // that is not in the cache. If any hints do request a chunk not in cache, @@ -122,6 +126,7 @@ class CachingReader : public QObject { void trackLoading(); void trackLoaded(TrackPointer pTrack, mixxx::audio::SampleRate trackSampleRate, + mixxx::audio::ChannelCount trackChannelCount, double trackNumSamples); void trackLoadFailed(TrackPointer pTrack, const QString& reason); diff --git a/src/engine/cachingreader/cachingreaderchunk.cpp b/src/engine/cachingreader/cachingreaderchunk.cpp index 1217dc00f47..ff4c6a64b57 100644 --- a/src/engine/cachingreader/cachingreaderchunk.cpp +++ b/src/engine/cachingreader/cachingreaderchunk.cpp @@ -17,10 +17,11 @@ constexpr SINT kInvalidChunkIndex = -1; } // anonymous namespace CachingReaderChunk::CachingReaderChunk( - mixxx::SampleBuffer::WritableSlice sampleBuffer) + mixxx::SampleBuffer::WritableSlice sampleBuffer, mixxx::audio::ChannelCount channelCount) : m_index(kInvalidChunkIndex), - m_sampleBuffer(std::move(sampleBuffer)) { - DEBUG_ASSERT(m_sampleBuffer.length() == kSamples); + m_sampleBuffer(std::move(sampleBuffer)), + m_channelCount(channelCount) { + DEBUG_ASSERT(m_sampleBuffer.length() == samples()); } void CachingReaderChunk::init(SINT index) { @@ -49,17 +50,29 @@ mixxx::IndexRange CachingReaderChunk::bufferSampleFrames( mixxx::SampleBuffer::WritableSlice tempOutputBuffer) { DEBUG_ASSERT(m_index != kInvalidChunkIndex); const auto sourceFrameIndexRange = frameIndexRange(pAudioSource); - mixxx::AudioSourceStereoProxy audioSourceProxy( - pAudioSource, - tempOutputBuffer); - DEBUG_ASSERT( - audioSourceProxy.getSignalInfo().getChannelCount() == - kChannels); - m_bufferedSampleFrames = - audioSourceProxy.readSampleFrames( - mixxx::WritableSampleFrames( - sourceFrameIndexRange, - mixxx::SampleBuffer::WritableSlice(m_sampleBuffer))); + + if (pAudioSource->getSignalInfo().getChannelCount() % + mixxx::audio::ChannelCount::stereo() != + 0) { + // This happens if the audio source only contain a mono channel + mixxx::AudioSourceStereoProxy audioSourceProxy( + pAudioSource, + tempOutputBuffer); + DEBUG_ASSERT( + audioSourceProxy.getSignalInfo().getChannelCount() == + mixxx::audio::ChannelCount::stereo()); + m_bufferedSampleFrames = + audioSourceProxy.readSampleFrames( + mixxx::WritableSampleFrames( + sourceFrameIndexRange, + mixxx::SampleBuffer::WritableSlice(m_sampleBuffer))); + } else { + m_bufferedSampleFrames = + pAudioSource->readSampleFrames( + mixxx::WritableSampleFrames( + sourceFrameIndexRange, + mixxx::SampleBuffer::WritableSlice(m_sampleBuffer))); + } DEBUG_ASSERT(m_bufferedSampleFrames.frameIndexRange().empty() || m_bufferedSampleFrames.frameIndexRange().isSubrangeOf(sourceFrameIndexRange)); return m_bufferedSampleFrames.frameIndexRange(); @@ -67,16 +80,20 @@ mixxx::IndexRange CachingReaderChunk::bufferSampleFrames( mixxx::IndexRange CachingReaderChunk::readBufferedSampleFrames( CSAMPLE* sampleBuffer, + mixxx::audio::ChannelCount channelCount, const mixxx::IndexRange& frameIndexRange) const { DEBUG_ASSERT(m_index != kInvalidChunkIndex); const auto copyableFrameIndexRange = intersect(frameIndexRange, m_bufferedSampleFrames.frameIndexRange()); if (!copyableFrameIndexRange.empty()) { - const SINT dstSampleOffset = - frames2samples(copyableFrameIndexRange.start() - frameIndexRange.start()); - const SINT srcSampleOffset = - frames2samples(copyableFrameIndexRange.start() - m_bufferedSampleFrames.frameIndexRange().start()); - const SINT sampleCount = frames2samples(copyableFrameIndexRange.length()); + const SINT dstSampleOffset = frames2samples( + copyableFrameIndexRange.start() - frameIndexRange.start(), + channelCount); + const SINT srcSampleOffset = frames2samples( + copyableFrameIndexRange.start() - + m_bufferedSampleFrames.frameIndexRange().start(), + channelCount); + const SINT sampleCount = frames2samples(copyableFrameIndexRange.length(), channelCount); SampleUtil::copy( sampleBuffer + dstSampleOffset, m_bufferedSampleFrames.readableData(srcSampleOffset), @@ -87,16 +104,20 @@ mixxx::IndexRange CachingReaderChunk::readBufferedSampleFrames( mixxx::IndexRange CachingReaderChunk::readBufferedSampleFramesReverse( CSAMPLE* reverseSampleBuffer, + mixxx::audio::ChannelCount channelCount, const mixxx::IndexRange& frameIndexRange) const { DEBUG_ASSERT(m_index != kInvalidChunkIndex); const auto copyableFrameIndexRange = intersect(frameIndexRange, m_bufferedSampleFrames.frameIndexRange()); if (!copyableFrameIndexRange.empty()) { - const SINT dstSampleOffset = - frames2samples(copyableFrameIndexRange.start() - frameIndexRange.start()); - const SINT srcSampleOffset = - frames2samples(copyableFrameIndexRange.start() - m_bufferedSampleFrames.frameIndexRange().start()); - const SINT sampleCount = frames2samples(copyableFrameIndexRange.length()); + const SINT dstSampleOffset = frames2samples( + copyableFrameIndexRange.start() - frameIndexRange.start(), + channelCount); + const SINT srcSampleOffset = frames2samples( + copyableFrameIndexRange.start() - + m_bufferedSampleFrames.frameIndexRange().start(), + channelCount); + const SINT sampleCount = frames2samples(copyableFrameIndexRange.length(), channelCount); SampleUtil::copyReverse( reverseSampleBuffer - dstSampleOffset - sampleCount, m_bufferedSampleFrames.readableData(srcSampleOffset), @@ -107,7 +128,7 @@ mixxx::IndexRange CachingReaderChunk::readBufferedSampleFramesReverse( CachingReaderChunkForOwner::CachingReaderChunkForOwner( mixxx::SampleBuffer::WritableSlice sampleBuffer) - : CachingReaderChunk(std::move(sampleBuffer)), + : CachingReaderChunk(std::move(sampleBuffer), kMaxSupportedChannels), m_state(FREE), m_pPrev(nullptr), m_pNext(nullptr) { diff --git a/src/engine/cachingreader/cachingreaderchunk.h b/src/engine/cachingreader/cachingreaderchunk.h index c8c04e0b1b3..58062e9fc2a 100644 --- a/src/engine/cachingreader/cachingreaderchunk.h +++ b/src/engine/cachingreader/cachingreaderchunk.h @@ -23,22 +23,33 @@ class CachingReaderChunk { // easier memory alignment. // TODO(XXX): The optimum value of the "constant" kFrames depends // on the properties of the AudioSource as the remarks above suggest! - static constexpr mixxx::audio::ChannelCount kChannels = mixxx::kEngineChannelCount; + static constexpr mixxx::audio::ChannelCount kMaxSupportedChannels = + mixxx::audio::ChannelCount::stem(); static constexpr SINT kFrames = 8192; // ~ 170 ms at 48 kHz - static constexpr SINT kSamples = kFrames * kChannels; + static constexpr SINT kSamples = kFrames * kMaxSupportedChannels; + + constexpr SINT samples() noexcept { + return kFrames * m_channelCount; + } + + mixxx::audio::ChannelCount channelCount() const { + return m_channelCount; + } // Converts frames to samples - static constexpr SINT frames2samples(SINT frames) noexcept { - return frames * kChannels; + static constexpr SINT frames2samples( + SINT frames, mixxx::audio::ChannelCount channelCount) noexcept { + return frames * channelCount; } - static constexpr double dFrames2samples(SINT frames) noexcept { - return static_cast(frames) * kChannels; + static constexpr double dFrames2samples( + SINT frames, mixxx::audio::ChannelCount channelCount) noexcept { + return static_cast(frames) * channelCount; } // Converts samples to frames - static SINT samples2frames(SINT samples) { - DEBUG_ASSERT(0 == (samples % kChannels)); - return samples / kChannels; - } + static SINT samples2frames(SINT samples, mixxx::audio::ChannelCount channelCount) { + DEBUG_ASSERT(0 == (samples % channelCount)); + return samples / channelCount; + } // Returns the corresponding chunk index for a frame index static SINT indexForFrame( @@ -67,24 +78,25 @@ class CachingReaderChunk { const mixxx::AudioSourcePointer& pAudioSource, mixxx::SampleBuffer::WritableSlice tempOutputBuffer); - mixxx::IndexRange readBufferedSampleFrames( - CSAMPLE* sampleBuffer, + mixxx::IndexRange readBufferedSampleFrames(CSAMPLE* sampleBuffer, + mixxx::audio::ChannelCount channelCount, const mixxx::IndexRange& frameIndexRange) const; mixxx::IndexRange readBufferedSampleFramesReverse( CSAMPLE* reverseSampleBuffer, + mixxx::audio::ChannelCount channelCount, const mixxx::IndexRange& frameIndexRange) const; -protected: - explicit CachingReaderChunk( - mixxx::SampleBuffer::WritableSlice sampleBuffer); + protected: + explicit CachingReaderChunk(mixxx::SampleBuffer::WritableSlice sampleBuffer, + mixxx::audio::ChannelCount channelCount); virtual ~CachingReaderChunk() = default; void init(SINT index); -private: - SINT frameIndexOffset() const noexcept { + private: + SINT frameIndexOffset() const noexcept { return m_index * kFrames; - } + } SINT m_index; @@ -92,6 +104,7 @@ class CachingReaderChunk { // set the corresponding frame index range. mixxx::SampleBuffer::WritableSlice m_sampleBuffer; mixxx::ReadableSampleFrames m_bufferedSampleFrames; + mixxx::audio::ChannelCount m_channelCount; }; // This derived class is only accessible for the cache as the owner, @@ -99,22 +112,22 @@ class CachingReaderChunk { // the worker thread is in control. class CachingReaderChunkForOwner: public CachingReaderChunk { public: - explicit CachingReaderChunkForOwner( - mixxx::SampleBuffer::WritableSlice sampleBuffer); - ~CachingReaderChunkForOwner() override = default; + explicit CachingReaderChunkForOwner( + mixxx::SampleBuffer::WritableSlice sampleBuffer); + ~CachingReaderChunkForOwner() override = default; - void init(SINT index); - void free(); + void init(SINT index); + void free(); - enum State { - FREE, - READY, - READ_PENDING - }; + enum State { + FREE, + READY, + READ_PENDING + }; - State getState() const noexcept { + State getState() const noexcept { return m_state; - } + } // The state is controlled by the cache as the owner of each chunk! void giveToWorker() { diff --git a/src/engine/cachingreader/cachingreaderworker.cpp b/src/engine/cachingreader/cachingreaderworker.cpp index 872459c09ca..46d081d34cc 100644 --- a/src/engine/cachingreader/cachingreaderworker.cpp +++ b/src/engine/cachingreader/cachingreaderworker.cpp @@ -82,7 +82,7 @@ ReaderStatusUpdate CachingReaderWorker::processReadRequest( // to further checks whether a automatic offset adjustment is possible or a the // sample position metadata shall be treated as outdated. // Failures of the sanity check only result in an entry into the log at the moment. - verifyFirstSound(pChunk); + verifyFirstSound(pChunk, m_pAudioSource->getSignalInfo().getChannelCount()); ReaderStatusUpdate result; result.init(status, pChunk, m_pAudioSource ? m_pAudioSource->frameIndexRange() : mixxx::IndexRange()); @@ -187,7 +187,7 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { } mixxx::AudioSource::OpenParams config; - config.setChannelCount(CachingReaderChunk::kChannels); + config.setChannelCount(CachingReaderChunk::kMaxSupportedChannels); m_pAudioSource = SoundSourceProxy(pTrack).openAudioSource(config); if (!m_pAudioSource) { kLogger.warning() @@ -234,8 +234,8 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { // Emit that the track is loaded. const double sampleCount = - CachingReaderChunk::dFrames2samples( - m_pAudioSource->frameLength()); + CachingReaderChunk::dFrames2samples(m_pAudioSource->frameLength(), + m_pAudioSource->getSignalInfo().getChannelCount()); // This code is a workaround until we have found a better solution to // verify and correct offsets. @@ -252,6 +252,7 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { emit trackLoaded( pTrack, m_pAudioSource->getSignalInfo().getSampleRate(), + m_pAudioSource->getSignalInfo().getChannelCount(), sampleCount); } @@ -261,7 +262,8 @@ void CachingReaderWorker::quitWait() { wait(); } -void CachingReaderWorker::verifyFirstSound(const CachingReaderChunk* pChunk) { +void CachingReaderWorker::verifyFirstSound(const CachingReaderChunk* pChunk, + mixxx::audio::ChannelCount channelCount) { if (!m_firstSoundFrameToVerify.isValid()) { return; } @@ -271,11 +273,13 @@ void CachingReaderWorker::verifyFirstSound(const CachingReaderChunk* pChunk) { m_firstSoundFrameToVerify.toLowerFrameBoundary() .value())); if (pChunk->getIndex() == firstSoundIndex) { - CSAMPLE sampleBuffer[kNumSoundFrameToVerify * mixxx::kEngineChannelCount]; + mixxx::SampleBuffer sampleBuffer(kNumSoundFrameToVerify * channelCount); SINT end = static_cast(m_firstSoundFrameToVerify.toLowerFrameBoundary().value()); - pChunk->readBufferedSampleFrames(sampleBuffer, + pChunk->readBufferedSampleFrames(sampleBuffer.data(), + channelCount, mixxx::IndexRange::forward(end - 1, kNumSoundFrameToVerify)); - if (AnalyzerSilence::verifyFirstSound(std::span(sampleBuffer), + // TODO support multi channel + if (AnalyzerSilence::verifyFirstSound(sampleBuffer.span(), mixxx::audio::FramePos(1))) { qDebug() << "First sound found at the previously stored position"; } else { diff --git a/src/engine/cachingreader/cachingreaderworker.h b/src/engine/cachingreader/cachingreaderworker.h index 309edbd1d05..9c10d9431ef 100644 --- a/src/engine/cachingreader/cachingreaderworker.h +++ b/src/engine/cachingreader/cachingreaderworker.h @@ -114,7 +114,10 @@ class CachingReaderWorker : public EngineWorker { signals: // Emitted once a new track is loaded and ready to be read from. void trackLoading(); - void trackLoaded(TrackPointer pTrack, mixxx::audio::SampleRate sampleRate, double numSamples); + void trackLoaded(TrackPointer pTrack, + mixxx::audio::SampleRate sampleRate, + mixxx::audio::ChannelCount channelCount, + double numSamples); void trackLoadFailed(TrackPointer pTrack, const QString& reason); private: @@ -148,7 +151,8 @@ class CachingReaderWorker : public EngineWorker { ReaderStatusUpdate processReadRequest( const CachingReaderChunkReadRequest& request); - void verifyFirstSound(const CachingReaderChunk* pChunk); + void verifyFirstSound(const CachingReaderChunk* pChunk, + mixxx::audio::ChannelCount channelCount); // The current audio source of the track loaded mixxx::AudioSourcePointer m_pAudioSource; diff --git a/src/engine/channels/enginedeck.cpp b/src/engine/channels/enginedeck.cpp index 65e38b5ee60..94aff40268d 100644 --- a/src/engine/channels/enginedeck.cpp +++ b/src/engine/channels/enginedeck.cpp @@ -44,6 +44,34 @@ EngineDeck::~EngineDeck() { delete m_pPregain; } +void EngineDeck::processStem(CSAMPLE* pOut, const int iBufferSize) { + int stereoChannelCount = m_pBuffer->getChannelCount() / mixxx::kEngineChannelCount; + auto allChannelBufferSize = iBufferSize * stereoChannelCount; + if (m_stemBuffer.size() < allChannelBufferSize) { + m_stemBuffer = mixxx::SampleBuffer(allChannelBufferSize); + } + m_pBuffer->process(m_stemBuffer.data(), allChannelBufferSize); + + // TODO(XXX): process effects per stems + + for (int i = 0; i < iBufferSize / 2; i++) { + for (int c = 0; c < stereoChannelCount; c++) { + // TODO(XXX): apply stem gain or skip muted stem + if (!c) { + pOut[2 * i] = m_stemBuffer.data()[2 * stereoChannelCount * i]; + pOut[2 * i + 1] = m_stemBuffer.data()[2 * stereoChannelCount * i + 1]; + } else { + pOut[2 * i] += m_stemBuffer.data()[2 * stereoChannelCount * i + 2 * c]; + pOut[2 * i + 1] += + m_stemBuffer + .data()[2 * stereoChannelCount * i + + 2 * c + 1]; + } + } + } + // TODO(XXX): process stem DSP +} + void EngineDeck::process(CSAMPLE* pOut, const int iBufferSize) { // Feed the incoming audio through if passthrough is active const CSAMPLE* sampleBuffer = m_sampleBuffer; // save pointer on stack @@ -61,7 +89,13 @@ void EngineDeck::process(CSAMPLE* pOut, const int iBufferSize) { } // Process the raw audio - m_pBuffer->process(pOut, iBufferSize); + if (m_pBuffer->getChannelCount() == mixxx::kEngineChannelCount) { + // Process a single stereo channel + m_pBuffer->process(pOut, iBufferSize); + } else { + // Process multiple stereo channels (stems) and mix them together + processStem(pOut, iBufferSize); + } m_pPregain->setSpeedAndScratching(m_pBuffer->getSpeed(), m_pBuffer->getScratching()); m_bPassthroughWasActive = false; } diff --git a/src/engine/channels/enginedeck.h b/src/engine/channels/enginedeck.h index ca87feda2ad..3c7cb996b70 100644 --- a/src/engine/channels/enginedeck.h +++ b/src/engine/channels/enginedeck.h @@ -2,9 +2,10 @@ #include -#include "preferences/usersettings.h" #include "engine/channels/enginechannel.h" +#include "preferences/usersettings.h" #include "soundio/soundmanagerutil.h" +#include "util/samplebuffer.h" class EnginePregain; class EngineBuffer; @@ -23,6 +24,7 @@ class EngineDeck : public EngineChannel, public AudioDestination { bool primaryDeck); ~EngineDeck() override; + void processStem(CSAMPLE* pOutput, const int iBufferSize); void process(CSAMPLE* pOutput, const int iBufferSize) override; void collectFeatures(GroupFeatureState* pGroupFeatures) const override; @@ -74,6 +76,8 @@ class EngineDeck : public EngineChannel, public AudioDestination { EngineBuffer* m_pBuffer; EnginePregain* m_pPregain; + mixxx::SampleBuffer m_stemBuffer; + // Begin vinyl passthrough fields QScopedPointer m_pInputConfigured; ControlPushButton* m_pPassing; diff --git a/src/engine/controls/loopingcontrol.cpp b/src/engine/controls/loopingcontrol.cpp index d14a498b60d..892d9cd274a 100644 --- a/src/engine/controls/loopingcontrol.cpp +++ b/src/engine/controls/loopingcontrol.cpp @@ -536,9 +536,9 @@ mixxx::audio::FramePos LoopingControl::nextTrigger(bool reverse, return mixxx::audio::kInvalidFramePos; } -double LoopingControl::getTrackSamples() const { +mixxx::audio::FramePos LoopingControl::getTrackFrame() const { const FrameInfo info = frameInfo(); - return info.trackEndPosition.toEngineSamplePos(); + return info.trackEndPosition; } void LoopingControl::hintReader(gsl::not_null pHintList) { diff --git a/src/engine/controls/loopingcontrol.h b/src/engine/controls/loopingcontrol.h index 96167a41927..21294f359db 100644 --- a/src/engine/controls/loopingcontrol.h +++ b/src/engine/controls/loopingcontrol.h @@ -94,7 +94,7 @@ class LoopingControl : public EngineControl { void trackLoaded(TrackPointer pNewTrack) override; void trackBeatsUpdated(mixxx::BeatsPointer pBeats) override; - double getTrackSamples() const; + mixxx::audio::FramePos getTrackFrame() const; signals: void loopReset(); diff --git a/src/engine/effects/engineeffect.cpp b/src/engine/effects/engineeffect.cpp index 6aff599f7e4..0df4167e320 100644 --- a/src/engine/effects/engineeffect.cpp +++ b/src/engine/effects/engineeffect.cpp @@ -199,14 +199,14 @@ bool EngineEffect::process(const ChannelHandle& inputHandle, if (effectiveEffectEnableState == EffectEnableState::Disabling) { DEBUG_ASSERT(pInput != pOutput); // Fade to dry only works if pInput is not touched by pOutput // Fade out (fade to dry signal) - SampleUtil::linearCrossfadeBuffersOut( + SampleUtil::linearCrossfadeStereoBuffersOut( pOutput, pInput, numSamples); } else if (effectiveEffectEnableState == EffectEnableState::Enabling) { DEBUG_ASSERT(pInput != pOutput); // Fade to dry only works if pInput is not touched by pOutput // Fade in (fade to wet signal) - SampleUtil::linearCrossfadeBuffersIn( + SampleUtil::linearCrossfadeStereoBuffersIn( pOutput, pInput, numSamples); diff --git a/src/engine/enginebuffer.cpp b/src/engine/enginebuffer.cpp index 0a29a58af40..bb67ba9936f 100644 --- a/src/engine/enginebuffer.cpp +++ b/src/engine/enginebuffer.cpp @@ -47,8 +47,6 @@ const mixxx::Logger kLogger("EngineBuffer"); constexpr double kLinearScalerElipsis = 1.00058; // 2^(0.01/12): changes < 1 cent allows a linear scaler -constexpr SINT kSamplesPerFrame = 2; // Engine buffer uses Stereo frames only - // Rate at which the playpos slider is updated constexpr int kPlaypositionUpdateRate = 15; // updates per second @@ -91,6 +89,7 @@ EngineBuffer::EngineBuffer(const QString& group, m_iEnableSyncQueued(SYNC_REQUEST_NONE), m_iSyncModeQueued(static_cast(SyncMode::Invalid)), m_bPlayAfterLoading(false), + m_channelCount(mixxx::kEngineChannelCount), m_pCrossfadeBuffer(SampleUtil::alloc(kMaxEngineSamples)), m_bCrossfadeReady(false), m_iLastBufferSize(0) { @@ -170,6 +169,7 @@ EngineBuffer::EngineBuffer(const QString& group, m_pTrackSamples = new ControlObject(ConfigKey(m_group, "track_samples")); m_pTrackSampleRate = new ControlObject(ConfigKey(m_group, "track_samplerate")); + m_pTrackChannelCount = new ControlObject(ConfigKey(m_group, "track_channelcount")); m_pKeylock = new ControlPushButton(ConfigKey(m_group, "keylock"), true); m_pKeylock->setButtonMode(ControlPushButton::TOGGLE); @@ -309,6 +309,7 @@ EngineBuffer::~EngineBuffer() { delete m_pTrackLoaded; delete m_pTrackSamples; delete m_pTrackSampleRate; + delete m_pTrackChannelCount; delete m_pScaleLinear; delete m_pScaleST; @@ -451,7 +452,7 @@ void EngineBuffer::readToCrossfadeBuffer(const int iBufferSize) { // (Must be called only once per callback) m_pScale->scaleBuffer(m_pCrossfadeBuffer, iBufferSize); // Restore the original position that was lost due to scaleBuffer() above - m_pReadAheadManager->notifySeek(m_playPos); + m_pReadAheadManager->notifySeek(m_playPos.toEngineSamplePos(m_channelCount)); m_bCrossfadeReady = true; } } @@ -470,7 +471,7 @@ void EngineBuffer::setNewPlaypos(mixxx::audio::FramePos position) { // this also sets m_pReadAheadManager to newpos readToCrossfadeBuffer(m_iLastBufferSize); } else { - m_pReadAheadManager->notifySeek(m_playPos); + m_pReadAheadManager->notifySeek(m_playPos.toEngineSamplePos(m_channelCount)); } m_pScale->clear(); @@ -522,12 +523,14 @@ void EngineBuffer::loadFakeTrack(TrackPointer pTrack, bool bPlay) { slotTrackLoaded( pTrack, pTrack->getSampleRate(), + mixxx::audio::ChannelCount(pTrack->getChannels()), pTrack->getSampleRate() * pTrack->getDuration()); } // WARNING: Always called from the EngineWorker thread pool void EngineBuffer::slotTrackLoaded(TrackPointer pTrack, mixxx::audio::SampleRate trackSampleRate, + mixxx::audio::ChannelCount trackChannelCount, double trackNumSamples) { if (kLogger.traceEnabled()) { kLogger.trace() << getGroup() << "EngineBuffer::slotTrackLoaded"; @@ -538,8 +541,14 @@ void EngineBuffer::slotTrackLoaded(TrackPointer pTrack, m_visualPlayPos->setInvalid(); m_playPos = kInitialPlayPosition; // for execute seeks to 0.0 m_pCurrentTrack = pTrack; - m_pTrackSamples->set(trackNumSamples); + + // The sample count is indicated downmix, so we only consider the track in stereo + double stereoPairCount = trackChannelCount.toDouble() / mixxx::audio::ChannelCount::stereo(); + m_pTrackSamples->set(trackNumSamples / stereoPairCount); + m_pTrackSampleRate->set(trackSampleRate.toDouble()); + m_pTrackChannelCount->set(trackChannelCount.toDouble()); + m_channelCount = trackChannelCount; m_pTrackLoaded->forceSet(1); // Reset slip mode @@ -836,8 +845,9 @@ void EngineBuffer::slotKeylockEngineChanged(double dIndex) { } } -void EngineBuffer::processTrackLocked( - CSAMPLE* pOutput, const int iBufferSize, mixxx::audio::SampleRate sampleRate) { +void EngineBuffer::processTrackLocked(CSAMPLE* pOutput, + const int iBufferSize, + mixxx::audio::SampleRate sampleRate) { ScopedTimer t(u"EngineBuffer::process_pauselock"); m_trackSampleRateOld = mixxx::audio::SampleRate::fromDouble(m_pTrackSampleRate->get()); @@ -874,11 +884,13 @@ void EngineBuffer::processTrackLocked( // (1.0 being normal rate. 2.0 plays at 2x speed -- 2 track seconds // pass for every 1 real second). Depending on whether // keylock is enabled, this is applied to either the rate or the tempo. + int stereoPairCount = m_channelCount / mixxx::audio::ChannelCount::stereo(); double speed = m_pRateControl->calculateSpeed( baserate, tempoRatio, paused, - iBufferSize, + // The speed is calculate out of the buffer size for the stereo channel output channel + iBufferSize / stereoPairCount, &is_scratching, &is_reverse); @@ -1074,8 +1086,8 @@ void EngineBuffer::processTrackLocked( m_playPos += framesRead; } else { // Adjust filepos_play by the amount we processed. - m_playPos = - m_pReadAheadManager->getFilePlaypositionFromLog(m_playPos, framesRead); + m_playPos = m_pReadAheadManager->getFilePlaypositionFromLog( + m_playPos, framesRead, m_channelCount); } // Note: The last buffer of a track is padded with silence. // This silence is played together with the last samples in the last @@ -1086,8 +1098,21 @@ void EngineBuffer::processTrackLocked( if (m_bCrossfadeReady) { // Bring pOutput with the new parameters in and fade out the old one, // stored with the old parameters in m_pCrossfadeBuffer - SampleUtil::linearCrossfadeBuffersIn( - pOutput, m_pCrossfadeBuffer, iBufferSize); + switch (m_channelCount) { + case mixxx::audio::ChannelCount::stereo(): + SampleUtil::linearCrossfadeStereoBuffersOut( + pOutput, m_pCrossfadeBuffer, iBufferSize); + break; + case mixxx::audio::ChannelCount::stem(): + SampleUtil::linearCrossfadeStemBuffersOut( + pOutput, m_pCrossfadeBuffer, iBufferSize); + break; + default: + // Fallback to unoptimised function + SampleUtil::linearCrossfadeUnaryBuffersOut( + pOutput, m_pCrossfadeBuffer, iBufferSize, m_channelCount); + break; + } } // Note: we do not fade here if we pass the end or the start of // the track in reverse direction @@ -1142,7 +1167,7 @@ void EngineBuffer::processTrackLocked( void EngineBuffer::process(CSAMPLE* pOutput, const int iBufferSize) { // Bail if we receive a buffer size with incomplete sample frames. Assert in debug builds. - VERIFY_OR_DEBUG_ASSERT((iBufferSize % kSamplesPerFrame) == 0) { + VERIFY_OR_DEBUG_ASSERT((iBufferSize % m_channelCount) == 0) { return; } m_pReader->process(); @@ -1161,10 +1186,10 @@ void EngineBuffer::process(CSAMPLE* pOutput, const int iBufferSize) { // If the sample rate has changed, force Rubberband to reset so that // it doesn't reallocate when the user engages keylock during playback. // We do this even if rubberband is not active. - m_pScaleLinear->setSampleRate(m_sampleRate); - m_pScaleST->setSampleRate(m_sampleRate); + m_pScaleLinear->setOutputSignal(m_sampleRate, m_channelCount); + m_pScaleST->setOutputSignal(m_sampleRate, m_channelCount); #ifdef __RUBBERBAND__ - m_pScaleRB->setSampleRate(m_sampleRate); + m_pScaleRB->setOutputSignal(m_sampleRate, m_channelCount); #endif bool hasStableTrack = m_pTrackLoaded->toBool() && m_iTrackLoading.loadAcquire() == 0; @@ -1232,8 +1257,8 @@ void EngineBuffer::processSlip(int iBufferSize) { // TODO: Check if we can replace `iBufferSize` with the number of // frames per buffer in most engine method signatures to avoid this // back and forth calculations. - const int bufferFrameCount = iBufferSize / mixxx::kEngineChannelCount; - DEBUG_ASSERT(bufferFrameCount * mixxx::kEngineChannelCount == iBufferSize); + const int bufferFrameCount = iBufferSize / m_channelCount; + DEBUG_ASSERT(bufferFrameCount * m_channelCount == iBufferSize); const mixxx::audio::FrameDiff_t slipDelta = static_cast(bufferFrameCount) * m_dSlipRate; // Simulate looping if a regular loop is active @@ -1452,7 +1477,7 @@ void EngineBuffer::updateIndicators(double speed, int iBufferSize) { // Update indicators that are only updated after every // sampleRate/kiUpdateRate samples processed. (e.g. playposSlider) if (m_iSamplesSinceLastIndicatorUpdate > - (kSamplesPerFrame * m_pSampleRate->get() / + (mixxx::kEngineChannelCount * m_pSampleRate->get() / kPlaypositionUpdateRate)) { m_playposSlider->set(fFractionalPlaypos); m_pCueControl->updateIndicators(); @@ -1464,7 +1489,7 @@ void EngineBuffer::updateIndicators(double speed, int iBufferSize) { fFractionalPlaypos, speed * m_baserate_old, static_cast(iBufferSize) / - m_trackEndPositionOld.toEngineSamplePos(), + m_trackEndPositionOld.toEngineSamplePos(mixxx::kEngineChannelCount), fFractionalSlipPos, effectiveSlipRate, m_slipModeState, @@ -1474,7 +1499,7 @@ void EngineBuffer::updateIndicators(double speed, int iBufferSize) { fFractionalLoopStartPos, fFractionalLoopEndPos, tempoTrackSeconds, - iBufferSize / kSamplesPerFrame / m_sampleRate.toDouble() * 1000000.0); + iBufferSize / mixxx::kEngineChannelCount / m_sampleRate.toDouble() * 1000000.0); // TODO: Especially with long audio buffers, jitter is visible. This can be fixed by moving the // ClockControl::updateIndicators into the waveform update loop which is synced with the display refresh rate. @@ -1485,7 +1510,7 @@ void EngineBuffer::updateIndicators(double speed, int iBufferSize) { void EngineBuffer::hintReader(const double dRate) { m_hintList.clear(); - m_pReadAheadManager->hintReader(dRate, &m_hintList); + m_pReadAheadManager->hintReader(dRate, &m_hintList, m_channelCount); //if slipping, hint about virtual position so we're ready for it if (m_bSlipEnabledProcessing) { @@ -1550,11 +1575,13 @@ double EngineBuffer::getVisualPlayPos() const { } mixxx::audio::FramePos EngineBuffer::getTrackEndPosition() const { - return mixxx::audio::FramePos::fromEngineSamplePosMaybeInvalid(m_pTrackSamples->get()); + return mixxx::audio::FramePos::fromEngineSamplePosMaybeInvalid( + m_pTrackSamples->get(), mixxx::audio::ChannelCount::stereo()); } void EngineBuffer::setTrackEndPosition(mixxx::audio::FramePos position) { - m_pTrackSamples->set(position.toEngineSamplePosMaybeInvalid()); + m_pTrackSamples->set(position.toEngineSamplePosMaybeInvalid( + mixxx::audio::ChannelCount::stereo())); } double EngineBuffer::getUserOffset() const { diff --git a/src/engine/enginebuffer.h b/src/engine/enginebuffer.h index 113286187d6..00f7e72067d 100644 --- a/src/engine/enginebuffer.h +++ b/src/engine/enginebuffer.h @@ -110,6 +110,9 @@ class EngineBuffer : public EngineObject { QString getGroup() const; // Return the current rate (not thread-safe) double getSpeed() const; + mixxx::audio::ChannelCount getChannelCount() const { + return m_channelCount; + } bool getScratching() const; bool isReverse() const; /// Returns current bpm value (not thread-safe) @@ -239,6 +242,7 @@ class EngineBuffer : public EngineObject { void slotTrackLoaded( TrackPointer pTrack, mixxx::audio::SampleRate trackSampleRate, + mixxx::audio::ChannelCount trackChannelCount, double trackNumSamples); void slotTrackLoadFailed(TrackPointer pTrack, const QString& reason); @@ -332,7 +336,7 @@ class EngineBuffer : public EngineObject { // List of hints to provide to the CachingReader HintVector m_hintList; - // The current sample to play in the file. + // The current frame to play in the file. mixxx::audio::FramePos m_playPos; // The previous callback's speed. Used to check if the scaler parameters @@ -382,6 +386,7 @@ class EngineBuffer : public EngineObject { ControlObject* m_pTrackSamples; ControlObject* m_pTrackSampleRate; + ControlObject* m_pTrackChannelCount; ControlPushButton* m_playButton; ControlPushButton* m_playStartButton; @@ -457,6 +462,10 @@ class EngineBuffer : public EngineObject { // 0 to guarantee we see a change on the first callback. mixxx::audio::SampleRate m_sampleRate; + // Records the channel count so it can be reused without having to cast it + // from the double value in CO + mixxx::audio::ChannelCount m_channelCount; + TrackPointer m_pCurrentTrack; #ifdef __SCALER_DEBUG__ QFile df; diff --git a/src/engine/filters/enginefilteriir.h b/src/engine/filters/enginefilteriir.h index f14ff5a0d86..148c2cd3d56 100644 --- a/src/engine/filters/enginefilteriir.h +++ b/src/engine/filters/enginefilteriir.h @@ -69,7 +69,7 @@ class EngineFilterIIR : public EngineFilterIIRBase { int iBufferSize) { process(pIn, pOutput, iBufferSize); if (m_startFromDry) { - SampleUtil::linearCrossfadeBuffersOut( + SampleUtil::linearCrossfadeStereoBuffersOut( pOutput, // fade out filtered pIn, // fade in dry iBufferSize); diff --git a/src/engine/filters/enginefiltermoogladder4.h b/src/engine/filters/enginefiltermoogladder4.h index f9b5b0fbdec..b45af803cc3 100644 --- a/src/engine/filters/enginefiltermoogladder4.h +++ b/src/engine/filters/enginefiltermoogladder4.h @@ -118,7 +118,7 @@ class EngineFilterMoogLadderBase : public EngineObjectConstIn { CSAMPLE* M_RESTRICT pOutput, const int iBufferSize) { process(pIn, pOutput, iBufferSize); - SampleUtil::linearCrossfadeBuffersOut( + SampleUtil::linearCrossfadeStereoBuffersOut( pOutput, // fade out filtered pIn, // fade in dry iBufferSize); diff --git a/src/engine/readaheadmanager.cpp b/src/engine/readaheadmanager.cpp index 6af6981f566..629521a82e3 100644 --- a/src/engine/readaheadmanager.cpp +++ b/src/engine/readaheadmanager.cpp @@ -1,5 +1,7 @@ #include "engine/readaheadmanager.h" +#include + #include "engine/cachingreader/cachingreader.h" #include "engine/controls/loopingcontrol.h" #include "engine/controls/ratecontrol.h" @@ -32,11 +34,13 @@ ReadAheadManager::~ReadAheadManager() { SampleUtil::free(m_pCrossFadeBuffer); } -SINT ReadAheadManager::getNextSamples(double dRate, CSAMPLE* pOutput, - SINT requested_samples) { +SINT ReadAheadManager::getNextSamples(double dRate, + CSAMPLE* pOutput, + SINT requested_samples, + mixxx::audio::ChannelCount channelCount) { // qDebug() << "getNextSamples:" << m_currentPosition << requested_samples; - int modSamples = requested_samples % mixxx::kEngineChannelCount; + int modSamples = requested_samples % channelCount; if (modSamples != 0) { qDebug() << "ERROR: Non-even requested_samples to ReadAheadManager::getNextSamples"; requested_samples -= modSamples; @@ -49,10 +53,10 @@ SINT ReadAheadManager::getNextSamples(double dRate, CSAMPLE* pOutput, const mixxx::audio::FramePos loopTriggerPosition = m_pLoopingControl->nextTrigger(in_reverse, mixxx::audio::FramePos::fromEngineSamplePosMaybeInvalid( - m_currentPosition), + m_currentPosition, channelCount), &targetPosition); - const double loop_trigger = loopTriggerPosition.toEngineSamplePosMaybeInvalid(); - const double target = targetPosition.toEngineSamplePosMaybeInvalid(); + const double loop_trigger = loopTriggerPosition.toEngineSamplePosMaybeInvalid(channelCount); + const double target = targetPosition.toEngineSamplePosMaybeInvalid(channelCount); SINT preloop_samples = 0; double samplesToLoopTrigger = 0.0; @@ -68,7 +72,7 @@ SINT ReadAheadManager::getNextSamples(double dRate, CSAMPLE* pOutput, // We can only read whole frames from the reader. // Use ceil here, to be sure to reach the loop trigger. preloop_samples = SampleUtil::ceilPlayPosToFrameStart( - samplesToLoopTrigger, mixxx::kEngineChannelCount); + samplesToLoopTrigger, channelCount); // clamp requested samples from the caller to the loop trigger point if (preloop_samples <= requested_samples) { reachedTrigger = true; @@ -84,10 +88,10 @@ SINT ReadAheadManager::getNextSamples(double dRate, CSAMPLE* pOutput, } SINT start_sample = SampleUtil::roundPlayPosToFrameStart( - m_currentPosition, mixxx::kEngineChannelCount); + m_currentPosition, channelCount); const auto readResult = m_pReader->read( - start_sample, samples_from_reader, in_reverse, pOutput); + start_sample, samples_from_reader, in_reverse, pOutput, channelCount); if (readResult == CachingReader::ReadResult::UNAVAILABLE) { // Cache miss - no samples written SampleUtil::clear(pOutput, samples_from_reader); @@ -143,7 +147,7 @@ SINT ReadAheadManager::getNextSamples(double dRate, CSAMPLE* pOutput, int loop_read_position = SampleUtil::roundPlayPosToFrameStart( m_currentPosition + (in_reverse ? preloop_samples : -preloop_samples), - mixxx::kEngineChannelCount); + channelCount); int crossFadeStart = 0; int crossFadeSamples = samples_from_reader; @@ -152,7 +156,9 @@ SINT ReadAheadManager::getNextSamples(double dRate, CSAMPLE* pOutput, crossFadeStart = -loop_read_position; crossFadeSamples -= crossFadeStart; } else { - int trackSamples = static_cast(m_pLoopingControl->getTrackSamples()); + int trackSamples = static_cast( + m_pLoopingControl->getTrackFrame().toEngineSamplePos( + channelCount)); if (loop_read_position > trackSamples) { // looping in reverse overlapping post-roll without samples crossFadeStart = loop_read_position - trackSamples; @@ -165,7 +171,8 @@ SINT ReadAheadManager::getNextSamples(double dRate, CSAMPLE* pOutput, (in_reverse ? crossFadeStart : -crossFadeStart), crossFadeSamples, in_reverse, - m_pCrossFadeBuffer); + m_pCrossFadeBuffer, + channelCount); if (readResult == CachingReader::ReadResult::UNAVAILABLE) { qDebug() << "ERROR: Couldn't get all needed samples for crossfade."; // Cache miss - no samples written @@ -177,10 +184,28 @@ SINT ReadAheadManager::getNextSamples(double dRate, CSAMPLE* pOutput, // do crossfade from the current buffer into the new loop beginning if (samples_from_reader != 0) { // avoid division by zero - SampleUtil::linearCrossfadeBuffersOut( - pOutput + crossFadeStart, - m_pCrossFadeBuffer, - crossFadeSamples); + switch (channelCount) { + case mixxx::audio::ChannelCount::stereo(): + SampleUtil::linearCrossfadeStereoBuffersOut( + pOutput + crossFadeStart, + m_pCrossFadeBuffer, + crossFadeSamples); + break; + case mixxx::audio::ChannelCount::stem(): + SampleUtil::linearCrossfadeStemBuffersOut( + pOutput + crossFadeStart, + m_pCrossFadeBuffer, + crossFadeSamples); + break; + default: + // Fallback to unoptimised function + SampleUtil::linearCrossfadeUnaryBuffersOut( + pOutput + crossFadeStart, + m_pCrossFadeBuffer, + crossFadeSamples, + channelCount); + break; + } } } } @@ -208,7 +233,9 @@ void ReadAheadManager::notifySeek(double seekPosition) { // } } -void ReadAheadManager::hintReader(double dRate, gsl::not_null pHintList) { +void ReadAheadManager::hintReader(double dRate, + gsl::not_null pHintList, + mixxx::audio::ChannelCount channelCount) { bool in_reverse = dRate < 0; Hint current_position; @@ -220,11 +247,11 @@ void ReadAheadManager::hintReader(double dRate, gsl::not_null pHint // this called after the precious chunk was consumed if (in_reverse) { current_position.frame = - static_cast(ceil(m_currentPosition / mixxx::kEngineChannelCount)) - + static_cast(ceil(m_currentPosition / channelCount)) - frameCountToCache; } else { current_position.frame = - static_cast(floor(m_currentPosition / mixxx::kEngineChannelCount)); + static_cast(floor(m_currentPosition / channelCount)); } // If we are trying to cache before the start of the track, @@ -256,8 +283,9 @@ void ReadAheadManager::addReadLogEntry(double virtualPlaypositionStart, // Not thread-save, call from engine thread only double ReadAheadManager::getFilePlaypositionFromLog( - double currentFilePlayposition, double numConsumedSamples) { - + double currentFilePlayposition, + double numConsumedSamples, + mixxx::audio::ChannelCount channelCount) { if (numConsumedSamples == 0) { return currentFilePlayposition; } @@ -281,7 +309,7 @@ double ReadAheadManager::getFilePlaypositionFromLog( if (m_pRateControl) { const auto seekPosition = mixxx::audio::FramePos::fromEngineSamplePos( - entry.virtualPlaypositionStart); + entry.virtualPlaypositionStart, channelCount); m_pRateControl->notifySeek(seekPosition); } } @@ -302,9 +330,11 @@ double ReadAheadManager::getFilePlaypositionFromLog( mixxx::audio::FramePos ReadAheadManager::getFilePlaypositionFromLog( mixxx::audio::FramePos currentPosition, - mixxx::audio::FrameDiff_t numConsumedFrames) { + mixxx::audio::FrameDiff_t numConsumedFrames, + mixxx::audio::ChannelCount channelCount) { const double positionSamples = - getFilePlaypositionFromLog(currentPosition.toEngineSamplePos(), - numConsumedFrames * mixxx::kEngineChannelCount); - return mixxx::audio::FramePos::fromEngineSamplePos(positionSamples); + getFilePlaypositionFromLog(currentPosition.toEngineSamplePos(channelCount), + numConsumedFrames * channelCount, + channelCount); + return mixxx::audio::FramePos::fromEngineSamplePos(positionSamples, channelCount); } diff --git a/src/engine/readaheadmanager.h b/src/engine/readaheadmanager.h index ae71a5f6b1c..d320c7e814b 100644 --- a/src/engine/readaheadmanager.h +++ b/src/engine/readaheadmanager.h @@ -32,7 +32,10 @@ class ReadAheadManager { /// direction the audio is progressing in. Returns the total number of /// samples read into buffer. Note that it is very common that the total /// samples read is less than the requested number of samples. - virtual SINT getNextSamples(double dRate, CSAMPLE* buffer, SINT requested_samples); + virtual SINT getNextSamples(double dRate, + CSAMPLE* buffer, + SINT requested_samples, + mixxx::audio::ChannelCount channelCount); /// Used to add a new EngineControls that ReadAheadManager will use to decide /// which samples to return. @@ -46,20 +49,23 @@ class ReadAheadManager { } virtual void notifySeek(double seekPosition); - virtual void notifySeek(mixxx::audio::FramePos position) { - notifySeek(position.toEngineSamplePos()); - } /// hintReader allows the ReadAheadManager to provide hints to the reader to /// indicate that the given portion of a song is about to be read. - virtual void hintReader(double dRate, gsl::not_null pHintList); + virtual void hintReader(double dRate, + gsl::not_null pHintList, + mixxx::audio::ChannelCount channelCount); + /// Return the position in sample virtual double getFilePlaypositionFromLog( double currentFilePlayposition, - double numConsumedSamples); + double numConsumedSamples, + mixxx::audio::ChannelCount channelCount); + /// Return the position in frame mixxx::audio::FramePos getFilePlaypositionFromLog( mixxx::audio::FramePos currentPosition, - mixxx::audio::FrameDiff_t numConsumedFrames); + mixxx::audio::FrameDiff_t numConsumedFrames, + mixxx::audio::ChannelCount channelCount); private: /// An entry in the read log indicates the virtual playposition the read diff --git a/src/test/enginebufferscalelineartest.cpp b/src/test/enginebufferscalelineartest.cpp index 1cb4399392a..8a35fcb3527 100644 --- a/src/test/enginebufferscalelineartest.cpp +++ b/src/test/enginebufferscalelineartest.cpp @@ -74,7 +74,8 @@ class EngineBufferScaleLinearTest : public MixxxTest { void SetRate(double rate) { double tempoRatio = rate; double pitchRatio = rate; - m_pScaler->setSampleRate(mixxx::audio::SampleRate(44100)); + m_pScaler->setOutputSignal(mixxx::audio::SampleRate(44100), + mixxx::audio::ChannelCount::stereo()); m_pScaler->setScaleParameters( 1.0, &tempoRatio, &pitchRatio); } diff --git a/src/test/mockedenginebackendtest.h b/src/test/mockedenginebackendtest.h index e1a0f1a045e..14287d62e68 100644 --- a/src/test/mockedenginebackendtest.h +++ b/src/test/mockedenginebackendtest.h @@ -55,7 +55,8 @@ class MockScaler : public EngineBufferScale { } private: - void onSampleRateChanged() override {} + void onOutputSignalChanged() override { + } double m_processedTempo; double m_processedPitch; diff --git a/src/test/readaheadmanager_test.cpp b/src/test/readaheadmanager_test.cpp index 67674f318cb..65417f692a6 100644 --- a/src/test/readaheadmanager_test.cpp +++ b/src/test/readaheadmanager_test.cpp @@ -22,10 +22,14 @@ class StubReader : public CachingReader { : CachingReader(kGroup, UserSettingsPointer()) { } - CachingReader::ReadResult read(SINT startSample, SINT numSamples, bool reverse, - CSAMPLE* buffer) override { + CachingReader::ReadResult read(SINT startSample, + SINT numSamples, + bool reverse, + CSAMPLE* buffer, + mixxx::audio::ChannelCount channelCount) override { Q_UNUSED(startSample); Q_UNUSED(reverse); + Q_UNUSED(channelCount); SampleUtil::clear(buffer, numSamples); return CachingReader::ReadResult::AVAILABLE; } @@ -120,17 +124,47 @@ TEST_F(ReadAheadManagerTest, FractionalFrameLoop) { m_pLoopControl->pushTargetReturnValue(3.3); m_pLoopControl->pushTargetReturnValue(kNoTrigger); // read from start to loop trigger, overshoot 0.3 - EXPECT_EQ(20, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 100)); + EXPECT_EQ(20, + m_pReadAheadManager->getNextSamples( + 1.0, m_pBuffer, 100, mixxx::audio::ChannelCount::stereo())); // read loop - EXPECT_EQ(18, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 80)); + EXPECT_EQ(18, + m_pReadAheadManager->getNextSamples( + 1.0, m_pBuffer, 80, mixxx::audio::ChannelCount::stereo())); // read loop - EXPECT_EQ(16, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 62)); + EXPECT_EQ(16, + m_pReadAheadManager->getNextSamples( + 1.0, m_pBuffer, 62, mixxx::audio::ChannelCount::stereo())); // read loop - EXPECT_EQ(18, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 46)); + EXPECT_EQ(18, + m_pReadAheadManager->getNextSamples( + 1.0, m_pBuffer, 46, mixxx::audio::ChannelCount::stereo())); // read loop - EXPECT_EQ(16, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 28)); + EXPECT_EQ(16, + m_pReadAheadManager->getNextSamples( + 1.0, m_pBuffer, 28, mixxx::audio::ChannelCount::stereo())); // read loop - EXPECT_EQ(12, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 12)); + EXPECT_EQ(12, + m_pReadAheadManager->getNextSamples( + 1.0, m_pBuffer, 12, mixxx::audio::ChannelCount::stereo())); + + // EXPECT_EQ(20, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 100, + // mixxx::audio::ChannelCount::stereo())); + // // read loop + // EXPECT_EQ(18, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 80, + // mixxx::audio::ChannelCount::stereo())); + // // read loop + // EXPECT_EQ(16, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 62, + // mixxx::audio::ChannelCount::stereo())); + // // read loop + // EXPECT_EQ(18, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 46, + // mixxx::audio::ChannelCount::stereo())); + // // read loop + // EXPECT_EQ(16, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 28, + // mixxx::audio::ChannelCount::stereo())); + // // read loop + // EXPECT_EQ(12, m_pReadAheadManager->getNextSamples(1.0, m_pBuffer, 12, + // mixxx::audio::ChannelCount::stereo())); // start 0.5 to 20.2 = 19.7 // loop 3.3 to 20.2 = 16.9 diff --git a/src/test/stems/01-drum.wav b/src/test/stems/01-drum.wav new file mode 100644 index 00000000000..e5b8a3b750d Binary files /dev/null and b/src/test/stems/01-drum.wav differ diff --git a/src/test/stems/02-bass.wav b/src/test/stems/02-bass.wav new file mode 100644 index 00000000000..35b71c87155 Binary files /dev/null and b/src/test/stems/02-bass.wav differ diff --git a/src/test/stems/03-melody.wav b/src/test/stems/03-melody.wav new file mode 100644 index 00000000000..11f09449727 Binary files /dev/null and b/src/test/stems/03-melody.wav differ diff --git a/src/test/stems/04-vocal.wav b/src/test/stems/04-vocal.wav new file mode 100644 index 00000000000..7aaf453db06 Binary files /dev/null and b/src/test/stems/04-vocal.wav differ diff --git a/src/test/stems/StemMetadata.json b/src/test/stems/StemMetadata.json new file mode 100644 index 00000000000..40d04dba77a --- /dev/null +++ b/src/test/stems/StemMetadata.json @@ -0,0 +1 @@ +{"mastering_dsp":{"compressor":{"attack":0.003000000026077032,"dry_wet":50,"enabled":false,"hp_cutoff":300,"input_gain":0.5,"output_gain":0.5,"ratio":3,"release":0.300000011920929,"threshold":0},"limiter":{"ceiling":-0.3499999940395355,"enabled":false,"release":0.05000000074505806,"threshold":0}},"stems":[{"color":"#fd4a4a","name":"Drums"},{"color":"#ffff00","name":"Bass"},{"color":"#00e8e8","name":"Synths"},{"color":"#ad65ff","name":"Vox"}],"version":1} diff --git a/src/test/stems/mainmix.wav b/src/test/stems/mainmix.wav new file mode 100644 index 00000000000..f2cbf67f7bd Binary files /dev/null and b/src/test/stems/mainmix.wav differ diff --git a/src/test/stems/test.stem.mp4 b/src/test/stems/test.stem.mp4 new file mode 100644 index 00000000000..dee4f3fa62f Binary files /dev/null and b/src/test/stems/test.stem.mp4 differ diff --git a/src/util/defs.h b/src/util/defs.h index 06e6dee817c..f7ea9bbb575 100644 --- a/src/util/defs.h +++ b/src/util/defs.h @@ -18,7 +18,7 @@ // see class SoundManagerConfig::AudioBufferSizeIndex // 96 kHz * 80 ms = 7680 -> 8192 (2^13) constexpr unsigned int kMaxEngineFrames = 8192; -constexpr unsigned int kMaxEngineChannels = 2; +constexpr unsigned int kMaxEngineChannels = 8; constexpr unsigned int kMaxEngineSamples = kMaxEngineChannels * kMaxEngineFrames; constexpr unsigned int MAX_BUFFER_LEN = 160000; diff --git a/src/util/sample.cpp b/src/util/sample.cpp index 56bf630c728..abfce030ac4 100644 --- a/src/util/sample.cpp +++ b/src/util/sample.cpp @@ -492,6 +492,30 @@ void SampleUtil::interleaveBuffer(CSAMPLE* M_RESTRICT pDest, } } +// static +void SampleUtil::interleaveBuffer(CSAMPLE* M_RESTRICT pDest, + const CSAMPLE* M_RESTRICT pSrc1, + const CSAMPLE* M_RESTRICT pSrc2, + const CSAMPLE* M_RESTRICT pSrc3, + const CSAMPLE* M_RESTRICT pSrc4, + const CSAMPLE* M_RESTRICT pSrc5, + const CSAMPLE* M_RESTRICT pSrc6, + const CSAMPLE* M_RESTRICT pSrc7, + const CSAMPLE* M_RESTRICT pSrc8, + SINT numFrames) { + // note: LOOP VECTORIZED. + for (SINT i = 0; i < numFrames; ++i) { + pDest[8 * i] = pSrc1[i]; + pDest[8 * i + 1] = pSrc2[i]; + pDest[8 * i + 2] = pSrc3[i]; + pDest[8 * i + 3] = pSrc4[i]; + pDest[8 * i + 4] = pSrc5[i]; + pDest[8 * i + 5] = pSrc6[i]; + pDest[8 * i + 6] = pSrc7[i]; + pDest[8 * i + 7] = pSrc8[i]; + } +} + // static void SampleUtil::deinterleaveBuffer(CSAMPLE* M_RESTRICT pDest1, CSAMPLE* M_RESTRICT pDest2, @@ -505,7 +529,31 @@ void SampleUtil::deinterleaveBuffer(CSAMPLE* M_RESTRICT pDest1, } // static -void SampleUtil::linearCrossfadeBuffersOut( +void SampleUtil::deinterleaveBuffer(CSAMPLE* M_RESTRICT pDest1, + CSAMPLE* M_RESTRICT pDest2, + CSAMPLE* M_RESTRICT pDest3, + CSAMPLE* M_RESTRICT pDest4, + CSAMPLE* M_RESTRICT pDest5, + CSAMPLE* M_RESTRICT pDest6, + CSAMPLE* M_RESTRICT pDest7, + CSAMPLE* M_RESTRICT pDest8, + const CSAMPLE* M_RESTRICT pSrc, + SINT numFrames) { + // note: LOOP VECTORIZED. + for (SINT i = 0; i < numFrames; ++i) { + pDest1[i] = pSrc[i * 8]; + pDest2[i] = pSrc[i * 8 + 1]; + pDest3[i] = pSrc[i * 8 + 2]; + pDest4[i] = pSrc[i * 8 + 3]; + pDest5[i] = pSrc[i * 8 + 4]; + pDest6[i] = pSrc[i * 8 + 5]; + pDest7[i] = pSrc[i * 8 + 6]; + pDest8[i] = pSrc[i * 8 + 7]; + } +} + +// static +void SampleUtil::linearCrossfadeStereoBuffersOut( CSAMPLE* M_RESTRICT pDestSrcFadeOut, const CSAMPLE* M_RESTRICT pSrcFadeIn, SINT numSamples) { @@ -527,7 +575,82 @@ void SampleUtil::linearCrossfadeBuffersOut( } // static -void SampleUtil::linearCrossfadeBuffersIn( +void SampleUtil::linearCrossfadeStemBuffersOut( + CSAMPLE* M_RESTRICT pDestSrcFadeOut, + const CSAMPLE* M_RESTRICT pSrcFadeIn, + SINT numSamples) { + // M_RESTRICT unoptimizes the function for some reason. + const CSAMPLE_GAIN cross_inc = CSAMPLE_GAIN_ONE / CSAMPLE_GAIN(numSamples / 8); + // note: LOOP VECTORIZED only with "int i" (not SINT i) + for (int i = 0; i < numSamples / 8; ++i) { + const CSAMPLE_GAIN cross_mix = cross_inc * i; + pDestSrcFadeOut[i * 8] *= (CSAMPLE_GAIN_ONE - cross_mix); + pDestSrcFadeOut[i * 8] += pSrcFadeIn[i * 8] * cross_mix; + } + // note: LOOP VECTORIZED only with "int i" (not SINT i) + for (int i = 0; i < numSamples / 8; ++i) { + const CSAMPLE_GAIN cross_mix = cross_inc * i; + pDestSrcFadeOut[i * 8 + 1] *= (CSAMPLE_GAIN_ONE - cross_mix); + pDestSrcFadeOut[i * 8 + 1] += pSrcFadeIn[i * 8 + 1] * cross_mix; + } + // note: LOOP VECTORIZED only with "int i" (not SINT i) + for (int i = 0; i < numSamples / 8; ++i) { + const CSAMPLE_GAIN cross_mix = cross_inc * i; + pDestSrcFadeOut[i * 8 + 2] *= (CSAMPLE_GAIN_ONE - cross_mix); + pDestSrcFadeOut[i * 8 + 2] += pSrcFadeIn[i * 8 + 2] * cross_mix; + } + // note: LOOP VECTORIZED only with "int i" (not SINT i) + for (int i = 0; i < numSamples / 8; ++i) { + const CSAMPLE_GAIN cross_mix = cross_inc * i; + pDestSrcFadeOut[i * 8 + 3] *= (CSAMPLE_GAIN_ONE - cross_mix); + pDestSrcFadeOut[i * 8 + 3] += pSrcFadeIn[i * 8 + 3] * cross_mix; + } + // note: LOOP VECTORIZED only with "int i" (not SINT i) + for (int i = 0; i < numSamples / 8; ++i) { + const CSAMPLE_GAIN cross_mix = cross_inc * i; + pDestSrcFadeOut[i * 8 + 4] *= (CSAMPLE_GAIN_ONE - cross_mix); + pDestSrcFadeOut[i * 8 + 4] += pSrcFadeIn[i * 8 + 4] * cross_mix; + } + // note: LOOP VECTORIZED only with "int i" (not SINT i) + for (int i = 0; i < numSamples / 8; ++i) { + const CSAMPLE_GAIN cross_mix = cross_inc * i; + pDestSrcFadeOut[i * 8 + 5] *= (CSAMPLE_GAIN_ONE - cross_mix); + pDestSrcFadeOut[i * 8 + 5] += pSrcFadeIn[i * 8 + 5] * cross_mix; + } + // note: LOOP VECTORIZED only with "int i" (not SINT i) + for (int i = 0; i < numSamples / 8; ++i) { + const CSAMPLE_GAIN cross_mix = cross_inc * i; + pDestSrcFadeOut[i * 8 + 6] *= (CSAMPLE_GAIN_ONE - cross_mix); + pDestSrcFadeOut[i * 8 + 6] += pSrcFadeIn[i * 8 + 6] * cross_mix; + } + // note: LOOP VECTORIZED only with "int i" (not SINT i) + for (int i = 0; i < numSamples / 8; ++i) { + const CSAMPLE_GAIN cross_mix = cross_inc * i; + pDestSrcFadeOut[i * 8 + 7] *= (CSAMPLE_GAIN_ONE - cross_mix); + pDestSrcFadeOut[i * 8 + 7] += pSrcFadeIn[i * 8 + 7] * cross_mix; + } +} + +// static +void SampleUtil::linearCrossfadeUnaryBuffersOut( + CSAMPLE* pDestSrcFadeOut, + const CSAMPLE* pSrcFadeIn, + SINT numSamples, + int channelCount) { + DEBUG_ASSERT(numSamples % channelCount == 0); + int numFrame = numSamples / channelCount; + const CSAMPLE_GAIN cross_inc = CSAMPLE_GAIN_ONE / CSAMPLE_GAIN(numSamples / channelCount); + for (int c = 0; c < channelCount; c++) { + for (int i = 0; i < numFrame; ++i) { + const CSAMPLE_GAIN cross_mix = cross_inc * i; + pDestSrcFadeOut[i * channelCount + c] *= (CSAMPLE_GAIN_ONE - cross_mix); + pDestSrcFadeOut[i * channelCount + c] += pSrcFadeIn[i * channelCount + c] * cross_mix; + } + } +} + +// static +void SampleUtil::linearCrossfadeStereoBuffersIn( CSAMPLE* M_RESTRICT pDestSrcFadeIn, const CSAMPLE* M_RESTRICT pSrcFadeOut, SINT numSamples) { @@ -547,6 +670,82 @@ void SampleUtil::linearCrossfadeBuffersIn( } } +// static +void SampleUtil::linearCrossfadeStemBuffersIn( + CSAMPLE* M_RESTRICT pDestSrcFadeIn, + const CSAMPLE* M_RESTRICT pSrcFadeOut, + SINT numSamples) { + // M_RESTRICT unoptimizes the function for some reason. + const CSAMPLE_GAIN cross_inc = CSAMPLE_GAIN_ONE / CSAMPLE_GAIN(numSamples / 8); + /// note: LOOP VECTORIZED only with "int i" (not SINT i) + for (int i = 0; i < numSamples / 8; ++i) { + const CSAMPLE_GAIN cross_mix = cross_inc * i; + pDestSrcFadeIn[i * 8] *= cross_mix; + pDestSrcFadeIn[i * 8] += pSrcFadeOut[i * 8] * (CSAMPLE_GAIN_ONE - cross_mix); + } + // note: LOOP VECTORIZED only with "int i" (not SINT i) + for (int i = 0; i < numSamples / 8; ++i) { + const CSAMPLE_GAIN cross_mix = cross_inc * i; + pDestSrcFadeIn[i * 8 + 1] *= cross_mix; + pDestSrcFadeIn[i * 8 + 1] += pSrcFadeOut[i * 8 + 1] * (CSAMPLE_GAIN_ONE - cross_mix); + } + /// note: LOOP VECTORIZED only with "int i" (not SINT i) + for (int i = 0; i < numSamples / 8; ++i) { + const CSAMPLE_GAIN cross_mix = cross_inc * i; + pDestSrcFadeIn[i * 8 + 2] *= cross_mix; + pDestSrcFadeIn[i * 8 + 2] += pSrcFadeOut[i * 8 + 2] * (CSAMPLE_GAIN_ONE - cross_mix); + } + // note: LOOP VECTORIZED only with "int i" (not SINT i) + for (int i = 0; i < numSamples / 8; ++i) { + const CSAMPLE_GAIN cross_mix = cross_inc * i; + pDestSrcFadeIn[i * 8 + 3] *= cross_mix; + pDestSrcFadeIn[i * 8 + 3] += pSrcFadeOut[i * 8 + 3] * (CSAMPLE_GAIN_ONE - cross_mix); + } + /// note: LOOP VECTORIZED only with "int i" (not SINT i) + for (int i = 0; i < numSamples / 8; ++i) { + const CSAMPLE_GAIN cross_mix = cross_inc * i; + pDestSrcFadeIn[i * 8 + 4] *= cross_mix; + pDestSrcFadeIn[i * 8 + 4] += pSrcFadeOut[i * 8 + 4] * (CSAMPLE_GAIN_ONE - cross_mix); + } + // note: LOOP VECTORIZED only with "int i" (not SINT i) + for (int i = 0; i < numSamples / 8; ++i) { + const CSAMPLE_GAIN cross_mix = cross_inc * i; + pDestSrcFadeIn[i * 8 + 5] *= cross_mix; + pDestSrcFadeIn[i * 8 + 5] += pSrcFadeOut[i * 8 + 5] * (CSAMPLE_GAIN_ONE - cross_mix); + } + /// note: LOOP VECTORIZED only with "int i" (not SINT i) + for (int i = 0; i < numSamples / 8; ++i) { + const CSAMPLE_GAIN cross_mix = cross_inc * i; + pDestSrcFadeIn[i * 8 + 8] *= cross_mix; + pDestSrcFadeIn[i * 8 + 8] += pSrcFadeOut[i * 8 + 6] * (CSAMPLE_GAIN_ONE - cross_mix); + } + // note: LOOP VECTORIZED only with "int i" (not SINT i) + for (int i = 0; i < numSamples / 8; ++i) { + const CSAMPLE_GAIN cross_mix = cross_inc * i; + pDestSrcFadeIn[i * 8 + 7] *= cross_mix; + pDestSrcFadeIn[i * 8 + 7] += pSrcFadeOut[i * 8 + 7] * (CSAMPLE_GAIN_ONE - cross_mix); + } +} + +// static +void SampleUtil::linearCrossfadeUnaryBuffersIn( + CSAMPLE* pDestSrcFadeIn, + const CSAMPLE* pSrcFadeOut, + SINT numSamples, + int channelCount) { + int numFrame = numSamples / channelCount; + const CSAMPLE_GAIN cross_inc = CSAMPLE_GAIN_ONE / CSAMPLE_GAIN(numSamples / 2); + for (int c = 0; c < channelCount; c++) { + for (int i = 0; i < numFrame; ++i) { + const CSAMPLE_GAIN cross_mix = cross_inc * i; + pDestSrcFadeIn[i * channelCount + c] *= cross_mix; + pDestSrcFadeIn[i * channelCount + c] += + pSrcFadeOut[i * channelCount + c] * + (CSAMPLE_GAIN_ONE - cross_mix); + } + } +} + // static void SampleUtil::mixStereoToMono(CSAMPLE* M_RESTRICT pDest, const CSAMPLE* M_RESTRICT pSrc, diff --git a/src/util/sample.h b/src/util/sample.h index f94c87ca4fb..ddf17f58a4f 100644 --- a/src/util/sample.h +++ b/src/util/sample.h @@ -235,26 +235,67 @@ class SampleUtil { static void copyClampBuffer(CSAMPLE* pDest, const CSAMPLE* pSrc, SINT numSamples); - // Interleave the samples in pSrc1 and pSrc2 into pDest. iNumSamples must be + // Interleave the samples in pSrc1 and pSrc2 into pDest (stereo). iNumSamples must be // the number of samples in pSrc1 and pSrc2, and pDest must have at least - // space for iNumSamples*2 samples. pDest must not be an alias of pSrc1 or + // space for numFrames*2 samples. pDest must not be an alias of pSrc1 or // pSrc2. static void interleaveBuffer(CSAMPLE* pDest, const CSAMPLE* pSrc1, const CSAMPLE* pSrc2, SINT numSamples); + // Interleave the samples in pSrc1, pSrc2, etc... into pDest (stem stereo). numFrames must be + // the number of samples each pSrc, and pDest must have at least + // space for numFrames*8 samples. pDest must not be an alias any pSrc. + static void interleaveBuffer(CSAMPLE* pDest, + const CSAMPLE* pSrc1, + const CSAMPLE* pSrc2, + const CSAMPLE* pSrc3, + const CSAMPLE* pSrc4, + const CSAMPLE* pSrc5, + const CSAMPLE* pSrc6, + const CSAMPLE* pSrc7, + const CSAMPLE* pSrc8, + SINT numFrames); + // Deinterleave the samples in pSrc alternately into pDest1 and - // pDest2. iNumSamples must be the number of samples in pDest1 and pDest2, - // and pSrc must have at least iNumSamples*2 samples. Neither pDest1 or + // pDest2 (stereo). numFrames must be the number of samples in pDest1 and pDest2, + // and pSrc must have at least numFrames*2 samples. Neither pDest1 or // pDest2 can be aliases of pSrc. - static void deinterleaveBuffer(CSAMPLE* pDest1, CSAMPLE* pDest2, - const CSAMPLE* pSrc, SINT numSamples); + static void deinterleaveBuffer(CSAMPLE* pDest1, + CSAMPLE* pDest2, + const CSAMPLE* pSrc, + SINT numFrames); + + // Deinterleave the samples in pSrc alternately into pDest1, pDest2, etc ti + // pDest8 (stem stereo). numFrames must be the number of samples in each + // pDest*, and pSrc must have at least numFrames*8 samples. None of the + // pDest can be aliases of pSrc. + static void deinterleaveBuffer(CSAMPLE* pDest1, + CSAMPLE* pDest2, + CSAMPLE* pDest3, + CSAMPLE* pDest4, + CSAMPLE* pDest5, + CSAMPLE* pDest6, + CSAMPLE* pDest7, + CSAMPLE* pDest8, + const CSAMPLE* pSrc, + SINT numFrames); /// Crossfade two buffers together. All the buffers must be the same length. /// pDest is in one version the Out and in the other version the In buffer. - static void linearCrossfadeBuffersOut( + static void linearCrossfadeStereoBuffersOut( CSAMPLE* pDestSrcFadeOut, const CSAMPLE* pSrcFadeIn, SINT numSamples); - static void linearCrossfadeBuffersIn( + static void linearCrossfadeStemBuffersOut( + CSAMPLE* pDestSrcFadeOut, const CSAMPLE* pSrcFadeIn, SINT numSamples); + // Generic version used for unoptimised multi channel count + static void linearCrossfadeUnaryBuffersOut( + CSAMPLE* pDestSrcFadeOut, const CSAMPLE* pSrcFadeIn, SINT numSamples, int channelCount); + static void linearCrossfadeStereoBuffersIn( + CSAMPLE* pDestSrcFadeIn, const CSAMPLE* pSrcFadeOut, SINT numSamples); + static void linearCrossfadeStemBuffersIn( CSAMPLE* pDestSrcFadeIn, const CSAMPLE* pSrcFadeOut, SINT numSamples); + // Generic version used for unoptimised multi channel count + static void linearCrossfadeUnaryBuffersIn( + CSAMPLE* pDestSrcFadeIn, const CSAMPLE* pSrcFadeOut, SINT numSamples, int channelCount); // Mix a buffer down to mono, putting the result in both of the channels. // This uses a simple (L+R)/2 method, which assumes that the audio is diff --git a/tools/get-stem-spec.cpp b/tools/get-stem-spec.cpp new file mode 100644 index 00000000000..8ae8f323a2d --- /dev/null +++ b/tools/get-stem-spec.cpp @@ -0,0 +1,78 @@ +#include +#include +#include + +constexpr uint8_t atomPreambleSize = 8; +constexpr uint8_t extendedSizeFieldSize = 8; + +constexpr uint32_t be32(const char* bytes) { + return ((uint8_t)bytes[0] << 24) | ((uint8_t)bytes[1] << 16) | + ((uint8_t)bytes[2] << 8) | (uint8_t)bytes[3]; +} + +constexpr uint64_t be64(const char* bytes) { + return ((uint64_t)be32(bytes) << 32) | be32(bytes + 4); +} + +constexpr auto moovAtom = be32("moov"); +constexpr auto udtaAtom = be32("udta"); +constexpr auto stemAtom = be32("stem"); + +bool read(std::ifstream& stream, char* buffer, uint64_t size) { + stream.read(buffer, size); + return stream.gcount() == size; +} + +uint32_t recursive_search(std::ifstream& file, + const std::vector& path, + uint32_t box_size = 0, + uint32_t pathIdx = 0) { + if (path.size() <= pathIdx) { + return box_size; + } + + uint32_t byteRead = 0; + char buffer[atomPreambleSize]; + while (!file.eof() || (box_size && byteRead >= box_size)) { + file.read(buffer, atomPreambleSize); + byteRead += box_size; + if (be32(buffer + 4) == path[pathIdx]) { + return recursive_search(file, path, be32(buffer) - atomPreambleSize, pathIdx + 1); + } + file.seekg(be32(buffer) - atomPreambleSize, std::ios::cur); + } + return -1; +} + +int main(int argc, char** argv) { + if (argc != 2) { + std::cerr << "usage: " << argv[0] << " " << std::endl; + return 1; + } + + std::ifstream file(argv[1], std::ios::binary); + if (!file) { + std::cerr << "cannot open file " << argv[1] << std::endl; + return -1; + } + + std::vector path = { + be32("moov"), + be32("udta"), + be32("stem"), + }; + + int manifest_size = 0; + if ((manifest_size = recursive_search(file, path)) > 0) { + std::cerr << "steam at " << file.tellg() << std::endl; + std::string stemData(manifest_size, 0); + if (!read(file, stemData.data(), manifest_size)) { + std::cerr << "failed to read stem metadata" << std::endl; + return 1; + } + std::cout << stemData << std::endl; + return 0; + } + std::cerr << "not a stem file" << std::endl; + return 1; +} diff --git a/tools/rubberband_test.cpp b/tools/rubberband_test.cpp new file mode 100644 index 00000000000..c7632831aee --- /dev/null +++ b/tools/rubberband_test.cpp @@ -0,0 +1,108 @@ +#include + +#include +#include +#include + +#define RUBBERBANDV3 (RUBBERBAND_API_MAJOR_VERSION >= 2 && RUBBERBAND_API_MINOR_VERSION >= 7) + +constexpr size_t buffer_size = 160000; +constexpr size_t channel_count = 8; +constexpr size_t channel_sample_count = buffer_size / channel_count; +constexpr size_t kAlignment = 32; + +using RubberBand::RubberBandStretcher; + +int main(int argc, char** argv) { + if (argc != 2) { + std::cerr << "usage: " << argv[0] << " " << std::endl; + return 1; + } + + std::ifstream file(argv[1], std::ios::binary); + if (!file) { + std::cerr << "cannot open file " << argv[1] << std::endl; + return -1; + } + + float* interleaved_buffer = static_cast( + std::aligned_alloc(kAlignment, sizeof(float) * buffer_size)); + + float* deinterleaved_buffer[channel_count]; + + for (int c = 0; c < channel_count; c++) { + deinterleaved_buffer[c] = static_cast(std::aligned_alloc( + kAlignment, sizeof(float) * channel_sample_count)); + } + + RubberBandStretcher::Options rubberbandOptions = + RubberBandStretcher::OptionProcessRealTime; +#if RUBBERBANDV3 + rubberbandOptions |= + RubberBandStretcher::OptionEngineFiner | + // Process Channels Together. otherwise the result is not + // mono-compatible. See #11361 + RubberBandStretcher::OptionChannelsTogether; +#endif + RubberBandStretcher rubber_band( + 44100, + channel_count, + rubberbandOptions, + 1.0, + 1.0); + + // rubber_band.setTimeRatio(1.1); + // rubber_band.setPitchScale(1.1); + +#if RUBBERBANDV3 + int rubberband_version = rubber_band.getEngineVersion(); +#else + int rubberband_version = 2; +#endif + std::cerr << "Using RubberBand " << rubberband_version << std::endl; + + while (!file.eof()) { + size_t to_read = buffer_size * sizeof(float); + do { + file.read((char*)interleaved_buffer, to_read); + to_read -= file.gcount(); + } while (to_read > 0 && !file.eof()); + + for (int i = 0; i < channel_sample_count; i++) { + for (int c = 0; c < channel_count; c++) { + deinterleaved_buffer[c][i] = + interleaved_buffer[channel_count * i + c] / + std::numeric_limits::max(); + // std::cerr< 0) { + int frame_to_fetch = std::min((int)channel_sample_count, frame_count); + std::cerr << frame_count << " frame available. Fetching" << frame_to_fetch << std::endl; + if (rubber_band.retrieve( + deinterleaved_buffer, frame_to_fetch) != frame_to_fetch) { + std::cerr << "Problem with frame count" << std::endl; + return 1; + } + std::fill(interleaved_buffer, interleaved_buffer + frame_to_fetch, .0f); + for (int i = 0; i < frame_to_fetch; i++) { + for (int c = 0; c < channel_count / 2; c++) { + interleaved_buffer[2 * i] += + deinterleaved_buffer[2 * c][i] * + std::numeric_limits::max(); + interleaved_buffer[2 * i + 1] += + deinterleaved_buffer[2 * c + 1][i] * + std::numeric_limits::max(); + } + } + std::cout.write((char*)interleaved_buffer, sizeof(float) * frame_to_fetch * 2); + frame_count -= frame_to_fetch; + } + } + + return 0; +}