diff --git a/src/analyzer/analyzersilence.cpp b/src/analyzer/analyzersilence.cpp index dde96531b88..0ba2da32811 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::fromSamplePos(firstSoundSample, channelCount) .toLowerFrameBoundary() == firstSoundFrame.toLowerFrameBoundary(); } return false; diff --git a/src/analyzer/analyzersilence.h b/src/analyzer/analyzersilence.h index c57a9750400..9adff59f39f 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 a84e0b307c2..d8048e0175b 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 1136196d6ac..1e3ffb1c8a3 100644 --- a/src/audio/frame.h +++ b/src/audio/frame.h @@ -36,7 +36,17 @@ class FramePos final { /// "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); + return FramePos(engineSamplePos / mixxx::kEngineChannelOutputCount); + } + + static constexpr FramePos fromSamplePos(double samplePos, + mixxx::audio::ChannelCount channelCount) { + return FramePos(static_cast(samplePos) / channelCount); + } + + static constexpr FramePos fromSamplePos(double samplePos, + const mixxx::audio::SignalInfo& signalInfo) { + return FramePos(static_cast(samplePos) / signalInfo.getChannelCount()); } /// Return an engine sample position. The `FramePos` is expected to be @@ -44,7 +54,7 @@ class FramePos final { /// values), use `FramePos::toEngineSamplePosMaybeInvalid` instead. double toEngineSamplePos() const { DEBUG_ASSERT(isValid()); - double engineSamplePos = value() * mixxx::kEngineChannelCount; + double engineSamplePos = value() * mixxx::kEngineChannelOutputCount; // 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 @@ -55,6 +65,10 @@ class FramePos final { return engineSamplePos; } + double toSamplePos(mixxx::audio::ChannelCount channelCount) const { + DEBUG_ASSERT(isValid()); + return value() * channelCount; + } /// Return a `FramePos` from a given engine sample position. Sample /// positions that equal `kLegacyInvalidEnginePosition` are considered /// invalid and result in an invalid `FramePos` instead. @@ -70,6 +84,14 @@ class FramePos final { return fromEngineSamplePos(engineSamplePos); } + static constexpr FramePos fromSamplePosMaybeInvalid( + double samplePos, mixxx::audio::ChannelCount channelCount) { + if (samplePos == kLegacyInvalidEnginePosition) { + return {}; + } + return fromSamplePos(samplePos, channelCount); + } + /// Return an engine sample position. If the `FramePos` is invalid, /// `kLegacyInvalidEnginePosition` is returned instead. /// @@ -84,6 +106,13 @@ class FramePos final { return toEngineSamplePos(); } + double toSamplePosMaybeInvalid(mixxx::audio::ChannelCount channelCount) const { + if (!isValid()) { + return kLegacyInvalidEnginePosition; + } + return toSamplePos(channelCount); + } + /// Return true if the frame position is valid. Any finite value is /// considered valid, i.e. any value except NaN and negative/positive /// infinity. diff --git a/src/audio/types.h b/src/audio/types.h index 2e4254afe96..c3c2de58d1e 100644 --- a/src/audio/types.h +++ b/src/audio/types.h @@ -80,6 +80,14 @@ class ChannelCount { return ChannelCount(valueFromInt(value)); } + static ChannelCount fromDouble(double value) { + const auto channelCount = ChannelCount(static_cast(value)); + // The channel count should always be an integer value + // and this conversion is supposed to be lossless. + DEBUG_ASSERT(channelCount.toDouble() == value); + return channelCount; + } + static constexpr ChannelCount mono() { return ChannelCount(static_cast(1)); } @@ -88,6 +96,10 @@ class ChannelCount { return ChannelCount(static_cast(2)); } + static constexpr ChannelCount stem() { + return ChannelCount(static_cast(8)); // 4 stereo channels + } + explicit constexpr ChannelCount( value_t value = kValueDefault) : m_value(value) { @@ -115,6 +127,11 @@ class ChannelCount { return value(); } + // Helper cast for COs + constexpr double toDouble() const { + return static_cast(value()); + } + private: value_t m_value; }; diff --git a/src/engine/bufferscalers/enginebufferscale.cpp b/src/engine/bufferscalers/enginebufferscale.cpp index ee9ef873ab4..7427cf05b7c 100644 --- a/src/engine/bufferscalers/enginebufferscale.cpp +++ b/src/engine/bufferscalers/enginebufferscale.cpp @@ -2,26 +2,37 @@ #include "engine/engine.h" #include "moc_enginebufferscale.cpp" +#include "soundio/soundmanagerconfig.h" EngineBufferScale::EngineBufferScale() - : m_outputSignal( + : m_signal( mixxx::audio::SignalInfo( - mixxx::kEngineChannelCount, + mixxx::kEngineChannelOutputCount, mixxx::audio::SampleRate())), m_dBaseRate(1.0), m_bSpeedAffectsPitch(false), m_dTempoRatio(1.0), m_dPitchRatio(1.0), m_effectiveRate(1.0) { - DEBUG_ASSERT(!m_outputSignal.isValid()); + DEBUG_ASSERT(!m_signal.isValid()); } -void EngineBufferScale::setSampleRate( - mixxx::audio::SampleRate sampleRate) { +void EngineBufferScale::setSignal( + mixxx::audio::SampleRate sampleRate, + mixxx::audio::ChannelCount channelCount) { DEBUG_ASSERT(sampleRate.isValid()); - if (sampleRate != m_outputSignal.getSampleRate()) { - m_outputSignal.setSampleRate(sampleRate); - onSampleRateChanged(); + DEBUG_ASSERT(channelCount.isValid()); + bool changed = false; + if (sampleRate != m_signal.getSampleRate()) { + m_signal.setSampleRate(sampleRate); + changed = true; } - DEBUG_ASSERT(m_outputSignal.isValid()); + if (channelCount != m_signal.getChannelCount()) { + m_signal.setChannelCount(channelCount); + changed = true; + } + if (changed) { + onSignalChanged(); + } + DEBUG_ASSERT(m_signal.isValid()); } diff --git a/src/engine/bufferscalers/enginebufferscale.h b/src/engine/bufferscalers/enginebufferscale.h index 5480f83312a..b00f5b14d40 100644 --- a/src/engine/bufferscalers/enginebufferscale.h +++ b/src/engine/bufferscalers/enginebufferscale.h @@ -42,12 +42,13 @@ 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 setSignal( + mixxx::audio::SampleRate sampleRate, + mixxx::audio::ChannelCount channelCout); const mixxx::audio::SignalInfo& getOutputSignal() const { - return m_outputSignal; + return m_signal; } // Called from EngineBuffer when seeking, to ensure the buffers are flushed */ @@ -64,9 +65,9 @@ class EngineBufferScale : public QObject { SINT iOutputBufferSize) = 0; private: - mixxx::audio::SignalInfo m_outputSignal; + mixxx::audio::SignalInfo m_signal; - virtual void onSampleRateChanged() = 0; + virtual void onSignalChanged() = 0; protected: double m_dBaseRate; diff --git a/src/engine/bufferscalers/enginebufferscalelinear.cpp b/src/engine/bufferscalers/enginebufferscalelinear.cpp index a461dd21cb6..f8cb001d447 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; + onSignalChanged(); SampleUtil::clear(m_bufferInt, kiLinearScaleReadAheadLength); } @@ -26,6 +25,12 @@ EngineBufferScaleLinear::~EngineBufferScaleLinear() { SampleUtil::free(m_bufferInt); } +void EngineBufferScaleLinear::onSignalChanged() { + m_floorSampleOld = mixxx::SampleBuffer(getOutputSignal().getChannelCount()); + m_floorSample = mixxx::SampleBuffer(getOutputSignal().getChannelCount()); + m_ceilSample = mixxx::SampleBuffer(getOutputSignal().getChannelCount()); +} + void EngineBufferScaleLinear::setScaleParameters(double base_rate, double* pTempoRatio, double* pPitchRatio) { @@ -40,8 +45,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; + onSignalChanged(); } // laurent de soras - punked from musicdsp.org (mad props) @@ -85,9 +89,9 @@ 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) { + SampleUtil::copy(m_floorSampleOld.data(), &m_bufferInt[iNextSample], chCount); } // if the buffer has extra samples, do a read so RAMAN ends up back where @@ -103,7 +107,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 +149,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 +174,9 @@ 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) { + SampleUtil::copy(m_floorSampleOld.data(), &buf[read_samples - chCount], chCount); } return read_samples; } @@ -219,13 +225,9 @@ 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]; - - floor_sample[0] = 0; - floor_sample[1] = 0; - ceil_sample[0] = 0; - ceil_sample[1] = 0; + int chCount = getOutputSignal().getChannelCount(); + m_floorSample.clear(); + m_ceilSample.clear(); double startFrame = m_dNextFrame; SINT i = 0; @@ -248,27 +250,23 @@ 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) { - // 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]; + SampleUtil::copy(m_floorSample.data(), m_floorSampleOld.data(), chCount); + SampleUtil::copy(m_ceilSample.data(), m_bufferInt, chCount); + } else if (sampleCount + 2 * chCount - 1 < m_bufferIntSize) { + // take floorSample form the buffer of the previous run + SampleUtil::copy(m_floorSample.data(), &m_bufferInt[sampleCount], chCount); + SampleUtil::copy(m_ceilSample.data(), &m_bufferInt[sampleCount + chCount], chCount); } else { - // if we don't have the ceil_sample in buffer, load some more + // if we don't have the ceilSample in buffer, load some more - if (getOutputSignal().frames2samples(currentFrameFloor) + 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]; + if (sampleCount + chCount - 1 < m_bufferIntSize) { + // take floorSample form the buffer of the previous run + SampleUtil::copy(m_floorSample.data(), &m_bufferInt[sampleCount], chCount); } do { @@ -285,7 +283,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 +297,16 @@ 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]; + SampleUtil::copy(m_floorSample.data(), &m_bufferInt[sampleCount], chCount); } - ceil_sample[0] = m_bufferInt[getOutputSignal().frames2samples(currentFrameFloor) + 2]; - ceil_sample[1] = m_bufferInt[getOutputSignal().frames2samples(currentFrameFloor) + 3]; + SampleUtil::copy(m_ceilSample.data(), &m_bufferInt[sampleCount + chCount], chCount); } // For the current index, what percentage is it @@ -315,11 +314,12 @@ 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 chIdx = 0; chIdx < chCount; chIdx++) { + buf[i + chIdx] = m_floorSample[chIdx] + + frac * (m_ceilSample[chIdx] - m_floorSample[chIdx]); + } - m_floorSampleOld[0] = floor_sample[0]; - m_floorSampleOld[1] = floor_sample[1]; + m_floorSampleOld.swap(m_floorSample); // increment the index for the next loop m_dNextFrame = m_dCurrentFrame + rate_add; @@ -328,7 +328,7 @@ double EngineBufferScaleLinear::do_scale(CSAMPLE* buf, SINT buf_size) { // samples. This prevents the change from being discontinuous and helps // improve sound quality. rate_add += rate_delta_abs; - i += getOutputSignal().getChannelCount(); + i += chCount; } SampleUtil::clear(&buf[i], buf_size - i); diff --git a/src/engine/bufferscalers/enginebufferscalelinear.h b/src/engine/bufferscalers/enginebufferscalelinear.h index 362bad58898..fcc81cbd2b2 100644 --- a/src/engine/bufferscalers/enginebufferscalelinear.h +++ b/src/engine/bufferscalers/enginebufferscalelinear.h @@ -1,6 +1,7 @@ #pragma once #include "engine/bufferscalers/enginebufferscale.h" +#include "util/samplebuffer.h" class ReadAheadManager; @@ -24,7 +25,7 @@ class EngineBufferScaleLinear : public EngineBufferScale { double* pPitchRatio) override; private: - void onSampleRateChanged() override {} + void onSignalChanged() override; double do_scale(CSAMPLE* buf, SINT buf_size); SINT do_copy(CSAMPLE* buf, SINT buf_size); @@ -36,7 +37,9 @@ class EngineBufferScaleLinear : public EngineBufferScale { CSAMPLE* m_bufferInt; SINT m_bufferIntSize; - CSAMPLE m_floorSampleOld[2]; + mixxx::SampleBuffer m_floorSampleOld; + mixxx::SampleBuffer m_floorSample; + mixxx::SampleBuffer m_ceilSample; bool m_bClear; double m_dRate; diff --git a/src/engine/bufferscalers/enginebufferscalerubberband.cpp b/src/engine/bufferscalers/enginebufferscalerubberband.cpp index ef8c758df04..fc84498a937 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" @@ -18,14 +19,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(); + onSignalChanged(); } void EngineBufferScaleRubberBand::setScaleParameters(double base_rate, @@ -92,7 +93,7 @@ void EngineBufferScaleRubberBand::setScaleParameters(double base_rate, m_dPitchRatio = *pPitchRatio; } -void EngineBufferScaleRubberBand::onSampleRateChanged() { +void EngineBufferScaleRubberBand::onSignalChanged() { // TODO: Resetting the sample rate will cause internal // memory allocations that may block the real-time thread. // When is this function actually invoked?? @@ -100,6 +101,26 @@ void EngineBufferScaleRubberBand::onSampleRateChanged() { if (!getOutputSignal().isValid()) { 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_rubberBand.clear(); + + for (int chIdx = 0; chIdx < channelCount; chIdx++) { + if (m_buffers[chIdx].size() == MAX_BUFFER_LEN) { + continue; + } + m_buffers[chIdx] = mixxx::SampleBuffer(MAX_BUFFER_LEN); + m_bufferPtrs[chIdx] = m_buffers[chIdx].data(); + } + RubberBandStretcher::Options rubberbandOptions = RubberBandStretcher::OptionProcessRealTime; #if RUBBERBANDV3 @@ -159,10 +180,46 @@ 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(); + // The buffers samples are ordered as following + // m_buffers#1 = 11.. + // m_buffers#2 = 22.. + // m_buffers#3 = 33.. + // m_buffers#4 = 44.. + // m_buffers#X = XX.. + // And need to be reordered as following in pBuffer + // 1234..X1234...X... + // + // Because of the unanticipated number of buffer and channel, we cannot + // use any SampleUtil in this case + for (SINT frameIdx = 0; frameIdx < frames; ++frameIdx) { + for (int channel = 0; channel < chCount; channel++) { + pBuffer[frameIdx * chCount + channel] = m_buffers[channel].data()[frameIdx]; + } + } + } break; + } return received_frames; } @@ -175,11 +232,48 @@ void EngineBufferScaleRubberBand::deinterleaveAndProcess( } 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(); + // The sampler are ordered as following in pBuffer + // 1234..X1234...X... + // And need to be reordered as following + // m_buffers#1 = 11.. + // m_buffers#2 = 22.. + // m_buffers#3 = 33.. + // m_buffers#4 = 44.. + // m_buffers#X = XX.. + // + // Because of the unanticipated number of buffer and channel, we cannot + // use any SampleUtil in this case + for (SINT frameIdx = 0; frameIdx < frames; ++frameIdx) { + for (int channel = 0; channel < chCount; channel++) { + m_buffers[channel].data()[frameIdx] = + pBuffer[frameIdx * chCount + channel]; + } + } + } break; + } { ScopedTimer t(QStringLiteral("RubberBand::process")); @@ -231,7 +325,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) { @@ -278,7 +373,7 @@ bool EngineBufferScaleRubberBand::isEngineFinerAvailable() { void EngineBufferScaleRubberBand::useEngineFiner(bool enable) { if (isEngineFinerAvailable()) { m_useEngineFiner = enable; - onSampleRateChanged(); + onSignalChanged(); } } @@ -307,8 +402,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) { + buffer.clear(); + } while (remaining_padding > 0) { const size_t pad_samples = std::min(remaining_padding, block_size); { diff --git a/src/engine/bufferscalers/enginebufferscalerubberband.h b/src/engine/bufferscalers/enginebufferscalerubberband.h index 06c6b67d562..d1330d96f6e 100644 --- a/src/engine/bufferscalers/enginebufferscalerubberband.h +++ b/src/engine/bufferscalers/enginebufferscalerubberband.h @@ -43,7 +43,7 @@ class EngineBufferScaleRubberBand final : public EngineBufferScale { private: // Reset RubberBand library with new audio signal - void onSampleRateChanged() override; + void onSignalChanged() override; /// Calls `m_pRubberBand->getPreferredStartPad()`, with backwards /// compatibility for older librubberband versions. @@ -68,10 +68,10 @@ class EngineBufferScaleRubberBand final : public EngineBufferScale { /// The audio buffers samples used to send audio to Rubber Band and to /// receive processed audio from Rubber Band. This is needed because Mixxx /// uses interleaved buffers in most other places. - std::array m_buffers; + std::vector m_buffers; /// These point to the buffers in `m_buffers`. They can be defined here /// since this object cannot be moved or copied. - std::array m_bufferPtrs; + std::vector m_bufferPtrs; /// Contains interleaved samples read from `m_pReadAheadManager`. These need /// to be deinterleaved before they can be passed to Rubber Band. diff --git a/src/engine/bufferscalers/enginebufferscalest.cpp b/src/engine/bufferscalers/enginebufferscalest.cpp index 1d46edbf173..7f57247575f 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(); + onSignalChanged(); } 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::onSignalChanged() { + 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 eb751559238..b1be8e6b19e 100644 --- a/src/engine/bufferscalers/enginebufferscalest.h +++ b/src/engine/bufferscalers/enginebufferscalest.h @@ -32,7 +32,7 @@ class EngineBufferScaleST : public EngineBufferScale { void clear() override; private: - void onSampleRateChanged() override; + void onSignalChanged() override; // The read-ahead manager that we use to fetch samples ReadAheadManager* m_pReadAheadManager; diff --git a/src/engine/bufferscalers/rubberbandworkerpool.cpp b/src/engine/bufferscalers/rubberbandworkerpool.cpp index 3fec48ba528..35dda488be4 100644 --- a/src/engine/bufferscalers/rubberbandworkerpool.cpp +++ b/src/engine/bufferscalers/rubberbandworkerpool.cpp @@ -14,10 +14,17 @@ RubberBandWorkerPool::RubberBandWorkerPool(UserSettingsPointer pConfig) m_channelPerWorker = multiThreadedOnStereo ? mixxx::audio::ChannelCount::mono() : mixxx::audio::ChannelCount::stereo(); - DEBUG_ASSERT(mixxx::kEngineChannelCount % m_channelPerWorker == 0); + DEBUG_ASSERT(mixxx::kMaxEngineChannelInputCount % m_channelPerWorker == 0); + + int numCore = QThread::idealThreadCount(); + int numRBTasks = qMin(numCore, mixxx::kMaxEngineChannelInputCount / m_channelPerWorker); + + qDebug() << "RubberBand will use" << numRBTasks << "tasks to scale the audio signal"; setThreadPriority(QThread::HighPriority); - setMaxThreadCount(mixxx::kEngineChannelCount / m_channelPerWorker - 1); + // The RB pool will only be used to scale n-1 buffer sample, so the engine + // thread takes care of the last buffer and doesn't have to be idle. + setMaxThreadCount(numRBTasks - 1); // We allocate one runner less than the total of maximum supported channel, // so the engine thread will also perform a stretching operation, instead of diff --git a/src/engine/bufferscalers/rubberbandwrapper.cpp b/src/engine/bufferscalers/rubberbandwrapper.cpp index ed7f7bc562f..0f69d3ca487 100644 --- a/src/engine/bufferscalers/rubberbandwrapper.cpp +++ b/src/engine/bufferscalers/rubberbandwrapper.cpp @@ -11,9 +11,59 @@ using RubberBand::RubberBandStretcher; namespace { -mixxx::audio::ChannelCount getChannelPerWorker() { +/// The function is used to compute the best number of channel per RB task, +/// depending of the number of channels and available worker. This allows +/// hardware if will less than 8 core to adjust the task distribution in the +/// most optimum way. +/// +/// The following table provide the expected number of channel per task with +/// stereo processing for a given number of CPU core (the default behaviour) +/// +/// | NbOfCore | Stereo | Stem | +/// |----------|--------|------| +/// | 1 | 2 | 8 | +/// | 2 | 2 | 4 | +/// | 3 | 2 | 2 | +/// | 4 | 2 | 2 | +/// +/// The following table provide the expected number of channel per task when the +/// user has explicitly requested stereo channel to be processed as mono +/// channels for a given number of CPU core. +/// +/// | NbOfCore | Stereo | Stem | +/// |----------|--------|------| +/// | 1 | 2 | 8 | +/// | 2 | 1 | 4 | +/// | 3 | 1 | 2 | +/// | 4 | 1 | 2 | +/// | 5 | 1 | 2 | +/// | 6 | 1 | 2 | +/// | 7 | 1 | 2 | +/// | 8 | 1 | 1 | + +mixxx::audio::ChannelCount getChannelPerWorker(mixxx::audio::ChannelCount chCount) { RubberBandWorkerPool* pPool = RubberBandWorkerPool::instance(); - return pPool ? pPool->channelPerWorker() : mixxx::kEngineChannelCount; + + // There should always be a pool set, even if multi threading isn't enabled. + // This is because multi threading will always be used for stem when + // possible. + VERIFY_OR_DEBUG_ASSERT(pPool) { + return mixxx::kMaxEngineChannelInputCount; + } + auto channelPerWorker = pPool->channelPerWorker(); + // The task count includes all the thread in the pool + the engine thread + auto maxThreadCount = pPool->maxThreadCount() + 1; + VERIFY_OR_DEBUG_ASSERT(chCount % channelPerWorker == 0) { + return mixxx::kEngineChannelOutputCount; + } + auto numTasks = chCount / channelPerWorker; + if (numTasks > maxThreadCount) { + VERIFY_OR_DEBUG_ASSERT(numTasks % maxThreadCount == 0) { + return mixxx::kEngineChannelOutputCount; + } + return mixxx::audio::ChannelCount(chCount / maxThreadCount); + } + return channelPerWorker; } } // namespace @@ -65,13 +115,13 @@ size_t RubberBandWrapper::retrieve( // interruption will still create undesirable audio artefacts. VERIFY_OR_DEBUG_ASSERT(numSamplesRetrieved == samples) { if (samples > numSamplesRetrieved) { - for (int ch = 0; ch < getChannelPerWorker(); ch++) { + for (int ch = 0; ch < m_channelPerWorker; ch++) { SampleUtil::clear(output[ch] + numSamplesRetrieved, samples - numSamplesRetrieved); } } } - output += getChannelPerWorker(); + output += m_channelPerWorker; } return samples; } @@ -139,7 +189,7 @@ void RubberBandWrapper::process(const float* const* input, size_t samples, bool // Otherwise, it means the main thread should take care of the stretching pInstance->run(); } - input += pPool->channelPerWorker(); + input += m_channelPerWorker; } // We always perform a wait, even for task that were ran in the main // thread, so it resets the semaphore @@ -164,24 +214,24 @@ void RubberBandWrapper::setup(mixxx::audio::SampleRate sampleRate, m_pInstances.clear(); }; - auto channelPerWorker = getChannelPerWorker(); - qDebug() << "RubberBandWrapper::setup" << channelPerWorker; - VERIFY_OR_DEBUG_ASSERT(0 == chCount % channelPerWorker) { + m_channelPerWorker = getChannelPerWorker(chCount); + qDebug() << "RubberBandWrapper::setup - using" << m_channelPerWorker << "channel(s) per task"; + VERIFY_OR_DEBUG_ASSERT(0 == chCount % m_channelPerWorker) { // If we have an uneven number of channel, which we can't evenly // distribute across the RubberBandPool workers, we fallback to using a - // single instance to limit the audio impefection that may come from - // using RB withg different parameters. + // single instance to limit the audio imperfection that may come from + // using RB with different parameters. m_pInstances.emplace_back( std::make_unique( sampleRate, chCount, opt)); return; } - m_pInstances.reserve(chCount / channelPerWorker); - for (int c = 0; c < chCount; c += channelPerWorker) { + m_pInstances.reserve(chCount / m_channelPerWorker); + for (int c = 0; c < chCount; c += m_channelPerWorker) { m_pInstances.emplace_back( std::make_unique( - sampleRate, channelPerWorker, opt)); + sampleRate, m_channelPerWorker, opt)); } } void RubberBandWrapper::setPitchScale(double scale) { diff --git a/src/engine/bufferscalers/rubberbandwrapper.h b/src/engine/bufferscalers/rubberbandwrapper.h index 610c1d2caa3..97531fd7373 100644 --- a/src/engine/bufferscalers/rubberbandwrapper.h +++ b/src/engine/bufferscalers/rubberbandwrapper.h @@ -32,4 +32,7 @@ class RubberBandWrapper { private: // copy constructor of RubberBand::RubberBandStretcher is implicitly deleted. std::vector> m_pInstances; + // Number of channel used for each instance. This may vary whether the track + // is a stereo track or a stem track + mixxx::audio::ChannelCount m_channelPerWorker; }; diff --git a/src/engine/cachingreader/cachingreader.cpp b/src/engine/cachingreader/cachingreader.cpp index 45a83cf525a..d2b868fa353 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 // @@ -38,7 +38,8 @@ constexpr SINT kNumberOfCachedChunksInMemory = 80; } // anonymous namespace CachingReader::CachingReader(const QString& group, - UserSettingsPointer config) + UserSettingsPointer config, + mixxx::audio::ChannelCount maxSupportedChannel) : 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 @@ -57,8 +58,12 @@ CachingReader::CachingReader(const QString& group, m_state(STATE_IDLE), m_mruCachingReaderChunk(nullptr), m_lruCachingReaderChunk(nullptr), - m_sampleBuffer(CachingReaderChunk::kSamples * kNumberOfCachedChunksInMemory), - m_worker(group, &m_chunkReadRequestFIFO, &m_readerStatusUpdateFIFO) { + m_sampleBuffer(CachingReaderChunk::kFrames * maxSupportedChannel * + kNumberOfCachedChunksInMemory), + m_worker(group, + &m_chunkReadRequestFIFO, + &m_readerStatusUpdateFIFO, + maxSupportedChannel) { m_allocatedCachingReaderChunks.reserve(kNumberOfCachedChunksInMemory); // Divide up the allocated raw memory buffer into total_chunks // chunks. Initialize each chunk to hold nothing and add it to the free @@ -68,8 +73,8 @@ CachingReader::CachingReader(const QString& group, new CachingReaderChunkForOwner( mixxx::SampleBuffer::WritableSlice( m_sampleBuffer, - CachingReaderChunk::kSamples * i, - CachingReaderChunk::kSamples)); + CachingReaderChunk::kFrames * maxSupportedChannel * i, + CachingReaderChunk::kFrames * maxSupportedChannel)); m_chunks.push_back(c); m_freeChunks.push_back(c); } @@ -287,17 +292,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 +353,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 +379,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 +446,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 +494,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 +507,8 @@ CachingReader::ReadResult CachingReader::read(SINT startSample, SINT numSamples, remainingFrameIndexRange.shrinkFront(paddingFrameIndexRange.length()); result = ReadResult::PARTIALLY_AVAILABLE; } - const SINT chunkSamples = - CachingReaderChunk::frames2samples(bufferedFrameIndexRange.length()); + const SINT chunkSamples = CachingReaderChunk::frames2samples( + bufferedFrameIndexRange.length(), channelCount); DEBUG_ASSERT(chunkSamples > 0); if (!reverse) { buffer += chunkSamples; diff --git a/src/engine/cachingreader/cachingreader.h b/src/engine/cachingreader/cachingreader.h index 6d18841fd77..384f12a8047 100644 --- a/src/engine/cachingreader/cachingreader.h +++ b/src/engine/cachingreader/cachingreader.h @@ -82,7 +82,8 @@ class CachingReader : public QObject { public: // Construct a CachingReader with the given group. CachingReader(const QString& group, - UserSettingsPointer _config); + UserSettingsPointer _config, + mixxx::audio::ChannelCount maxSupportedChannel); ~CachingReader() override; void process(); @@ -100,7 +101,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,7 +127,8 @@ class CachingReader : public QObject { void trackLoading(); void trackLoaded(TrackPointer pTrack, mixxx::audio::SampleRate trackSampleRate, - double trackNumSamples); + mixxx::audio::ChannelCount trackChannelCount, + mixxx::audio::FramePos trackNumFrame); void trackLoadFailed(TrackPointer pTrack, const QString& reason); private: diff --git a/src/engine/cachingreader/cachingreaderchunk.cpp b/src/engine/cachingreader/cachingreaderchunk.cpp index 1217dc00f47..3c946ed134d 100644 --- a/src/engine/cachingreader/cachingreaderchunk.cpp +++ b/src/engine/cachingreader/cachingreaderchunk.cpp @@ -20,7 +20,6 @@ CachingReaderChunk::CachingReaderChunk( mixxx::SampleBuffer::WritableSlice sampleBuffer) : m_index(kInvalidChunkIndex), m_sampleBuffer(std::move(sampleBuffer)) { - DEBUG_ASSERT(m_sampleBuffer.length() == kSamples); } void CachingReaderChunk::init(SINT index) { @@ -49,17 +48,30 @@ 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 + // odd 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 { + 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 +79,20 @@ mixxx::IndexRange CachingReaderChunk::bufferSampleFrames( mixxx::IndexRange CachingReaderChunk::readBufferedSampleFrames( CSAMPLE* sampleBuffer, + mixxx::audio::ChannelCount channelCount, const mixxx::IndexRange& frameIndexRange) const { DEBUG_ASSERT(m_index != kInvalidChunkIndex); const auto copyableFrameIndexRange = intersect(frameIndexRange, m_bufferedSampleFrames.frameIndexRange()); if (!copyableFrameIndexRange.empty()) { - const SINT dstSampleOffset = - frames2samples(copyableFrameIndexRange.start() - frameIndexRange.start()); - const SINT srcSampleOffset = - frames2samples(copyableFrameIndexRange.start() - m_bufferedSampleFrames.frameIndexRange().start()); - const SINT sampleCount = frames2samples(copyableFrameIndexRange.length()); + const SINT dstSampleOffset = frames2samples( + copyableFrameIndexRange.start() - frameIndexRange.start(), + channelCount); + const SINT srcSampleOffset = frames2samples( + copyableFrameIndexRange.start() - + m_bufferedSampleFrames.frameIndexRange().start(), + channelCount); + const SINT sampleCount = frames2samples(copyableFrameIndexRange.length(), channelCount); SampleUtil::copy( sampleBuffer + dstSampleOffset, m_bufferedSampleFrames.readableData(srcSampleOffset), @@ -87,20 +103,26 @@ mixxx::IndexRange CachingReaderChunk::readBufferedSampleFrames( mixxx::IndexRange CachingReaderChunk::readBufferedSampleFramesReverse( CSAMPLE* reverseSampleBuffer, + mixxx::audio::ChannelCount channelCount, const mixxx::IndexRange& frameIndexRange) const { DEBUG_ASSERT(m_index != kInvalidChunkIndex); const auto copyableFrameIndexRange = intersect(frameIndexRange, m_bufferedSampleFrames.frameIndexRange()); if (!copyableFrameIndexRange.empty()) { - const SINT dstSampleOffset = - frames2samples(copyableFrameIndexRange.start() - frameIndexRange.start()); - const SINT srcSampleOffset = - frames2samples(copyableFrameIndexRange.start() - m_bufferedSampleFrames.frameIndexRange().start()); - const SINT sampleCount = frames2samples(copyableFrameIndexRange.length()); + const SINT dstSampleOffset = frames2samples( + copyableFrameIndexRange.start() - frameIndexRange.start(), + channelCount); + const SINT srcSampleOffset = frames2samples( + copyableFrameIndexRange.start() - + m_bufferedSampleFrames.frameIndexRange().start(), + channelCount); + const SINT sampleCount = frames2samples(copyableFrameIndexRange.length(), channelCount); + SampleUtil::copyReverse( reverseSampleBuffer - dstSampleOffset - sampleCount, m_bufferedSampleFrames.readableData(srcSampleOffset), - sampleCount); + sampleCount, + channelCount); } return copyableFrameIndexRange; } diff --git a/src/engine/cachingreader/cachingreaderchunk.h b/src/engine/cachingreader/cachingreaderchunk.h index c8c04e0b1b3..607da309d17 100644 --- a/src/engine/cachingreader/cachingreaderchunk.h +++ b/src/engine/cachingreader/cachingreaderchunk.h @@ -23,22 +23,18 @@ 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 SINT kFrames = 8192; // ~ 170 ms at 48 kHz - static constexpr SINT kSamples = kFrames * kChannels; // Converts frames to samples - static constexpr SINT frames2samples(SINT frames) noexcept { - return frames * kChannels; - } - static constexpr double dFrames2samples(SINT frames) noexcept { - return static_cast(frames) * kChannels; + static constexpr SINT frames2samples( + SINT frames, mixxx::audio::ChannelCount channelCount) noexcept { + return 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 +63,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 +96,22 @@ class CachingReaderChunk { // the worker thread is in control. class CachingReaderChunkForOwner: public CachingReaderChunk { public: - explicit CachingReaderChunkForOwner( - mixxx::SampleBuffer::WritableSlice sampleBuffer); - ~CachingReaderChunkForOwner() override = default; + explicit CachingReaderChunkForOwner( + mixxx::SampleBuffer::WritableSlice sampleBuffer); + ~CachingReaderChunkForOwner() override = default; - void init(SINT index); - void free(); + void init(SINT index); + void free(); - enum State { - FREE, - READY, - READ_PENDING - }; + enum State { + FREE, + READY, + READ_PENDING + }; - State getState() const noexcept { + State getState() const noexcept { return m_state; - } + } // The state is controlled by the cache as the owner of each chunk! void giveToWorker() { diff --git a/src/engine/cachingreader/cachingreaderworker.cpp b/src/engine/cachingreader/cachingreaderworker.cpp index 872459c09ca..cb4d12488e4 100644 --- a/src/engine/cachingreader/cachingreaderworker.cpp +++ b/src/engine/cachingreader/cachingreaderworker.cpp @@ -25,11 +25,13 @@ constexpr SINT kNumSoundFrameToVerify = 2; CachingReaderWorker::CachingReaderWorker( const QString& group, FIFO* pChunkReadRequestFIFO, - FIFO* pReaderStatusFIFO) + FIFO* pReaderStatusFIFO, + mixxx::audio::ChannelCount maxSupportedChannel) : m_group(group), m_tag(QString("CachingReaderWorker %1").arg(m_group)), m_pChunkReadRequestFIFO(pChunkReadRequestFIFO), - m_pReaderStatusFIFO(pReaderStatusFIFO) { + m_pReaderStatusFIFO(pReaderStatusFIFO), + m_maxSupportedChannel(maxSupportedChannel) { } ReaderStatusUpdate CachingReaderWorker::processReadRequest( @@ -82,7 +84,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 +189,7 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { } mixxx::AudioSource::OpenParams config; - config.setChannelCount(CachingReaderChunk::kChannels); + config.setChannelCount(m_maxSupportedChannel); m_pAudioSource = SoundSourceProxy(pTrack).openAudioSource(config); if (!m_pAudioSource) { kLogger.warning() @@ -202,6 +204,25 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { return; } + // It is critical that the audio source doesn't contain more channels than + // requested as this could lead to overflow when reading chunks + VERIFY_OR_DEBUG_ASSERT(m_pAudioSource->getSignalInfo().getChannelCount() >= + mixxx::audio::ChannelCount::mono() && + m_pAudioSource->getSignalInfo().getChannelCount() <= + m_maxSupportedChannel) { + m_pAudioSource.reset(); // Close open file handles + const auto update = ReaderStatusUpdate::trackUnloaded(); + m_pReaderStatusFIFO->writeBlocking(&update, 1); + emit trackLoadFailed(pTrack, + tr("The file '%1' could not be loaded because it contains %2 " + "channels, and only 1 to %3 are supported.") + .arg(QDir::toNativeSeparators(pTrack->getLocation()), + QString::number(m_pAudioSource->getSignalInfo() + .getChannelCount()), + QString::number(m_maxSupportedChannel))); + return; + } + // Initially assume that the complete content offered by audio source // is available for reading. Later if read errors occur this value will // be decreased to avoid repeated reading of corrupt audio data. @@ -233,9 +254,6 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { m_pReaderStatusFIFO->writeBlocking(&update, 1); // Emit that the track is loaded. - const double sampleCount = - CachingReaderChunk::dFrames2samples( - m_pAudioSource->frameLength()); // This code is a workaround until we have found a better solution to // verify and correct offsets. @@ -252,7 +270,8 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { emit trackLoaded( pTrack, m_pAudioSource->getSignalInfo().getSampleRate(), - sampleCount); + m_pAudioSource->getSignalInfo().getChannelCount(), + mixxx::audio::FramePos(m_pAudioSource->frameLength())); } void CachingReaderWorker::quitWait() { @@ -261,7 +280,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 +291,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 309edbd1d05..a99bbb150a6 100644 --- a/src/engine/cachingreader/cachingreaderworker.h +++ b/src/engine/cachingreader/cachingreaderworker.h @@ -99,7 +99,8 @@ class CachingReaderWorker : public EngineWorker { // Construct a CachingReader with the given group. CachingReaderWorker(const QString& group, FIFO* pChunkReadRequestFIFO, - FIFO* pReaderStatusFIFO); + FIFO* pReaderStatusFIFO, + mixxx::audio::ChannelCount maxSupportedChannel); ~CachingReaderWorker() override = default; // Request to load a new track. wake() must be called afterwards. @@ -114,7 +115,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, + mixxx::audio::FramePos numFrame); void trackLoadFailed(TrackPointer pTrack, const QString& reason); private: @@ -148,7 +152,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; @@ -159,5 +164,8 @@ class CachingReaderWorker : public EngineWorker { // before conversion to a stereo signal. mixxx::SampleBuffer m_tempReadBuffer; + // The maximum number of channel that this reader can support + mixxx::audio::ChannelCount m_maxSupportedChannel; + QAtomicInt m_stop; }; diff --git a/src/engine/channels/enginedeck.cpp b/src/engine/channels/enginedeck.cpp index 65e38b5ee60..7b04c7db263 100644 --- a/src/engine/channels/enginedeck.cpp +++ b/src/engine/channels/enginedeck.cpp @@ -35,7 +35,12 @@ EngineDeck::EngineDeck( Qt::DirectConnection); m_pPregain = new EnginePregain(getGroup()); - m_pBuffer = new EngineBuffer(getGroup(), pConfig, this, pMixingEngine); + m_pBuffer = new EngineBuffer(getGroup(), + pConfig, + this, + pMixingEngine, + primaryDeck ? mixxx::audio::ChannelCount::stem() + : mixxx::audio::ChannelCount::stereo()); } EngineDeck::~EngineDeck() { @@ -44,6 +49,29 @@ 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 + + SampleUtil::clear(pOut, iBufferSize); + const CSAMPLE* pIn = m_stemBuffer.data(); + for (int i = 0; i < iBufferSize; i += mixxx::kEngineChannelOutputCount) { + for (int chIdx = 0; chIdx < m_pBuffer->getChannelCount(); + chIdx += mixxx::kEngineChannelOutputCount) { + // TODO(XXX): apply stem gain or skip muted stem + pOut[i] += pIn[stereoChannelCount * i + chIdx]; + pOut[i + 1] += pIn[stereoChannelCount * i + chIdx + 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 ca87feda2ad..670787270b0 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 42c709677cb..e249bb09bdf 100644 --- a/src/engine/controls/bpmcontrol.cpp +++ b/src/engine/controls/bpmcontrol.cpp @@ -324,7 +324,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 3213f58de3f..fdb2cdc0891 100644 --- a/src/engine/controls/loopingcontrol.cpp +++ b/src/engine/controls/loopingcontrol.cpp @@ -540,9 +540,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 0e41ad24fc0..3401b3123c5 100644 --- a/src/engine/controls/loopingcontrol.h +++ b/src/engine/controls/loopingcontrol.h @@ -103,7 +103,7 @@ class LoopingControl : public EngineControl { void trackLoaded(TrackPointer pNewTrack) override; void trackBeatsUpdated(mixxx::BeatsPointer pBeats) override; - double getTrackSamples() const; + mixxx::audio::FramePos getTrackFrame() const; signals: void loopReset(); diff --git a/src/engine/effects/engineeffect.cpp b/src/engine/effects/engineeffect.cpp index 6aff599f7e4..168f164773a 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, @@ -202,14 +202,16 @@ bool EngineEffect::process(const ChannelHandle& inputHandle, SampleUtil::linearCrossfadeBuffersOut( pOutput, pInput, - numSamples); + numSamples, + mixxx::kEngineChannelOutputCount); } 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::linearCrossfadeBuffersOut( pOutput, pInput, - numSamples); + numSamples, + mixxx::kEngineChannelOutputCount); } } } diff --git a/src/engine/effects/engineeffectsdelay.h b/src/engine/effects/engineeffectsdelay.h index 5cdde538e7e..964b01a4fae 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 8022444444b..4df4ab6c270 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 59370924b0c..4bfc4a3cf3a 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 @@ -59,7 +57,8 @@ const QString kAppGroup = QStringLiteral("[App]"); EngineBuffer::EngineBuffer(const QString& group, UserSettingsPointer pConfig, EngineChannel* pChannel, - EngineMixer* pMixingEngine) + EngineMixer* pMixingEngine, + mixxx::audio::ChannelCount maxSupportedChannel) : m_group(group), m_pConfig(pConfig), m_pLoopingControl(nullptr), @@ -91,7 +90,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,9 +101,9 @@ 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); + m_pReader = new CachingReader(group, pConfig, maxSupportedChannel); connect(m_pReader, &CachingReader::trackLoading, this, &EngineBuffer::slotTrackLoading, Qt::DirectConnection); @@ -451,7 +452,7 @@ void EngineBuffer::readToCrossfadeBuffer(const int iBufferSize) { // (Must be called only once per callback) m_pScale->scaleBuffer(m_pCrossfadeBuffer, iBufferSize); // Restore the original position that was lost due to scaleBuffer() above - m_pReadAheadManager->notifySeek(m_playPos); + m_pReadAheadManager->notifySeek(m_playPos.toSamplePos(m_channelCount)); m_bCrossfadeReady = true; } } @@ -470,7 +471,7 @@ void EngineBuffer::setNewPlaypos(mixxx::audio::FramePos position) { // this also sets m_pReadAheadManager to newpos readToCrossfadeBuffer(m_iLastBufferSize); } else { - m_pReadAheadManager->notifySeek(m_playPos); + m_pReadAheadManager->notifySeek(m_playPos.toSamplePos(m_channelCount)); } m_pScale->clear(); @@ -519,16 +520,18 @@ void EngineBuffer::loadFakeTrack(TrackPointer pTrack, bool bPlay) { if (bPlay) { m_playButton->set((double)bPlay); } - slotTrackLoaded( - pTrack, + slotTrackLoaded(pTrack, pTrack->getSampleRate(), - pTrack->getSampleRate() * pTrack->getDuration()); + pTrack->getChannels(), + mixxx::audio::FramePos::fromEngineSamplePos( + pTrack->getSampleRate() * pTrack->getDuration())); } // WARNING: Always called from the EngineWorker thread pool void EngineBuffer::slotTrackLoaded(TrackPointer pTrack, mixxx::audio::SampleRate trackSampleRate, - double trackNumSamples) { + mixxx::audio::ChannelCount trackChannelCount, + mixxx::audio::FramePos trackNumFrame) { if (kLogger.traceEnabled()) { kLogger.trace() << getGroup() << "EngineBuffer::slotTrackLoaded"; } @@ -538,7 +541,24 @@ 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 + VERIFY_OR_DEBUG_ASSERT(m_channelCount % mixxx::audio::ChannelCount::stereo() == 0) { + // Make it stereo for the frame calculation + kLogger.warning() << "Odd number of channel in the track is not supported"; + }; + } 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(trackNumFrame.toEngineSamplePos()); m_pTrackSampleRate->set(trackSampleRate.toDouble()); m_pTrackLoaded->forceSet(1); @@ -880,11 +900,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; + int 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); @@ -1080,8 +1107,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 @@ -1093,7 +1120,7 @@ void EngineBuffer::processTrackLocked( // 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); + pOutput, m_pCrossfadeBuffer, iBufferSize, m_channelCount); } // Note: we do not fade here if we pass the end or the start of // the track in reverse direction @@ -1148,7 +1175,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(); @@ -1167,10 +1194,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->setSignal(m_sampleRate, m_channelCount); + m_pScaleST->setSignal(m_sampleRate, m_channelCount); #ifdef __RUBBERBAND__ - m_pScaleRB->setSampleRate(m_sampleRate); + m_pScaleRB->setSignal(m_sampleRate, m_channelCount); #endif bool hasStableTrack = m_pTrackLoaded->toBool() && m_iTrackLoading.loadAcquire() == 0; @@ -1232,14 +1259,14 @@ void EngineBuffer::processSlip(int iBufferSize) { // Increment slip position even if it was just toggled -- this ensures the position is correct. if (enabled) { // `iBufferSize` originates from `SoundManager::onDeviceOutputCallback` - // and is always a multiple of 2, so we can safely use integer division + // and is always a multiple of channel count, so we can safely use integer division // to find the number of frames per buffer here. // // 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 @@ -1458,7 +1485,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(); @@ -1480,7 +1507,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. @@ -1491,7 +1518,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) { @@ -1556,7 +1583,8 @@ 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()); } void EngineBuffer::setTrackEndPosition(mixxx::audio::FramePos position) { diff --git a/src/engine/enginebuffer.h b/src/engine/enginebuffer.h index 113286187d6..202d64dd5a5 100644 --- a/src/engine/enginebuffer.h +++ b/src/engine/enginebuffer.h @@ -102,7 +102,8 @@ class EngineBuffer : public EngineObject { EngineBuffer(const QString& group, UserSettingsPointer pConfig, EngineChannel* pChannel, - EngineMixer* pMixingEngine); + EngineMixer* pMixingEngine, + mixxx::audio::ChannelCount maxSupportedChannel); virtual ~EngineBuffer(); void bindWorkers(EngineWorkerScheduler* pWorkerScheduler); @@ -110,6 +111,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,7 +243,8 @@ class EngineBuffer : public EngineObject { void slotTrackLoaded( TrackPointer pTrack, mixxx::audio::SampleRate trackSampleRate, - double trackNumSamples); + mixxx::audio::ChannelCount trackChannelCount, + mixxx::audio::FramePos trackNumFrame); void slotTrackLoadFailed(TrackPointer pTrack, const QString& reason); // Fired when passthrough mode is enabled or disabled. @@ -332,7 +337,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 @@ -380,6 +385,7 @@ class EngineBuffer : public EngineObject { SlipModeState m_slipModeState; + // Track samples are always given assuming a stereo track ControlObject* m_pTrackSamples; ControlObject* m_pTrackSampleRate; @@ -457,6 +463,9 @@ class EngineBuffer : public EngineObject { // 0 to guarantee we see a change on the first callback. mixxx::audio::SampleRate m_sampleRate; + // The current channel count of the loaded track + 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 0add163d763..c8054b0c833 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 a5ea629ee2d..ff6f798fd3e 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 f14ff5a0d86..200f1ef630d 100644 --- a/src/engine/filters/enginefilteriir.h +++ b/src/engine/filters/enginefilteriir.h @@ -6,6 +6,7 @@ #define MIXXX #include +#include "engine/engine.h" #include "engine/engineobject.h" #include "util/sample.h" @@ -72,7 +73,8 @@ class EngineFilterIIR : public EngineFilterIIRBase { SampleUtil::linearCrossfadeBuffersOut( pOutput, // fade out filtered pIn, // fade in dry - iBufferSize); + iBufferSize, + mixxx::kEngineChannelOutputCount); } else { SampleUtil::applyRampingGain( pOutput, 1.0, 0, // fade out filtered diff --git a/src/engine/filters/enginefiltermoogladder4.h b/src/engine/filters/enginefiltermoogladder4.h index f9b5b0fbdec..69ecbdd5f1f 100644 --- a/src/engine/filters/enginefiltermoogladder4.h +++ b/src/engine/filters/enginefiltermoogladder4.h @@ -11,6 +11,7 @@ // http://musicdsp.org/showArchiveComment.php?ArchiveID=196 #include "audio/types.h" +#include "engine/engine.h" #include "engine/engineobject.h" #include "util/sample.h" @@ -121,7 +122,8 @@ class EngineFilterMoogLadderBase : public EngineObjectConstIn { SampleUtil::linearCrossfadeBuffersOut( pOutput, // fade out filtered pIn, // fade in dry - iBufferSize); + iBufferSize, + mixxx::kEngineChannelOutputCount); initBuffers(); } diff --git a/src/engine/filters/enginefilterpan.h b/src/engine/filters/enginefilterpan.h index 9f2986faaa6..31e99ef9a6e 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 cbe292ff667..05379355299 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 d26a9bbdc93..86757087e36 100644 --- a/src/engine/readaheadmanager.cpp +++ b/src/engine/readaheadmanager.cpp @@ -32,13 +32,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; @@ -48,11 +50,11 @@ SINT ReadAheadManager::getNextSamples(double dRate, CSAMPLE* pOutput, // can read in one shot. const mixxx::audio::FramePos loopTriggerPosition = m_pLoopingControl->nextTrigger(in_reverse, - mixxx::audio::FramePos::fromEngineSamplePosMaybeInvalid( - m_currentPosition), + mixxx::audio::FramePos::fromSamplePosMaybeInvalid( + m_currentPosition, channelCount), &targetPosition); - const double loop_trigger = loopTriggerPosition.toEngineSamplePosMaybeInvalid(); - const double target = targetPosition.toEngineSamplePosMaybeInvalid(); + const double loop_trigger = loopTriggerPosition.toSamplePosMaybeInvalid(channelCount); + const double target = targetPosition.toSamplePosMaybeInvalid(channelCount); SINT preloop_samples = 0; double samplesToLoopTrigger = 0.0; @@ -68,7 +70,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 +86,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); @@ -150,7 +152,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; @@ -159,7 +161,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().toSamplePos( + channelCount)); if (loop_read_position > trackSamples) { // looping in reverse overlapping post-roll without samples crossFadeStart = loop_read_position - trackSamples; @@ -172,7 +176,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 @@ -185,9 +190,10 @@ 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, + pOutput + SampleUtil::ceilPlayPosToFrameStart(crossFadeStart, channelCount), m_pCrossFadeBuffer, - crossFadeSamples); + crossFadeSamples, + channelCount); } } else { // No samples for crossfading, ramp to zero @@ -221,7 +227,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; @@ -233,11 +241,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, @@ -269,8 +277,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; } @@ -293,8 +302,8 @@ double ReadAheadManager::getFilePlaypositionFromLog( if (shouldNotifySeek) { if (m_pRateControl) { const auto seekPosition = - mixxx::audio::FramePos::fromEngineSamplePos( - entry.virtualPlaypositionStart); + mixxx::audio::FramePos::fromSamplePos( + entry.virtualPlaypositionStart, channelCount); m_pRateControl->notifySeek(seekPosition); } } @@ -315,9 +324,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.toSamplePos(channelCount), + numConsumedFrames * channelCount, + channelCount); + return mixxx::audio::FramePos::fromSamplePos(positionSamples, channelCount); } diff --git a/src/engine/readaheadmanager.h b/src/engine/readaheadmanager.h index ae71a5f6b1c..3ef71d2b090 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 @@ -120,7 +126,7 @@ class ReadAheadManager { LoopingControl* m_pLoopingControl; RateControl* m_pRateControl; std::list m_readAheadLog; - double m_currentPosition; + double m_currentPosition; // In absolute samples CachingReader* m_pReader; CSAMPLE* m_pCrossFadeBuffer; bool m_cacheMissHappened; diff --git a/src/engine/sidechain/enginesidechain.cpp b/src/engine/sidechain/enginesidechain.cpp index 306b161aec5..49fbdf1f656 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 28861f0f67b..5533382e3dd 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 dd1169a5d21..7403e068af3 100644 --- a/src/mixer/basetrackplayer.cpp +++ b/src/mixer/basetrackplayer.cpp @@ -287,7 +287,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/preferences/dialog/dlgprefsound.cpp b/src/preferences/dialog/dlgprefsound.cpp index e1b844892ec..72bca9fe9df 100644 --- a/src/preferences/dialog/dlgprefsound.cpp +++ b/src/preferences/dialog/dlgprefsound.cpp @@ -37,22 +37,25 @@ bool soundItemAlreadyExists(const AudioPath& output, const QWidget& widget) { } #ifdef __RUBBERBAND__ -const QString kKeylockMultiThreadedAvailable = - QStringLiteral("

") + +const QString kKeylockMultiThreadedAvailable = QStringLiteral("

") + + QObject::tr( + "Distribute stereo channels into mono channels processed in " + "parallel.") + + QStringLiteral("

") + QObject::tr("Warning!") + QStringLiteral("

") + QObject::tr( - "Using multi " - "threading may result in pitch and tone imperfection, and this " + "Processing stereo signal as mono channel " + "may result in pitch and tone imperfection, and this " "is " "mono-incompatible, due to third party limitations.") + QStringLiteral("

"); const QString kKeylockMultiThreadedUnavailableMono = QStringLiteral("") + QObject::tr( - "Multi threading mode is incompatible with mono main mix.") + + "Dual threading mode is incompatible with mono main mix.") + QStringLiteral(""); const QString kKeylockMultiThreadedUnavailableRubberband = QStringLiteral("") + - QObject::tr("Multi threading mode is only available with RubberBand.") + + QObject::tr("Dual threading mode is only available with RubberBand.") + QStringLiteral(""); #endif } // namespace @@ -209,12 +212,12 @@ DlgPrefSound::DlgPrefSound(QWidget* pParent, this, &DlgPrefSound::settingChanged); #ifdef __RUBBERBAND__ - connect(keylockMultithreadedCheckBox, + connect(keylockDualthreadedCheckBox, &QCheckBox::clicked, this, &DlgPrefSound::updateKeylockMultithreading); #else - keylockMultithreadedCheckBox->hide(); + keylockDualthreadedCheckBox->hide(); #endif connect(queryButton, &QAbstractButton::clicked, this, &DlgPrefSound::queryClicked); @@ -303,6 +306,7 @@ DlgPrefSound::DlgPrefSound(QWidget* pParent, void DlgPrefSound::slotUpdate() { m_bSkipConfigClear = true; loadSettings(); + settingChanged(); checkLatencyCompensation(); m_bSkipConfigClear = false; } @@ -330,11 +334,11 @@ void DlgPrefSound::slotApply() { bool keylockMultithreading = m_pSettings->getValue( ConfigKey(kAppGroup, "keylock_multithreading"), false); m_pSettings->setValue(ConfigKey(kAppGroup, "keylock_multithreading"), - keylockMultithreadedCheckBox->isChecked() && - keylockMultithreadedCheckBox->isEnabled()); + keylockDualthreadedCheckBox->isChecked() && + keylockDualthreadedCheckBox->isEnabled()); if (keylockMultithreading != - (keylockMultithreadedCheckBox->isChecked() && - keylockMultithreadedCheckBox->isEnabled())) { + (keylockDualthreadedCheckBox->isChecked() && + keylockDualthreadedCheckBox->isEnabled())) { QMessageBox::information(this, tr("Information"), tr("Mixxx must be restarted before the multi-threaded " @@ -537,7 +541,7 @@ void DlgPrefSound::loadSettings(const SoundManagerConfig& config) { #ifdef __RUBBERBAND__ // Default is no multi threading on keylock - keylockMultithreadedCheckBox->setChecked(m_pSettings->getValue( + keylockDualthreadedCheckBox->setChecked(m_pSettings->getValue( ConfigKey(kAppGroup, QStringLiteral("keylock_multithreading")), false)); #endif @@ -742,8 +746,8 @@ void DlgPrefSound::settingChanged() { .value() != EngineBuffer::KeylockEngine::SoundTouch; bool monoMix = mainOutputModeComboBox->currentIndex() == 1; - keylockMultithreadedCheckBox->setEnabled(!monoMix && supportedScaler); - keylockMultithreadedCheckBox->setToolTip(monoMix + keylockDualthreadedCheckBox->setEnabled(!monoMix && supportedScaler); + keylockDualthreadedCheckBox->setToolTip(monoMix ? kKeylockMultiThreadedUnavailableMono : (supportedScaler ? kKeylockMultiThreadedAvailable @@ -758,18 +762,20 @@ void DlgPrefSound::updateKeylockMultithreading(bool enabled) { QMessageBox msg; msg.setIcon(QMessageBox::Warning); msg.setWindowTitle(tr("Are you sure?")); - msg.setText(QStringLiteral("

%1

%2

") - .arg(tr("Using multi threading result in a loss of " - "mono compatibility and a diffuse stereo " - "image. It is not recommended during " - "broadcasting or recording."), - tr("Are you sure you wish to proceed?"))); + msg.setText( + QStringLiteral("

%1

%2

") + .arg(tr("Distribute stereo channels into mono channels for " + "parallel processing will result in a loss of " + "mono compatibility and a diffuse stereo " + "image. It is not recommended during " + "broadcasting or recording."), + tr("Are you sure you wish to proceed?"))); QPushButton* pNoBtn = msg.addButton(tr("No"), QMessageBox::AcceptRole); QPushButton* pYesBtn = msg.addButton( tr("Yes, I know what I am doing"), QMessageBox::RejectRole); msg.setDefaultButton(pNoBtn); msg.exec(); - keylockMultithreadedCheckBox->setChecked(msg.clickedButton() == pYesBtn); + keylockDualthreadedCheckBox->setChecked(msg.clickedButton() == pYesBtn); #endif } @@ -935,8 +941,8 @@ void DlgPrefSound::mainOutputModeComboBoxChanged(int value) { bool supportedScaler = keylockComboBox->currentData() .value() != EngineBuffer::KeylockEngine::SoundTouch; - keylockMultithreadedCheckBox->setEnabled(!value && supportedScaler); - keylockMultithreadedCheckBox->setToolTip( + keylockDualthreadedCheckBox->setEnabled(!value && supportedScaler); + keylockDualthreadedCheckBox->setToolTip( value ? kKeylockMultiThreadedUnavailableMono : (supportedScaler ? kKeylockMultiThreadedAvailable diff --git a/src/preferences/dialog/dlgprefsounddlg.ui b/src/preferences/dialog/dlgprefsounddlg.ui index ca0bebbd380..34511d6535e 100644 --- a/src/preferences/dialog/dlgprefsounddlg.ui +++ b/src/preferences/dialog/dlgprefsounddlg.ui @@ -224,18 +224,15 @@ - + 0 0 - - <html><head/><body><p><span style=" font-weight:600;">Warning!</span></p><p>Using multi threading may result in pitch and tone imperfaction depending of the platform, leading to mono-incompatibiltiy, due to third party limitations. </p><p><br/></p></body></html> - - Multi-threaded + Dual-threaded Stereo diff --git a/src/sources/soundsourcecoreaudio.cpp b/src/sources/soundsourcecoreaudio.cpp index 83006a8f9c7..db0f76efbab 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 eee9c7affbe..af171ecb994 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 a9d4b3eb9da..4cebd3b7b9b 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 81f3379a4fd..09c8f0d6778 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 86cad1d0253..71ca00283b1 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 1cb4399392a..fe1d63dc1e3 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->setSignal(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 149dcd5b852..41ff0245fe9 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 74c34f1cad5..f4431980162 100644 --- a/src/test/mockedenginebackendtest.h +++ b/src/test/mockedenginebackendtest.h @@ -55,7 +55,8 @@ class MockScaler : public EngineBufferScale { } private: - void onSampleRateChanged() override {} + void onSignalChanged() override { + } double m_processedTempo; double m_processedPitch; diff --git a/src/test/readaheadmanager_test.cpp b/src/test/readaheadmanager_test.cpp index 67674f318cb..393be52397e 100644 --- a/src/test/readaheadmanager_test.cpp +++ b/src/test/readaheadmanager_test.cpp @@ -19,13 +19,17 @@ const QString kGroup = "[test]"; class StubReader : public CachingReader { public: StubReader() - : CachingReader(kGroup, UserSettingsPointer()) { + : CachingReader(kGroup, UserSettingsPointer(), mixxx::audio::ChannelCount::stereo()) { } - 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/sampleutiltest.cpp b/src/test/sampleutiltest.cpp index ecf19f3b321..8365a44526b 100644 --- a/src/test/sampleutiltest.cpp +++ b/src/test/sampleutiltest.cpp @@ -373,28 +373,57 @@ TEST_F(SampleUtilTest, reverse) { } } -TEST_F(SampleUtilTest, copyReverse) { - if (buffers.size() > 1 && sizes[0] > 10 && sizes[1] > 10) { - CSAMPLE* source = buffers[0]; - CSAMPLE* destination = buffers[1]; - for (int i = 0; i < 10; ++i) { - source[i] = i * 0.1f; - } +TEST_F(SampleUtilTest, copyReverseStereo) { + EXPECT_TRUE(buffers.size() > 1 && sizes[0] > 10 && sizes[1] > 10); - SampleUtil::copyReverse(destination, source, 10); + CSAMPLE* source = buffers[0]; + CSAMPLE* destination = buffers[1]; + for (int i = 0; i < 10; ++i) { + source[i] = i * 0.1f; + } - // check if right channel remains at odd index - EXPECT_FLOAT_EQ(destination[0], 0.8f); - EXPECT_FLOAT_EQ(destination[1], 0.9f); - EXPECT_FLOAT_EQ(destination[2], 0.6f); - EXPECT_FLOAT_EQ(destination[3], 0.7f); - EXPECT_FLOAT_EQ(destination[4], 0.4f); - EXPECT_FLOAT_EQ(destination[5], 0.5f); - EXPECT_FLOAT_EQ(destination[6], 0.2f); - EXPECT_FLOAT_EQ(destination[7], 0.3f); - EXPECT_FLOAT_EQ(destination[8], 0.0f); - EXPECT_FLOAT_EQ(destination[9], 0.1f); + SampleUtil::copyReverse(destination, source, 10, mixxx::audio::ChannelCount::stereo()); + + // check if right channel remains at odd index + EXPECT_FLOAT_EQ(destination[0], 0.8f); + EXPECT_FLOAT_EQ(destination[1], 0.9f); + EXPECT_FLOAT_EQ(destination[2], 0.6f); + EXPECT_FLOAT_EQ(destination[3], 0.7f); + EXPECT_FLOAT_EQ(destination[4], 0.4f); + EXPECT_FLOAT_EQ(destination[5], 0.5f); + EXPECT_FLOAT_EQ(destination[6], 0.2f); + EXPECT_FLOAT_EQ(destination[7], 0.3f); + EXPECT_FLOAT_EQ(destination[8], 0.0f); + EXPECT_FLOAT_EQ(destination[9], 0.1f); +} + +TEST_F(SampleUtilTest, copyReverseStem) { + EXPECT_TRUE(buffers.size() > 1 && sizes[0] > 16 && sizes[1] > 16); + CSAMPLE* source = buffers[0]; + CSAMPLE* destination = buffers[1]; + for (int i = 0; i < 16; ++i) { + source[i] = i * 0.1f; } + + SampleUtil::copyReverse(destination, source, 16, mixxx::audio::ChannelCount::stem()); + + // check if multi channel remains in the same order + EXPECT_FLOAT_EQ(destination[0], 0.8f); + EXPECT_FLOAT_EQ(destination[1], 0.9f); + EXPECT_FLOAT_EQ(destination[2], 1.0f); + EXPECT_FLOAT_EQ(destination[3], 1.1f); + EXPECT_FLOAT_EQ(destination[4], 1.2f); + EXPECT_FLOAT_EQ(destination[5], 1.3f); + EXPECT_FLOAT_EQ(destination[6], 1.4f); + EXPECT_FLOAT_EQ(destination[7], 1.5f); + EXPECT_FLOAT_EQ(destination[8], 0.0f); + EXPECT_FLOAT_EQ(destination[9], 0.1f); + EXPECT_FLOAT_EQ(destination[10], 0.2f); + EXPECT_FLOAT_EQ(destination[11], 0.3f); + EXPECT_FLOAT_EQ(destination[12], 0.4f); + EXPECT_FLOAT_EQ(destination[13], 0.5f); + EXPECT_FLOAT_EQ(destination[14], 0.6f); + EXPECT_FLOAT_EQ(destination[15], 0.7f); } static void BM_MemCpy(benchmark::State& state) { diff --git a/src/test/stems/01-drum.wav b/src/test/stems/01-drum.wav new file mode 100644 index 00000000000..e5b8a3b750d Binary files /dev/null and b/src/test/stems/01-drum.wav differ diff --git a/src/test/stems/02-bass.wav b/src/test/stems/02-bass.wav new file mode 100644 index 00000000000..35b71c87155 Binary files /dev/null and b/src/test/stems/02-bass.wav differ diff --git a/src/test/stems/03-melody.wav b/src/test/stems/03-melody.wav new file mode 100644 index 00000000000..11f09449727 Binary files /dev/null and b/src/test/stems/03-melody.wav differ diff --git a/src/test/stems/04-vocal.wav b/src/test/stems/04-vocal.wav new file mode 100644 index 00000000000..7aaf453db06 Binary files /dev/null and b/src/test/stems/04-vocal.wav differ diff --git a/src/test/stemtest.cpp b/src/test/stemtest.cpp index 7a382e131dc..ae7d2bf79d0 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()); } }; @@ -88,4 +96,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/track.cpp b/src/track/track.cpp index 0b0ba35da9e..ff2c42d79dd 100644 --- a/src/track/track.cpp +++ b/src/track/track.cpp @@ -842,7 +842,7 @@ mixxx::audio::SampleRate Track::getSampleRate() const { return m_record.getMetadata().getStreamInfo().getSignalInfo().getSampleRate(); } -int Track::getChannels() const { +mixxx::audio::ChannelCount Track::getChannels() const { const auto locked = lockMutex(&m_qMutex); return m_record.getMetadata().getStreamInfo().getSignalInfo().getChannelCount(); } diff --git a/src/track/track.h b/src/track/track.h index 6e5a82906a0..6075257cd1b 100644 --- a/src/track/track.h +++ b/src/track/track.h @@ -111,7 +111,7 @@ class Track : public QObject { QString getType() const; // Get number of channels - int getChannels() const; + mixxx::audio::ChannelCount getChannels() const; mixxx::audio::SampleRate getSampleRate() const; diff --git a/src/track/trackrecord.cpp b/src/track/trackrecord.cpp index 521abee151d..96b4a25ffd1 100644 --- a/src/track/trackrecord.cpp +++ b/src/track/trackrecord.cpp @@ -332,9 +332,15 @@ bool TrackRecord::updateStreamInfoFromSource( streamInfoFromSource.setBitrate( getMetadata().getStreamInfo().getBitrate()); } - // Stream properties are not expected to vary during a session + // Stream properties are not expected to vary during a session, apart from + // the channel count and so the bitrate as different components may request + // the stream in stereo or multi channels VERIFY_OR_DEBUG_ASSERT(!m_streamInfoFromSource || - *m_streamInfoFromSource == streamInfoFromSource) { + (m_streamInfoFromSource->getDuration() == + streamInfoFromSource.getDuration() && + m_streamInfoFromSource->getSignalInfo().getSampleRate() == + streamInfoFromSource.getSignalInfo() + .getSampleRate())) { kLogger.warning() << "Varying stream properties:" << *m_streamInfoFromSource diff --git a/src/util/sample.cpp b/src/util/sample.cpp index 56bf630c728..b23561c5207 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); @@ -203,7 +203,6 @@ void SampleUtil::applyRampingAlternatingGain(CSAMPLE* pBuffer, / CSAMPLE_GAIN(numSamples / 2); if (gain1Delta != 0) { const CSAMPLE_GAIN start_gain = gain1Old + gain1Delta; - // note: LOOP VECTORIZED. for (int i = 0; i < numSamples / 2; ++i) { const CSAMPLE_GAIN gain = start_gain + gain1Delta * i; pBuffer[i * 2] *= gain; @@ -219,7 +218,7 @@ void SampleUtil::applyRampingAlternatingGain(CSAMPLE* pBuffer, / CSAMPLE_GAIN(numSamples / 2); if (gain2Delta != 0) { const CSAMPLE_GAIN start_gain = gain2Old + gain2Delta; - // note: LOOP VECTORIZED. + // note: LOOP VECTORIZED. (gcc + clang >= 14) for (int i = 0; i < numSamples / 2; ++i) { const CSAMPLE_GAIN gain = start_gain + gain2Delta * i; pBuffer[i * 2 + 1] *= gain; @@ -236,7 +235,7 @@ void SampleUtil::applyRampingAlternatingGain(CSAMPLE* pBuffer, void SampleUtil::add(CSAMPLE* M_RESTRICT pDest, const CSAMPLE* M_RESTRICT pSrc, SINT numSamples) { - // note: LOOP VECTORIZED. + // note: LOOP VECTORIZED. (gcc + clang >= 14) for (SINT i = 0; i < numSamples; ++i) { pDest[i] += pSrc[i]; } @@ -366,7 +365,7 @@ void SampleUtil::copyWithRampingGain(CSAMPLE* M_RESTRICT pDest, / CSAMPLE_GAIN(numSamples / 2); if (gain_delta != 0) { const CSAMPLE_GAIN start_gain = old_gain + gain_delta; - // note: LOOP VECTORIZED only with "int i" (not SINT i) + // note: LOOP VECTORIZED only with "int i" (not SINT i). for (int i = 0; i < numSamples / 2; ++i) { const CSAMPLE_GAIN gain = start_gain + gain_delta * i; pDest[i * 2] = pSrc[i * 2] * gain; @@ -405,7 +404,7 @@ void SampleUtil::convertFloat32ToS16(SAMPLE* pDest, const CSAMPLE* pSrc, // +1.0 is clamped to 32767 (0.99996942) DEBUG_ASSERT(-SAMPLE_MINIMUM >= SAMPLE_MAXIMUM); const CSAMPLE kConversionFactor = SAMPLE_MINIMUM * -1.0f; - // note: LOOP VECTORIZED only with "int i" (not SINT i) + // note: LOOP VECTORIZED only with "int i" (not SINT i). for (int i = 0; i < numSamples; ++i) { pDest[i] = static_cast(math_clamp(pSrc[i] * kConversionFactor, static_cast(SAMPLE_MINIMUM), @@ -448,6 +447,7 @@ SampleUtil::CLIP_STATUS SampleUtil::sumAbsPerChannel(CSAMPLE* pfAbsL, CSAMPLE SampleUtil::sumSquared(const CSAMPLE* pBuffer, SINT numSamples) { CSAMPLE sumSq = CSAMPLE_ZERO; + // note: LOOP VECTORIZED. for (SINT i = 0; i < numSamples; ++i) { sumSq += pBuffer[i] * pBuffer[i]; } @@ -462,6 +462,7 @@ CSAMPLE SampleUtil::rms(const CSAMPLE* pBuffer, SINT numSamples) { CSAMPLE SampleUtil::maxAbsAmplitude(const CSAMPLE* pBuffer, SINT numSamples) { CSAMPLE max = pBuffer[0]; + // note: LOOP VECTORIZED. for (SINT i = 1; i < numSamples; ++i) { CSAMPLE absValue = abs(pBuffer[i]); if (absValue > max) { @@ -492,6 +493,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 +530,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 +576,74 @@ 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. + 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; + pDestSrcFadeOut[i * 8 + 1] *= (CSAMPLE_GAIN_ONE - cross_mix); + pDestSrcFadeOut[i * 8 + 1] += pSrcFadeIn[i * 8 + 1] * cross_mix; + pDestSrcFadeOut[i * 8 + 2] *= (CSAMPLE_GAIN_ONE - cross_mix); + pDestSrcFadeOut[i * 8 + 2] += pSrcFadeIn[i * 8 + 2] * cross_mix; + pDestSrcFadeOut[i * 8 + 3] *= (CSAMPLE_GAIN_ONE - cross_mix); + pDestSrcFadeOut[i * 8 + 3] += pSrcFadeIn[i * 8 + 3] * cross_mix; + pDestSrcFadeOut[i * 8 + 4] *= (CSAMPLE_GAIN_ONE - cross_mix); + pDestSrcFadeOut[i * 8 + 4] += pSrcFadeIn[i * 8 + 4] * cross_mix; + pDestSrcFadeOut[i * 8 + 5] *= (CSAMPLE_GAIN_ONE - cross_mix); + pDestSrcFadeOut[i * 8 + 5] += pSrcFadeIn[i * 8 + 5] * cross_mix; + pDestSrcFadeOut[i * 8 + 6] *= (CSAMPLE_GAIN_ONE - cross_mix); + pDestSrcFadeOut[i * 8 + 6] += pSrcFadeIn[i * 8 + 6] * cross_mix; + pDestSrcFadeOut[i * 8 + 7] *= (CSAMPLE_GAIN_ONE - cross_mix); + pDestSrcFadeOut[i * 8 + 7] += pSrcFadeIn[i * 8 + 7] * cross_mix; + } +} + +// static +void SampleUtil::linearCrossfadeBuffersOut( + CSAMPLE* pDestSrcFadeOut, + const CSAMPLE* pSrcFadeIn, + SINT numSamples, + int channelCount) { + switch (channelCount) { + case mixxx::audio::ChannelCount::stereo(): + return SampleUtil::linearCrossfadeStereoBuffersOut( + pDestSrcFadeOut, + pSrcFadeIn, + numSamples); + case mixxx::audio::ChannelCount::stem(): + return SampleUtil::linearCrossfadeStemBuffersOut( + pDestSrcFadeOut, + pSrcFadeIn, + numSamples); + default: + // Fallback to unoptimised function + { + DEBUG_ASSERT(numSamples % channelCount == 0); + int numFrame = numSamples / channelCount; + const CSAMPLE_GAIN cross_inc = + CSAMPLE_GAIN_ONE / CSAMPLE_GAIN(numSamples / channelCount); + for (int i = 0; i < numFrame; ++i) { + const CSAMPLE_GAIN cross_mix = cross_inc * i; + // note: LOOP VECTORIZED. + for (int chIdx = 0; chIdx < channelCount; chIdx++) { + pDestSrcFadeOut[i * channelCount + chIdx] *= (CSAMPLE_GAIN_ONE - cross_mix); + pDestSrcFadeOut[i * channelCount + chIdx] += + pSrcFadeIn[i * channelCount + chIdx] * cross_mix; + } + } + } + break; + } +} + +// static +void SampleUtil::linearCrossfadeStereoBuffersIn( CSAMPLE* M_RESTRICT pDestSrcFadeIn, const CSAMPLE* M_RESTRICT pSrcFadeOut, SINT numSamples) { @@ -547,6 +663,73 @@ 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. + 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); + pDestSrcFadeIn[i * 8 + 1] *= cross_mix; + pDestSrcFadeIn[i * 8 + 1] += pSrcFadeOut[i * 8 + 1] * (CSAMPLE_GAIN_ONE - cross_mix); + pDestSrcFadeIn[i * 8 + 2] *= cross_mix; + pDestSrcFadeIn[i * 8 + 2] += pSrcFadeOut[i * 8 + 2] * (CSAMPLE_GAIN_ONE - cross_mix); + pDestSrcFadeIn[i * 8 + 3] *= cross_mix; + pDestSrcFadeIn[i * 8 + 3] += pSrcFadeOut[i * 8 + 3] * (CSAMPLE_GAIN_ONE - cross_mix); + pDestSrcFadeIn[i * 8 + 4] *= cross_mix; + pDestSrcFadeIn[i * 8 + 4] += pSrcFadeOut[i * 8 + 4] * (CSAMPLE_GAIN_ONE - cross_mix); + pDestSrcFadeIn[i * 8 + 5] *= cross_mix; + pDestSrcFadeIn[i * 8 + 5] += pSrcFadeOut[i * 8 + 5] * (CSAMPLE_GAIN_ONE - cross_mix); + pDestSrcFadeIn[i * 8 + 6] *= cross_mix; + pDestSrcFadeIn[i * 8 + 6] += pSrcFadeOut[i * 8 + 6] * (CSAMPLE_GAIN_ONE - cross_mix); + pDestSrcFadeIn[i * 8 + 7] *= cross_mix; + pDestSrcFadeIn[i * 8 + 7] += pSrcFadeOut[i * 8 + 7] * (CSAMPLE_GAIN_ONE - cross_mix); + } +} + +// static +void SampleUtil::linearCrossfadeBuffersIn( + CSAMPLE* pDestSrcFadeIn, + const CSAMPLE* pSrcFadeOut, + SINT numSamples, + int channelCount) { + switch (channelCount) { + case mixxx::audio::ChannelCount::stereo(): + return SampleUtil::linearCrossfadeStereoBuffersIn( + pDestSrcFadeIn, + pSrcFadeOut, + numSamples); + case mixxx::audio::ChannelCount::stem(): + return SampleUtil::linearCrossfadeStemBuffersIn( + pDestSrcFadeIn, + pSrcFadeOut, + numSamples); + default: + // Fallback to unoptimised function + { + int numFrame = numSamples / channelCount; + const CSAMPLE_GAIN cross_inc = + CSAMPLE_GAIN_ONE / CSAMPLE_GAIN(numSamples / channelCount); + for (int i = 0; i < numFrame; ++i) { + const CSAMPLE_GAIN cross_mix = cross_inc * i; + // note: LOOP VECTORIZED. + for (int chIdx = 0; chIdx < channelCount; chIdx++) { + pDestSrcFadeIn[i * channelCount + chIdx] *= cross_mix; + pDestSrcFadeIn[i * channelCount + chIdx] += + pSrcFadeOut[i * channelCount + chIdx] * + (CSAMPLE_GAIN_ONE - cross_mix); + } + } + } + break; + } +} + // static void SampleUtil::mixStereoToMono(CSAMPLE* M_RESTRICT pDest, const CSAMPLE* M_RESTRICT pSrc, @@ -572,8 +755,9 @@ 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); + // note: LOOP VECTORIZED. for (SINT i = 0; i < numSamples / chCount; ++i) { pDest[i] = CSAMPLE_ZERO; for (auto ch = 0; ch < chCount; ++ch) { @@ -599,7 +783,7 @@ void SampleUtil::doubleMonoToDualMono(CSAMPLE* pBuffer, SINT numFrames) { void SampleUtil::copyMonoToDualMono(CSAMPLE* M_RESTRICT pDest, const CSAMPLE* M_RESTRICT pSrc, SINT numFrames) { // forward loop - // note: LOOP VECTORIZED + // note: LOOP VECTORIZED. for (SINT i = 0; i < numFrames; ++i) { const CSAMPLE s = pSrc[i]; pDest[i * 2] = s; @@ -611,7 +795,7 @@ void SampleUtil::copyMonoToDualMono(CSAMPLE* M_RESTRICT pDest, void SampleUtil::addMonoToStereo(CSAMPLE* M_RESTRICT pDest, const CSAMPLE* M_RESTRICT pSrc, SINT numFrames) { // forward loop - // note: LOOP VECTORIZED + // note: LOOP VECTORIZED. for (SINT i = 0; i < numFrames; ++i) { const CSAMPLE s = pSrc[i]; pDest[i * 2] += s; @@ -640,6 +824,7 @@ void SampleUtil::copyMultiToStereo( mixxx::audio::ChannelCount numChannels) { DEBUG_ASSERT(numChannels > mixxx::audio::ChannelCount::stereo()); // forward loop + // note: LOOP VECTORIZED. for (SINT i = 0; i < numFrames; ++i) { pDest[i * 2] = pSrc[i * numChannels]; pDest[i * 2 + 1] = pSrc[i * numChannels + 1]; @@ -661,10 +846,15 @@ void SampleUtil::reverse(CSAMPLE* pBuffer, SINT numSamples) { // static void SampleUtil::copyReverse(CSAMPLE* M_RESTRICT pDest, - const CSAMPLE* M_RESTRICT pSrc, SINT numSamples) { - for (SINT j = 0; j < numSamples / 2; ++j) { - const int endpos = (numSamples - 1) - j * 2; - pDest[j * 2] = pSrc[endpos - 1]; - pDest[j * 2 + 1] = pSrc[endpos]; + const CSAMPLE* M_RESTRICT pSrc, + SINT numSamples, + int channelCount) { + DEBUG_ASSERT(numSamples % channelCount == 0); + for (SINT frameIdx = 0; frameIdx < numSamples / channelCount; ++frameIdx) { + const int endpos = (numSamples - 1) - frameIdx * channelCount; + // note: LOOP VECTORIZED. + for (int chIdx = 0; chIdx < channelCount; chIdx++) { + pDest[frameIdx * channelCount + chIdx] = pSrc[endpos - channelCount + chIdx + 1]; + } } } diff --git a/src/util/sample.h b/src/util/sample.h index f94c87ca4fb..2be13a38109 100644 --- a/src/util/sample.h +++ b/src/util/sample.h @@ -235,27 +235,69 @@ 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( - CSAMPLE* pDestSrcFadeOut, const CSAMPLE* pSrcFadeIn, SINT numSamples); + CSAMPLE* pDestSrcFadeOut, const CSAMPLE* pSrcFadeIn, SINT numSamples, int channelCount); static void linearCrossfadeBuffersIn( + CSAMPLE* pDestSrcFadeIn, const CSAMPLE* pSrcFadeOut, SINT numSamples, int channelCount); + + private: + static void linearCrossfadeStereoBuffersOut( + CSAMPLE* pDestSrcFadeOut, const CSAMPLE* pSrcFadeIn, SINT numSamples); + static void linearCrossfadeStemBuffersOut( + CSAMPLE* pDestSrcFadeOut, const CSAMPLE* pSrcFadeIn, SINT numSamples); + static void linearCrossfadeStereoBuffersIn( + CSAMPLE* pDestSrcFadeIn, const CSAMPLE* pSrcFadeOut, SINT numSamples); + static void linearCrossfadeStemBuffersIn( CSAMPLE* pDestSrcFadeIn, const CSAMPLE* pSrcFadeOut, SINT numSamples); + public: // 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 // "mono-compatible", ie there are no major out-of-phase parts of the signal. @@ -314,8 +356,9 @@ class SampleUtil { // copy pSrc to pDest and reverses stereo sample order (backward) static void copyReverse(CSAMPLE* M_RESTRICT pDest, - const CSAMPLE* M_RESTRICT pSrc, SINT numSamples); - + const CSAMPLE* M_RESTRICT pSrc, + SINT numSamples, + int channelCount); // Include auto-generated methods (e.g. copyXWithGain, copyXWithRampingGain, // etc.) diff --git a/src/waveform/waveform.cpp b/src/waveform/waveform.cpp index 3f0331e1b31..3f01da89b27 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 35be6fe3d22..8af6442b986 100644 --- a/src/widget/woverview.cpp +++ b/src/widget/woverview.cpp @@ -1543,7 +1543,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(); }