diff --git a/src/analyzer/analyzersilence.cpp b/src/analyzer/analyzersilence.cpp index dde96531b887..1f157434a523 100644 --- a/src/analyzer/analyzersilence.cpp +++ b/src/analyzer/analyzersilence.cpp @@ -74,10 +74,11 @@ SINT AnalyzerSilence::findLastSoundInChunk(std::span samples) { // static bool AnalyzerSilence::verifyFirstSound( std::span samples, - mixxx::audio::FramePos firstSoundFrame) { + mixxx::audio::FramePos firstSoundFrame, + mixxx::audio::ChannelCount channelCount) { const SINT firstSoundSample = findFirstSoundInChunk(samples); if (firstSoundSample < static_cast(samples.size())) { - return mixxx::audio::FramePos::fromEngineSamplePos(firstSoundSample) + return mixxx::audio::FramePos::fromEngineSamplePos(firstSoundSample, channelCount) .toLowerFrameBoundary() == firstSoundFrame.toLowerFrameBoundary(); } return false; diff --git a/src/analyzer/analyzersilence.h b/src/analyzer/analyzersilence.h index c57a9750400e..9adff59f39f3 100644 --- a/src/analyzer/analyzersilence.h +++ b/src/analyzer/analyzersilence.h @@ -46,7 +46,8 @@ class AnalyzerSilence : public Analyzer { /// last analysis run and is an indicator for file edits or decoder /// changes/issues static bool verifyFirstSound(std::span samples, - mixxx::audio::FramePos firstSoundFrame); + mixxx::audio::FramePos firstSoundFrame, + mixxx::audio::ChannelCount channelCount); private: UserSettingsPointer m_pConfig; diff --git a/src/analyzer/constants.h b/src/analyzer/constants.h index a84e0b307c2c..d8048e0175bf 100644 --- a/src/analyzer/constants.h +++ b/src/analyzer/constants.h @@ -8,7 +8,7 @@ namespace mixxx { // depending on the track length. A block size of 4096 frames per block // seems to do fine. Signal processing during analysis uses the same, // fixed number of channels like the engine does, usually 2 = stereo. -constexpr audio::ChannelCount kAnalysisChannels = mixxx::kEngineChannelCount; +constexpr audio::ChannelCount kAnalysisChannels = mixxx::kEngineChannelOutputCount; constexpr SINT kAnalysisFramesPerChunk = 4096; constexpr SINT kAnalysisSamplesPerChunk = kAnalysisFramesPerChunk * kAnalysisChannels; diff --git a/src/audio/frame.h b/src/audio/frame.h index 1136196d6ac9..4b3308203225 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::kEngineChannelOutputCount) { + 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::kEngineChannelOutputCount) 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::kEngineChannelOutputCount) { 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::kEngineChannelOutputCount) 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 2e4254afe96c..c3c2de58d1ec 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 ee9ef873ab41..1cb1c04ea0c4 100644 --- a/src/engine/bufferscalers/enginebufferscale.cpp +++ b/src/engine/bufferscalers/enginebufferscale.cpp @@ -2,11 +2,12 @@ #include "engine/engine.h" #include "moc_enginebufferscale.cpp" +#include "soundio/soundmanagerconfig.h" EngineBufferScale::EngineBufferScale() : m_outputSignal( mixxx::audio::SignalInfo( - mixxx::kEngineChannelCount, + mixxx::kEngineChannelOutputCount, mixxx::audio::SampleRate())), m_dBaseRate(1.0), m_bSpeedAffectsPitch(false), @@ -16,12 +17,22 @@ 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()); + DEBUG_ASSERT(channelCount.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 5480f83312a1..4d058f172f7e 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 a461dd21cb61..ef4780071254 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 362bad588989..7775d012bee1 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 5e61b4ede742..b550a087b104 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,7 +92,7 @@ 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?? @@ -98,6 +100,26 @@ void EngineBufferScaleRubberBand::onSampleRateChanged() { 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 bfd42bc58a15..538aa124ee4a 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 1d46edbf173f..6558b2d33e55 100644 --- a/src/engine/bufferscalers/enginebufferscalest.cpp +++ b/src/engine/bufferscalers/enginebufferscalest.cpp @@ -24,22 +24,20 @@ 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 kBackBufferFrameSize = 512; } // namespace EngineBufferScaleST::EngineBufferScaleST(ReadAheadManager* pReadAheadManager) : m_pReadAheadManager(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 +89,18 @@ void EngineBufferScaleST::setScaleParameters(double base_rate, // changed direction. I removed it because this is handled by EngineBuffer. } -void EngineBufferScaleST::onSampleRateChanged() { - m_bufferBack.clear(); +void EngineBufferScaleST::onOutputSignalChanged() { + int backBufferSize = kBackBufferFrameSize * getOutputSignal().getChannelCount(); + if (m_bufferBack.size() == backBufferSize) { + m_bufferBack.clear(); + } else { + m_bufferBack = mixxx::SampleBuffer(backBufferSize); + } 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 +153,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 db3fb773b0a0..66e5543c6811 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 45a83cf525a4..8d3f10c096f4 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 6d18841fd777..bd9a6b70fe5e 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 1217dc00f47a..3d51a831f8df 100644 --- a/src/engine/cachingreader/cachingreaderchunk.cpp +++ b/src/engine/cachingreader/cachingreaderchunk.cpp @@ -20,7 +20,9 @@ CachingReaderChunk::CachingReaderChunk( mixxx::SampleBuffer::WritableSlice sampleBuffer) : m_index(kInvalidChunkIndex), m_sampleBuffer(std::move(sampleBuffer)) { - DEBUG_ASSERT(m_sampleBuffer.length() == kSamples); + DEBUG_ASSERT(m_sampleBuffer.length() == + CachingReaderChunk::kMaxSupportedChannels * + CachingReaderChunk::kFrames); } void CachingReaderChunk::init(SINT index) { @@ -49,17 +51,32 @@ 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, or an + // uneven number of 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 { + DEBUG_ASSERT(pAudioSource->getSignalInfo().getChannelCount() <= + CachingReaderChunk::kMaxSupportedChannels); + 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 +84,21 @@ mixxx::IndexRange CachingReaderChunk::bufferSampleFrames( mixxx::IndexRange CachingReaderChunk::readBufferedSampleFrames( CSAMPLE* sampleBuffer, + mixxx::audio::ChannelCount channelCount, const mixxx::IndexRange& frameIndexRange) const { DEBUG_ASSERT(m_index != kInvalidChunkIndex); + DEBUG_ASSERT(channelCount <= CachingReaderChunk::kMaxSupportedChannels); 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 +109,21 @@ mixxx::IndexRange CachingReaderChunk::readBufferedSampleFrames( mixxx::IndexRange CachingReaderChunk::readBufferedSampleFramesReverse( CSAMPLE* reverseSampleBuffer, + mixxx::audio::ChannelCount channelCount, const mixxx::IndexRange& frameIndexRange) const { DEBUG_ASSERT(m_index != kInvalidChunkIndex); + DEBUG_ASSERT(channelCount <= CachingReaderChunk::kMaxSupportedChannels); 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), diff --git a/src/engine/cachingreader/cachingreaderchunk.h b/src/engine/cachingreader/cachingreaderchunk.h index c8c04e0b1b31..49e9cf8364e8 100644 --- a/src/engine/cachingreader/cachingreaderchunk.h +++ b/src/engine/cachingreader/cachingreaderchunk.h @@ -23,22 +23,25 @@ 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; // 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 +70,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: + protected: explicit CachingReaderChunk( mixxx::SampleBuffer::WritableSlice sampleBuffer); virtual ~CachingReaderChunk() = default; void init(SINT index); -private: - SINT frameIndexOffset() const noexcept { + private: + SINT frameIndexOffset() const noexcept { return m_index * kFrames; - } + } SINT m_index; @@ -99,22 +103,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 872459c09ca9..6fd74673078e 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() @@ -233,9 +233,14 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { m_pReaderStatusFIFO->writeBlocking(&update, 1); // Emit that the track is loaded. + mixxx::audio::ChannelCount chCount = m_pAudioSource->getSignalInfo().getChannelCount(); + if (chCount % 2 != 0) { + // The engine only support a set of stereo channel + chCount = mixxx::audio::ChannelCount::stereo(); + } const double sampleCount = - CachingReaderChunk::dFrames2samples( - m_pAudioSource->frameLength()); + CachingReaderChunk::dFrames2samples(m_pAudioSource->frameLength(), + chCount); // This code is a workaround until we have found a better solution to // verify and correct offsets. @@ -252,6 +257,7 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { emit trackLoaded( pTrack, m_pAudioSource->getSignalInfo().getSampleRate(), + m_pAudioSource->getSignalInfo().getChannelCount(), sampleCount); } @@ -261,7 +267,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,12 +278,14 @@ 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), - mixxx::audio::FramePos(1))) { + if (AnalyzerSilence::verifyFirstSound(sampleBuffer.span(), + mixxx::audio::FramePos(1), + channelCount)) { qDebug() << "First sound found at the previously stored position"; } else { // This can happen in case of track edits or replacements, changed diff --git a/src/engine/cachingreader/cachingreaderworker.h b/src/engine/cachingreader/cachingreaderworker.h index 309edbd1d05c..9c10d9431eff 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 65e38b5ee60d..149e40230fa9 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::kEngineChannelOutputCount; + 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::kEngineChannelOutputCount) { + // Process a single mono or 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 ca87feda2ad0..670787270b0a 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; @@ -70,10 +71,16 @@ class EngineDeck : public EngineChannel, public AudioDestination { void slotPassthroughChangeRequest(double v); private: + // Process multiple channels and mix them together into the passed buffer + void processStem(CSAMPLE* pOutput, const int iBufferSize); + UserSettingsPointer m_pConfig; EngineBuffer* m_pBuffer; EnginePregain* m_pPregain; + // Stem buffer used to retrieve all the channel to mix together + mixxx::SampleBuffer m_stemBuffer; + // Begin vinyl passthrough fields QScopedPointer m_pInputConfigured; ControlPushButton* m_pPassing; diff --git a/src/engine/controls/bpmcontrol.cpp b/src/engine/controls/bpmcontrol.cpp index 52bef4ca3ba4..0b62f8b42c9a 100644 --- a/src/engine/controls/bpmcontrol.cpp +++ b/src/engine/controls/bpmcontrol.cpp @@ -225,7 +225,8 @@ void BpmControl::slotTranslateBeatsMove(double v) { if (pBeats) { // TODO(rryan): Track::frameInfo is possibly inaccurate! const double sampleOffset = frameInfo().sampleRate * v * 0.01; - const mixxx::audio::FrameDiff_t frameOffset = sampleOffset / mixxx::kEngineChannelCount; + const mixxx::audio::FrameDiff_t frameOffset = + sampleOffset / mixxx::kEngineChannelOutputCount; const auto translatedBeats = pBeats->tryTranslate(frameOffset); if (translatedBeats) { pTrack->trySetBeats(*translatedBeats); diff --git a/src/engine/controls/loopingcontrol.cpp b/src/engine/controls/loopingcontrol.cpp index d14a498b60d2..892d9cd274ae 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 96167a419271..21294f359dbe 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 6aff599f7e4d..0d5b9753eea3 100644 --- a/src/engine/effects/engineeffect.cpp +++ b/src/engine/effects/engineeffect.cpp @@ -182,7 +182,7 @@ bool EngineEffect::process(const ChannelHandle& inputHandle, //TODO: refactor rest of audio engine to use mixxx::AudioParameters const mixxx::EngineParameters engineParameters( sampleRate, - numSamples / mixxx::kEngineChannelCount); + numSamples / mixxx::kEngineChannelOutputCount); m_pProcessor->process(inputHandle, outputHandle, @@ -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/effects/engineeffectsdelay.h b/src/engine/effects/engineeffectsdelay.h index 5cdde538e7e3..964b01a4fae3 100644 --- a/src/engine/effects/engineeffectsdelay.h +++ b/src/engine/effects/engineeffectsdelay.h @@ -9,7 +9,7 @@ namespace { static constexpr int kMaxDelayFrames = mixxx::audio::SampleRate::kValueMax - 1; static constexpr int kDelayBufferSize = - mixxx::audio::SampleRate::kValueMax * mixxx::kEngineChannelCount; + mixxx::audio::SampleRate::kValueMax * mixxx::kEngineChannelOutputCount; } // anonymous namespace /// The effect can produce the output signal with a specific delay caused @@ -53,7 +53,7 @@ class EngineEffectsDelay final : public EngineObject { // to aware problems with a number of channels. The inner // EngineEffectsDelay structure works with delay samples, so the value // is recalculated for the EngineEffectsDelay usage. - m_currentDelaySamples = delayFrames * mixxx::kEngineChannelCount; + m_currentDelaySamples = delayFrames * mixxx::kEngineChannelOutputCount; } /// The method delays the input buffer by the set number of samples diff --git a/src/engine/engine.h b/src/engine/engine.h index 8022444444b9..4df4ab6c270e 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -4,9 +4,10 @@ namespace mixxx { - // TODO(XXX): When we move from stereo to multi-channel this needs updating. -static constexpr audio::ChannelCount kEngineChannelCount = +static constexpr audio::ChannelCount kEngineChannelOutputCount = audio::ChannelCount::stereo(); +static constexpr audio::ChannelCount kMaxEngineChannelInputCount = + audio::ChannelCount::stem(); // Contains the information needed to process a buffer of audio class EngineParameters final { @@ -30,7 +31,7 @@ class EngineParameters final { audio::SampleRate sampleRate, SINT framesPerBuffer) : m_outputSignal( - kEngineChannelCount, + kEngineChannelOutputCount, sampleRate), m_framesPerBuffer(framesPerBuffer) { DEBUG_ASSERT(framesPerBuffer > 0); diff --git a/src/engine/enginebuffer.cpp b/src/engine/enginebuffer.cpp index f4ab6cfe7cd4..f381fb507463 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,7 +89,9 @@ EngineBuffer::EngineBuffer(const QString& group, m_iEnableSyncQueued(SYNC_REQUEST_NONE), m_iSyncModeQueued(static_cast(SyncMode::Invalid)), m_bPlayAfterLoading(false), - m_pCrossfadeBuffer(SampleUtil::alloc(kMaxEngineSamples)), + m_channelCount(mixxx::kEngineChannelOutputCount), + m_pCrossfadeBuffer(SampleUtil::alloc( + kMaxEngineFrames * mixxx::kMaxEngineChannelInputCount)), m_bCrossfadeReady(false), m_iLastBufferSize(0) { // This should be a static assertion, but isValid() is not constexpr. @@ -100,7 +100,7 @@ EngineBuffer::EngineBuffer(const QString& group, m_queuedSeek.setValue(kNoQueuedSeek); // zero out crossfade buffer - SampleUtil::clear(m_pCrossfadeBuffer, kMaxEngineSamples); + SampleUtil::clear(m_pCrossfadeBuffer, kMaxEngineFrames * mixxx::kMaxEngineChannelInputCount); m_pReader = new CachingReader(group, pConfig); connect(m_pReader, &CachingReader::trackLoading, @@ -451,7 +451,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 +470,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 +522,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,7 +540,28 @@ void EngineBuffer::slotTrackLoaded(TrackPointer pTrack, m_visualPlayPos->setInvalid(); m_playPos = kInitialPlayPosition; // for execute seeks to 0.0 m_pCurrentTrack = pTrack; - m_pTrackSamples->set(trackNumSamples); + + m_channelCount = trackChannelCount; + if (m_channelCount > mixxx::audio::ChannelCount::stereo()) { + // The sample count is indicated downmix. This means that for stem + // track, we only consider the track in stereo, as it is perceived by + // the user on deck output + double channelCount = m_channelCount.toDouble(); + VERIFY_OR_DEBUG_ASSERT(m_channelCount % mixxx::audio::ChannelCount::stereo() == 0) { + // Make it stereo for the frame calculation + channelCount -= 1; + kLogger.warning() << "Uneven number of channel in the track is not supported"; + }; + m_pTrackSamples->set(trackNumSamples / + (channelCount / mixxx::audio::ChannelCount::stereo().toDouble())); + } else { + // The EngineBuffer only works with stereo channels. If the track is + // mono, it will be passed through the AudioSourceStereoProxy. See + // CachingReaderChunk::bufferSampleFrames + m_channelCount = mixxx::audio::ChannelCount::stereo(); + m_pTrackSamples->set(trackNumSamples); + } + m_pTrackSampleRate->set(trackSampleRate.toDouble()); m_pTrackLoaded->forceSet(1); @@ -874,11 +897,18 @@ 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 outputBufferSize = iBufferSize, + stereoPairCount = m_channelCount / mixxx::audio::ChannelCount::stereo(); + // The speed is calculated out of the buffer size for the stereo channel + // output, after mixing multi channel (stem) together + if (stereoPairCount > 1) { + outputBufferSize = iBufferSize / stereoPairCount; + } double speed = m_pRateControl->calculateSpeed( baseSampleRate, tempoRatio, paused, - iBufferSize, + outputBufferSize, &is_scratching, &is_reverse); @@ -1074,8 +1104,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 +1116,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::linearCrossfadeStereoBuffersIn( + pOutput, m_pCrossfadeBuffer, iBufferSize); + break; + case mixxx::audio::ChannelCount::stem(): + SampleUtil::linearCrossfadeStemBuffersIn( + pOutput, m_pCrossfadeBuffer, iBufferSize); + break; + default: + // Fallback to unoptimised function + SampleUtil::linearCrossfadeUnaryBuffersIn( + 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 +1185,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 +1204,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 +1275,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 +1495,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::kEngineChannelOutputCount * m_pSampleRate->get() / kPlaypositionUpdateRate)) { m_playposSlider->set(fFractionalPlaypos); m_pCueControl->updateIndicators(); @@ -1464,7 +1507,7 @@ void EngineBuffer::updateIndicators(double speed, int iBufferSize) { fFractionalPlaypos, speed * m_baserate_old, static_cast(iBufferSize) / - m_trackEndPositionOld.toEngineSamplePos(), + m_trackEndPositionOld.toEngineSamplePos(mixxx::kEngineChannelOutputCount), fFractionalSlipPos, effectiveSlipRate, m_slipModeState, @@ -1474,7 +1517,7 @@ void EngineBuffer::updateIndicators(double speed, int iBufferSize) { fFractionalLoopStartPos, fFractionalLoopEndPos, tempoTrackSeconds, - iBufferSize / kSamplesPerFrame / m_sampleRate.toDouble() * 1000000.0); + iBufferSize / mixxx::kEngineChannelOutputCount / 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 +1528,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 +1593,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 113286187d60..06e7be8f49f3 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 @@ -457,6 +461,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/enginedelay.cpp b/src/engine/enginedelay.cpp index 0add163d763d..c8054b0c8333 100644 --- a/src/engine/enginedelay.cpp +++ b/src/engine/enginedelay.cpp @@ -10,7 +10,7 @@ namespace { constexpr double kdMaxDelayPot = 500; const int kiMaxDelay = static_cast((kdMaxDelayPot + 8) / 1000 * - mixxx::audio::SampleRate::kValueMax * mixxx::kEngineChannelCount); + mixxx::audio::SampleRate::kValueMax * mixxx::kEngineChannelOutputCount); const QString kAppGroup = QStringLiteral("[App]"); } // anonymous namespace diff --git a/src/engine/filters/enginefilterdelay.h b/src/engine/filters/enginefilterdelay.h index a5ea629ee2d6..ff6f798fd3e7 100644 --- a/src/engine/filters/enginefilterdelay.h +++ b/src/engine/filters/enginefilterdelay.h @@ -6,7 +6,7 @@ template class EngineFilterDelay : public EngineObjectConstIn { - static_assert(SIZE % mixxx::kEngineChannelCount == 0, + static_assert(SIZE % mixxx::kEngineChannelOutputCount == 0, "The buffer size has to be divisible by the number of channels."); public: @@ -31,7 +31,7 @@ class EngineFilterDelay : public EngineObjectConstIn { } void setDelay(unsigned int delaySamples) { - unsigned int unalignedSamples = delaySamples % mixxx::kEngineChannelCount; + unsigned int unalignedSamples = delaySamples % mixxx::kEngineChannelOutputCount; VERIFY_OR_DEBUG_ASSERT(unalignedSamples == 0) { // Round to the previous multiple of the number of channel count. @@ -39,7 +39,7 @@ class EngineFilterDelay : public EngineObjectConstIn { } VERIFY_OR_DEBUG_ASSERT(delaySamples < SIZE) { - delaySamples = SIZE - mixxx::kEngineChannelCount; + delaySamples = SIZE - mixxx::kEngineChannelOutputCount; } m_delaySamples = delaySamples; diff --git a/src/engine/filters/enginefilteriir.h b/src/engine/filters/enginefilteriir.h index f14ff5a0d865..148c2cd3d56b 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 f9b5b0fbdec6..b45af803cc3b 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/filters/enginefilterpan.h b/src/engine/filters/enginefilterpan.h index 9f2986faaa60..31e99ef9a6e8 100644 --- a/src/engine/filters/enginefilterpan.h +++ b/src/engine/filters/enginefilterpan.h @@ -122,7 +122,7 @@ class EngineFilterPan : public EngineObjectConstIn { int m_leftDelayFrames; int m_oldLeftDelayFrames; int m_delayFrame; - CSAMPLE m_buf[SIZE * mixxx::kEngineChannelCount]; + CSAMPLE m_buf[SIZE * mixxx::kEngineChannelOutputCount]; bool m_doRamping; bool m_doStart; }; diff --git a/src/engine/filters/enginefilterpansingle.h b/src/engine/filters/enginefilterpansingle.h index cbe292ff6672..053793552996 100644 --- a/src/engine/filters/enginefilterpansingle.h +++ b/src/engine/filters/enginefilterpansingle.h @@ -77,6 +77,6 @@ class EngineFilterPanSingle { protected: int m_delayFrame; - CSAMPLE m_buf[SIZE * mixxx::kEngineChannelCount]; + CSAMPLE m_buf[SIZE * mixxx::kEngineChannelOutputCount]; bool m_doStart; }; diff --git a/src/engine/readaheadmanager.cpp b/src/engine/readaheadmanager.cpp index 09a8dc0030a1..e4b9b770769f 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,13 +34,15 @@ 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"; + qDebug() << "ERROR: Non-aligned requested_samples to ReadAheadManager::getNextSamples"; requested_samples -= modSamples; } bool in_reverse = dRate < 0; @@ -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); @@ -147,7 +151,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; @@ -156,7 +160,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; @@ -169,7 +175,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 @@ -181,10 +188,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; + } } } } @@ -212,7 +237,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; @@ -224,11 +251,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, @@ -260,8 +287,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; } @@ -285,7 +313,7 @@ double ReadAheadManager::getFilePlaypositionFromLog( if (m_pRateControl) { const auto seekPosition = mixxx::audio::FramePos::fromEngineSamplePos( - entry.virtualPlaypositionStart); + entry.virtualPlaypositionStart, channelCount); m_pRateControl->notifySeek(seekPosition); } } @@ -306,9 +334,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 ae71a5f6b1c1..d320c7e814b9 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/engine/sidechain/enginesidechain.cpp b/src/engine/sidechain/enginesidechain.cpp index 306b161aec53..49fbdf1f6563 100644 --- a/src/engine/sidechain/enginesidechain.cpp +++ b/src/engine/sidechain/enginesidechain.cpp @@ -71,13 +71,13 @@ void EngineSideChain::receiveBuffer(const AudioInput& input, } // Just copy the received samples form the sound card input to the // engine. After processing we get it back via writeSamples() - SampleUtil::copy(m_pSidechainMix, pBuffer, iFrames * mixxx::kEngineChannelCount); + SampleUtil::copy(m_pSidechainMix, pBuffer, iFrames * mixxx::kEngineChannelOutputCount); } void EngineSideChain::writeSamples(const CSAMPLE* pBuffer, int iFrames) { Trace sidechain("EngineSideChain::writeSamples"); // TODO: remove assumption of stereo buffer - const int numSamples = iFrames * mixxx::kEngineChannelCount; + const int numSamples = iFrames * mixxx::kEngineChannelOutputCount; const int numSamplesWritten = m_sampleFifo.write(pBuffer, numSamples); if (numSamplesWritten != numSamples) { diff --git a/src/library/dao/cuedao.cpp b/src/library/dao/cuedao.cpp index 28861f0f67bb..5533382e3ddd 100644 --- a/src/library/dao/cuedao.cpp +++ b/src/library/dao/cuedao.cpp @@ -41,7 +41,8 @@ CuePointer cueFromRow(const QSqlRecord& row) { const auto position = mixxx::audio::FramePos::fromEngineSamplePosMaybeInvalid( row.value(row.indexOf("position")).toDouble()); - double lengthFrames = row.value(row.indexOf("length")).toDouble() / mixxx::kEngineChannelCount; + double lengthFrames = row.value(row.indexOf("length")).toDouble() / + mixxx::kEngineChannelOutputCount; int hotcue = row.value(row.indexOf("hotcue")).toInt(); QString label = labelFromQVariant(row.value(row.indexOf("label"))); mixxx::RgbColor::optional_t color = mixxx::RgbColor::fromQVariant(row.value(row.indexOf("color"))); @@ -179,7 +180,7 @@ bool CueDAO::saveCue(TrackId trackId, Cue* cue) const { query.bindValue(":track_id", trackId.toVariant()); query.bindValue(":type", static_cast(cue->getType())); query.bindValue(":position", cue->getPosition().toEngineSamplePosMaybeInvalid()); - query.bindValue(":length", cue->getLengthFrames() * mixxx::kEngineChannelCount); + query.bindValue(":length", cue->getLengthFrames() * mixxx::kEngineChannelOutputCount); query.bindValue(":hotcue", cue->getHotCue()); query.bindValue(":label", labelToQVariant(cue->getLabel())); query.bindValue(":color", mixxx::RgbColor::toQVariant(cue->getColor())); diff --git a/src/mixer/basetrackplayer.cpp b/src/mixer/basetrackplayer.cpp index b59baec68d5b..f0ab52a44636 100644 --- a/src/mixer/basetrackplayer.cpp +++ b/src/mixer/basetrackplayer.cpp @@ -267,7 +267,7 @@ BaseTrackPlayerImpl::~BaseTrackPlayerImpl() { TrackPointer BaseTrackPlayerImpl::loadFakeTrack(bool bPlay, double filebpm) { TrackPointer pTrack(Track::newTemporary()); pTrack->setAudioProperties( - mixxx::kEngineChannelCount, + mixxx::kEngineChannelOutputCount, mixxx::audio::SampleRate(44100), mixxx::audio::Bitrate(), mixxx::Duration::fromSeconds(10)); diff --git a/src/sources/soundsourcecoreaudio.cpp b/src/sources/soundsourcecoreaudio.cpp index 83006a8f9c7a..db0f76efbab0 100644 --- a/src/sources/soundsourcecoreaudio.cpp +++ b/src/sources/soundsourcecoreaudio.cpp @@ -116,9 +116,9 @@ SoundSource::OpenResult SoundSourceCoreAudio::tryOpen( // create the output format const UInt32 numChannels = - params.getSignalInfo().getChannelCount().isValid() ? - params.getSignalInfo().getChannelCount() : - mixxx::kEngineChannelCount; + params.getSignalInfo().getChannelCount().isValid() + ? params.getSignalInfo().getChannelCount() + : mixxx::kEngineChannelOutputCount; m_outputFormat = CAStreamBasicDescription(m_inputFormat.mSampleRate, numChannels, CAStreamBasicDescription::kPCMFormatFloat32, diff --git a/src/test/analyzersilence_test.cpp b/src/test/analyzersilence_test.cpp index eee9c7affbe1..af171ecb9941 100644 --- a/src/test/analyzersilence_test.cpp +++ b/src/test/analyzersilence_test.cpp @@ -11,7 +11,7 @@ namespace { -constexpr mixxx::audio::ChannelCount kChannelCount = mixxx::kEngineChannelCount; +constexpr mixxx::audio::ChannelCount kChannelCount = mixxx::kEngineChannelOutputCount; constexpr int kTrackLengthFrames = 100000; constexpr double kTonePitchHz = 1000.0; // 1kHz @@ -234,8 +234,14 @@ TEST_F(AnalyzerSilenceTest, verifyFirstSound) { -0.0020f}; std::span samples = s; - EXPECT_EQ(false, AnalyzerSilence::verifyFirstSound(samples, mixxx::audio::FramePos(5))); - EXPECT_EQ(true, AnalyzerSilence::verifyFirstSound(samples, mixxx::audio::FramePos(4))); + EXPECT_EQ(false, + AnalyzerSilence::verifyFirstSound(samples, + mixxx::audio::FramePos(5), + mixxx::audio::ChannelCount::stereo())); + EXPECT_EQ(true, + AnalyzerSilence::verifyFirstSound(samples, + mixxx::audio::FramePos(4), + mixxx::audio::ChannelCount::stereo())); } } // namespace diff --git a/src/test/autodjprocessor_test.cpp b/src/test/autodjprocessor_test.cpp index 866fbaaf5377..1cedaac6db5e 100644 --- a/src/test/autodjprocessor_test.cpp +++ b/src/test/autodjprocessor_test.cpp @@ -23,7 +23,7 @@ using ::testing::Return; namespace { const int kDefaultTransitionTime = 10; -const mixxx::audio::ChannelCount kChannelCount = mixxx::kEngineChannelCount; +const mixxx::audio::ChannelCount kChannelCount = mixxx::kEngineChannelOutputCount; const QString kTrackLocationTest = QStringLiteral("id3-test-data/cover-test-png.mp3"); const QString kAppGroup = QStringLiteral("[App]"); } // namespace diff --git a/src/test/controllers/controller_columnid_regression_test.cpp b/src/test/controllers/controller_columnid_regression_test.cpp index 81f3379a4fdf..09c8f0d67782 100644 --- a/src/test/controllers/controller_columnid_regression_test.cpp +++ b/src/test/controllers/controller_columnid_regression_test.cpp @@ -57,7 +57,10 @@ QHash TEST_F(ControllerLibraryColumnIDRegressionTest, ensureS4MK3) { std::shared_ptr pMapping = LegacyControllerMappingFileHandler::loadMapping( - QFileInfo("res/controllers/Traktor Kontrol S4 MK3.hid.xml"), QDir()); + QFileInfo(getTestDir().filePath( + "../../res/controllers/Traktor Kontrol S4 " + "MK3.hid.xml")), + QDir()); EXPECT_TRUE(pMapping); auto settings = pMapping->getSettings(); EXPECT_TRUE(!settings.isEmpty()); diff --git a/src/test/cue_test.cpp b/src/test/cue_test.cpp index 86cad1d0253b..71ca00283b1d 100644 --- a/src/test/cue_test.cpp +++ b/src/test/cue_test.cpp @@ -37,7 +37,7 @@ TEST(CueTest, ConvertCueInfoToCueRoundtrip) { // in integer numbers. const auto cueInfo1 = CueInfo( CueType::HotCue, - std::make_optional(1.0 * 44100 * mixxx::kEngineChannelCount), + std::make_optional(1.0 * 44100 * mixxx::kEngineChannelOutputCount), std::nullopt, std::make_optional(3), QStringLiteral("label"), diff --git a/src/test/enginebufferscalelineartest.cpp b/src/test/enginebufferscalelineartest.cpp index 1cb4399392ab..632e7bec315a 100644 --- a/src/test/enginebufferscalelineartest.cpp +++ b/src/test/enginebufferscalelineartest.cpp @@ -28,8 +28,12 @@ class ReadAheadManagerMock : public ReadAheadManager { m_iSamplesRead(0) { } - SINT getNextSamplesFake(double dRate, CSAMPLE* buffer, SINT requested_samples) { + SINT getNextSamplesFake(double dRate, + CSAMPLE* buffer, + SINT requested_samples, + mixxx::audio::ChannelCount channelCount) { Q_UNUSED(dRate); + Q_UNUSED(channelCount); bool hasBuffer = m_pBuffer != NULL; // You forgot to set the mock read buffer. EXPECT_TRUE(hasBuffer); @@ -51,7 +55,11 @@ class ReadAheadManagerMock : public ReadAheadManager { return m_iSamplesRead; } - MOCK_METHOD3(getNextSamples, SINT(double dRate, CSAMPLE* buffer, SINT requested_samples)); + MOCK_METHOD4(getNextSamples, + SINT(double dRate, + CSAMPLE* buffer, + SINT requested_samples, + mixxx::audio::ChannelCount channelCount)); CSAMPLE* m_pBuffer; SINT m_iBufferSize; @@ -74,7 +82,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); } @@ -137,7 +146,7 @@ TEST_F(EngineBufferScaleLinearTest, ScaleConstant) { m_pReadAheadMock->setReadBuffer(readBuffer, 1); // Tell the RAMAN mock to invoke getNextSamplesFake - EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _)) + EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _, _)) .WillRepeatedly(Invoke(m_pReadAheadMock, &ReadAheadManagerMock::getNextSamplesFake)); CSAMPLE* pOutput = SampleUtil::alloc(kiLinearScaleReadAheadLength); @@ -157,7 +166,7 @@ TEST_F(EngineBufferScaleLinearTest, UnityRateIsSamplePerfect) { SetRateNoLerp(1.0); // Tell the RAMAN mock to invoke getNextSamplesFake - EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _)) + EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _, _)) .WillRepeatedly(Invoke(m_pReadAheadMock, &ReadAheadManagerMock::getNextSamplesFake)); QVector readBuffer; @@ -189,7 +198,7 @@ TEST_F(EngineBufferScaleLinearTest, TestRateLERPMonotonicallyProgresses) { m_pReadAheadMock->setReadBuffer(readBuffer, 1); // Tell the RAMAN mock to invoke getNextSamplesFake - EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _)) + EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _, _)) .WillRepeatedly(Invoke(m_pReadAheadMock, &ReadAheadManagerMock::getNextSamplesFake)); CSAMPLE* pOutput = SampleUtil::alloc(kiLinearScaleReadAheadLength); @@ -214,7 +223,7 @@ TEST_F(EngineBufferScaleLinearTest, TestDoubleSpeedSmoothlyHalvesSamples) { m_pReadAheadMock->setReadBuffer(readBuffer, 8); // Tell the RAMAN mock to invoke getNextSamplesFake - EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _)) + EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _, _)) .WillRepeatedly(Invoke(m_pReadAheadMock, &ReadAheadManagerMock::getNextSamplesFake)); CSAMPLE* pOutput = SampleUtil::alloc(kiLinearScaleReadAheadLength); @@ -243,7 +252,7 @@ TEST_F(EngineBufferScaleLinearTest, TestHalfSpeedSmoothlyDoublesSamples) { m_pReadAheadMock->setReadBuffer(readBuffer, 4); // Tell the RAMAN mock to invoke getNextSamplesFake - EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _)) + EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _, _)) .WillRepeatedly(Invoke(m_pReadAheadMock, &ReadAheadManagerMock::getNextSamplesFake)); CSAMPLE* pOutput = SampleUtil::alloc(kiLinearScaleReadAheadLength); @@ -275,7 +284,7 @@ TEST_F(EngineBufferScaleLinearTest, TestRepeatedScaleCalls) { m_pReadAheadMock->setReadBuffer(readBuffer, 4); // Tell the RAMAN mock to invoke getNextSamplesFake - EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _)) + EXPECT_CALL(*m_pReadAheadMock, getNextSamples(_, _, _, _)) .WillRepeatedly(Invoke(m_pReadAheadMock, &ReadAheadManagerMock::getNextSamplesFake)); CSAMPLE expectedResult[] = { -101.0, 101.0, diff --git a/src/test/engineeffectsdelay_test.cpp b/src/test/engineeffectsdelay_test.cpp index 149dcd5b8523..41ff0245fe93 100644 --- a/src/test/engineeffectsdelay_test.cpp +++ b/src/test/engineeffectsdelay_test.cpp @@ -1,7 +1,7 @@ // Tests for engineeffectsdelay.cpp // Premise: internal Mixxx structure works with a stereo signal. -// If the mixxx::kEngineChannelCount wouldn't be a stereo in the future, +// If the mixxx::kEngineChannelOutputCount wouldn't be a stereo in the future, // tests have to be updated. #include "engine/effects/engineeffectsdelay.h" @@ -26,7 +26,7 @@ namespace { -static_assert(mixxx::kEngineChannelCount == mixxx::audio::ChannelCount::stereo(), +static_assert(mixxx::kEngineChannelOutputCount == mixxx::audio::ChannelCount::stereo(), "EngineEffectsDelayTest requires stereo input signal."); class EngineEffectsDelayTest : public MixxxTest { @@ -366,7 +366,7 @@ BENCHMARK(BM_ZeroDelay)->Range(64, 4 << 10); static void BM_DelaySmallerThanBufferSize(benchmark::State& state) { const SINT bufferSizeInSamples = static_cast(state.range(0)); - const SINT bufferSizeInFrames = bufferSizeInSamples / mixxx::kEngineChannelCount; + const SINT bufferSizeInFrames = bufferSizeInSamples / mixxx::kEngineChannelOutputCount; // The delay is half of the buffer size. const SINT delayFrames = bufferSizeInFrames / 2; @@ -386,7 +386,7 @@ BENCHMARK(BM_DelaySmallerThanBufferSize)->Range(64, 4 << 10); static void BM_DelayGreaterThanBufferSize(benchmark::State& state) { const SINT bufferSizeInSamples = static_cast(state.range(0)); - const SINT bufferSizeInFrames = bufferSizeInSamples / mixxx::kEngineChannelCount; + const SINT bufferSizeInFrames = bufferSizeInSamples / mixxx::kEngineChannelOutputCount; // The delay is the same as twice of buffer size. const SINT delayFrames = bufferSizeInFrames * 2; @@ -406,7 +406,7 @@ BENCHMARK(BM_DelayGreaterThanBufferSize)->Range(64, 4 << 10); static void BM_DelayCrossfading(benchmark::State& state) { const SINT bufferSizeInSamples = static_cast(state.range(0)); - const SINT bufferSizeInFrames = bufferSizeInSamples / mixxx::kEngineChannelCount; + const SINT bufferSizeInFrames = bufferSizeInSamples / mixxx::kEngineChannelOutputCount; // The first delay is half of the buffer size. const SINT firstDelayFrames = bufferSizeInFrames / 2; @@ -430,7 +430,7 @@ BENCHMARK(BM_DelayCrossfading)->Range(64, 4 << 10); static void BM_DelayNoCrossfading(benchmark::State& state) { const SINT bufferSizeInSamples = static_cast(state.range(0)); - const SINT bufferSizeInFrames = bufferSizeInSamples / mixxx::kEngineChannelCount; + const SINT bufferSizeInFrames = bufferSizeInSamples / mixxx::kEngineChannelOutputCount; // The delay is half of the buffer size. const SINT delayFrames = bufferSizeInFrames / 2; diff --git a/src/test/mockedenginebackendtest.h b/src/test/mockedenginebackendtest.h index e1a0f1a045e2..14287d62e68b 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 67674f318cb6..718bc5c12d65 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,29 @@ 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())); // start 0.5 to 20.2 = 19.7 // loop 3.3 to 20.2 = 16.9 diff --git a/src/test/soundproxy_test.cpp b/src/test/soundproxy_test.cpp index 592a61684071..6bfdbe7751ce 100644 --- a/src/test/soundproxy_test.cpp +++ b/src/test/soundproxy_test.cpp @@ -216,7 +216,8 @@ TEST_F(SoundSourceProxyTest, openEmptyFile) { ASSERT_TRUE(SoundSourceProxy::isFileNameSupported(tmpFileName)); auto pTrack = Track::newTemporary(tmpFileName); SoundSourceProxy proxy(pTrack); - auto pAudioSource = proxy.openAudioSource(mixxx::AudioSource::OpenParams::make_default()); + + auto pAudioSource = proxy.openAudioSource(); EXPECT_TRUE(!pAudioSource); } } diff --git a/src/test/stems/01-drum.wav b/src/test/stems/01-drum.wav new file mode 100644 index 000000000000..e5b8a3b750d5 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 000000000000..35b71c871550 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 000000000000..11f094497273 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 000000000000..7aaf453db063 Binary files /dev/null and b/src/test/stems/04-vocal.wav differ diff --git a/src/test/stemtest.cpp b/src/test/stemtest.cpp index 0f7f6066295d..27a0ad1078c0 100644 --- a/src/test/stemtest.cpp +++ b/src/test/stemtest.cpp @@ -11,10 +11,18 @@ using namespace mixxx; namespace { +const QList kStemFiles = { + "01-drum.wav", + "02-bass.wav", + "03-melody.wav", + "04-vocal.wav", +}; + class StemTest : public MixxxTest { protected: void SetUp() override { - ASSERT_TRUE(SoundSourceProxy::registerProviders()); + ASSERT_TRUE(SoundSourceProxy::isFileTypeSupported("stem.mp4") || + SoundSourceProxy::registerProviders()); } }; @@ -71,4 +79,60 @@ TEST_F(StemTest, ReadMainMix) { EXPECT_TRUE(0 == std::memcmp(buffer1.data(), buffer1.data(), sizeof(buffer1))); } +TEST_F(StemTest, ReadEachStem) { + int stemIdx = 0; + for (auto& stem : kStemFiles) { + SoundSourceFFmpeg sourceStandaloneStem( + QUrl::fromLocalFile(getTestDir().filePath("stems/" + stem))); + SoundSourceSingleSTEM sourceStem( + QUrl::fromLocalFile( + getTestDir().filePath("stems/test.stem.mp4")), + stemIdx++); + + mixxx::AudioSource::OpenParams config; + config.setChannelCount(mixxx::audio::ChannelCount(2)); + + ASSERT_EQ(sourceStandaloneStem.open(AudioSource::OpenMode::Strict, config), + AudioSource::OpenResult::Succeeded); + ASSERT_EQ(sourceStem.open(AudioSource::OpenMode::Strict, config), + AudioSource::OpenResult::Succeeded); + + ASSERT_EQ(sourceStandaloneStem.getSignalInfo(), sourceStem.getSignalInfo()); + + SampleBuffer buffer1(1024), buffer2(1024); + ASSERT_EQ(sourceStandaloneStem.readSampleFrames(WritableSampleFrames( + IndexRange::between( + 0, + 512), + SampleBuffer::WritableSlice( + buffer1.data(), + buffer1.size()))) + .readableLength(), + buffer1.size()); + ASSERT_EQ(sourceStem.readSampleFrames(WritableSampleFrames( + IndexRange::between( + 0, + 512), + SampleBuffer::WritableSlice( + buffer2.data(), + buffer2.size()))) + .readableLength(), + buffer2.size()); + EXPECT_TRUE(0 == std::memcmp(buffer1.data(), buffer1.data(), sizeof(buffer1))); + } +} + +TEST_F(StemTest, OpenStem) { + SoundSourceSTEM sourceStem(QUrl::fromLocalFile(getTestDir().filePath("stems/test.stem.mp4"))); + + mixxx::AudioSource::OpenParams config; + config.setChannelCount(mixxx::audio::ChannelCount(8)); + ASSERT_EQ(sourceStem.open(AudioSource::OpenMode::Strict, config), + AudioSource::OpenResult::Succeeded); + + ASSERT_EQ(mixxx::audio::SignalInfo(mixxx::audio::ChannelCount::stem(), + mixxx::audio::SampleRate(44100)), + sourceStem.getSignalInfo()); +} + } // namespace diff --git a/src/track/trackrecord.cpp b/src/track/trackrecord.cpp index 521abee151d9..c49b0aacaa0d 100644 --- a/src/track/trackrecord.cpp +++ b/src/track/trackrecord.cpp @@ -334,7 +334,13 @@ bool TrackRecord::updateStreamInfoFromSource( } // Stream properties are not expected to vary during a session VERIFY_OR_DEBUG_ASSERT(!m_streamInfoFromSource || - *m_streamInfoFromSource == streamInfoFromSource) { + *m_streamInfoFromSource == streamInfoFromSource || + // TODO (XXX) currently, because analysis open the file in stereo, + // in is expected to see a channel mismatch when the track gets open + // for playback. This should be removed once analysis is also + // opening the file as a stem. + m_streamInfoFromSource->getSignalInfo().getChannelCount() == + mixxx::audio::ChannelCount::stem()) { kLogger.warning() << "Varying stream properties:" << *m_streamInfoFromSource diff --git a/src/util/sample.cpp b/src/util/sample.cpp index 56bf630c7280..4802db4212f1 100644 --- a/src/util/sample.cpp +++ b/src/util/sample.cpp @@ -162,7 +162,7 @@ CSAMPLE SampleUtil::copyWithRampingNormalization(CSAMPLE* pDest, CSAMPLE_GAIN old_gain, CSAMPLE_GAIN targetAmplitude, SINT numSamples) { - SINT numMonoSamples = numSamples / mixxx::kEngineChannelCount.value(); + SINT numMonoSamples = numSamples / mixxx::kEngineChannelOutputCount.value(); mixMultichannelToMono(pDest, pSrc, numSamples); CSAMPLE maxAmplitude = maxAbsAmplitude(pDest, numMonoSamples); @@ -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 + 6] *= cross_mix; + pDestSrcFadeIn[i * 8 + 6] += 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, @@ -572,7 +771,7 @@ void SampleUtil::mixStereoToMono(CSAMPLE* pBuffer, SINT numSamples) { // static void SampleUtil::mixMultichannelToMono(CSAMPLE* pDest, const CSAMPLE* pSrc, SINT numSamples) { - auto chCount = mixxx::kEngineChannelCount.value(); + auto chCount = mixxx::kEngineChannelOutputCount.value(); const CSAMPLE_GAIN mixScale = CSAMPLE_GAIN_ONE / (CSAMPLE_GAIN_ONE * chCount); for (SINT i = 0; i < numSamples / chCount; ++i) { pDest[i] = CSAMPLE_ZERO; diff --git a/src/util/sample.h b/src/util/sample.h index f94c87ca4fb7..ddf17f58a4ff 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/src/waveform/waveform.cpp b/src/waveform/waveform.cpp index 3f0331e1b312..3f01da89b27f 100644 --- a/src/waveform/waveform.cpp +++ b/src/waveform/waveform.cpp @@ -104,13 +104,13 @@ QByteArray Waveform::toByteArray() const { // TODO(vrince) set max/min for each signal all->set_units(io::Waveform::RMS); - all->set_channels(mixxx::kEngineChannelCount); + all->set_channels(mixxx::kEngineChannelOutputCount); low->set_units(io::Waveform::RMS); - low->set_channels(mixxx::kEngineChannelCount); + low->set_channels(mixxx::kEngineChannelOutputCount); mid->set_units(io::Waveform::RMS); - mid->set_channels(mixxx::kEngineChannelCount); + mid->set_channels(mixxx::kEngineChannelOutputCount); high->set_units(io::Waveform::RMS); - high->set_channels(mixxx::kEngineChannelCount); + high->set_channels(mixxx::kEngineChannelOutputCount); int dataSize = getDataSize(); for (int i = 0; i < dataSize; ++i) { diff --git a/src/widget/woverview.cpp b/src/widget/woverview.cpp index 14cd06146860..e048e92b80a4 100644 --- a/src/widget/woverview.cpp +++ b/src/widget/woverview.cpp @@ -1240,7 +1240,7 @@ void WOverview::paintText(const QString& text, QPainter* pPainter) { double WOverview::samplePositionToSeconds(double sample) { double trackTime = sample / - (m_trackSampleRateControl->get() * mixxx::kEngineChannelCount); + (m_trackSampleRateControl->get() * mixxx::kEngineChannelOutputCount); return trackTime / m_pRateRatioControl->get(); }