diff --git a/CMakeLists.txt b/CMakeLists.txt index 66b2a75967a..e74460556e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3455,7 +3455,15 @@ if (STEM) target_compile_definitions(mixxx-lib PUBLIC __STEM__) target_sources(mixxx-lib PRIVATE src/sources/soundsourcestem.cpp + src/track/steminfoimporter.cpp + src/track/steminfo.cpp # TODO precompiled header? ) + if(QML) + target_compile_definitions(mixxx-qml-lib PUBLIC __STEM__) + target_sources(mixxx-qml-lib PRIVATE + src/qml/qmlstemsmodel.cpp + ) + endif() endif() # Google PerfTools diff --git a/res/qml/EqColumn.qml b/res/qml/EqColumn.qml index acf1f3ab1c2..23188c98c8c 100644 --- a/res/qml/EqColumn.qml +++ b/res/qml/EqColumn.qml @@ -1,44 +1,130 @@ import "." as Skin import QtQuick 2.12 import QtQuick.Shapes 1.12 +import QtQuick.Layouts +import Mixxx 1.0 as Mixxx import "Theme" Column { id: root required property string group + property var player: Mixxx.PlayerManager.getPlayer(root.group) - spacing: 4 + Mixxx.ControlProxy { + id: stemCountControl - Skin.EqKnob { - statusKey: "button_parameter3" - knob.group: "[EqualizerRack1_" + root.group + "_Effect1]" - knob.key: "parameter3" - knob.color: Theme.eqHighColor + group: root.group + key: "stem_count" } - Skin.EqKnob { - statusKey: "button_parameter2" - knob.group: "[EqualizerRack1_" + root.group + "_Effect1]" - knob.key: "parameter2" - knob.color: Theme.eqMidColor - } + Row { + Column { + id: stem + spacing: 4 + width: undefined + Repeater { + model: root.player.stemsModel - Skin.EqKnob { - knob.group: "[EqualizerRack1_" + root.group + "_Effect1]" - knob.key: "parameter1" - statusKey: "button_parameter1" - knob.color: Theme.eqLowColor - } + Row { + id: stem + required property int index + required property string label + required property color color + + Rectangle { + id: stemRect + width: 56 + height: 56 + color: stem.color + radius: 5 + + Skin.ControlKnob { + id: knob + group: root.group + key: `stem_${index}_volume` + color: Theme.gainKnobColor + anchors.topMargin: 5 + anchors.top: stemRect.top + anchors.horizontalCenter: stemRect.horizontalCenter + + width: 36 + height: 36 + } + Text { + anchors.bottom: stemRect.bottom + anchors.horizontalCenter: stemRect.horizontalCenter + text: label + } + } + } + } + } + Column { + id: eq + spacing: 4 + width: undefined + Skin.EqKnob { + statusKey: "button_parameter3" + knob.group: "[EqualizerRack1_" + root.group + "_Effect1]" + knob.key: "parameter3" + knob.color: Theme.eqHighColor + } + + Skin.EqKnob { + statusKey: "button_parameter2" + knob.group: "[EqualizerRack1_" + root.group + "_Effect1]" + knob.key: "parameter2" + knob.color: Theme.eqMidColor + } + + Skin.EqKnob { + knob.group: "[EqualizerRack1_" + root.group + "_Effect1]" + knob.key: "parameter1" + statusKey: "button_parameter1" + knob.color: Theme.eqLowColor + } + + Skin.EqKnob { + knob.group: "[QuickEffectRack1_" + root.group + "]" + knob.key: "super1" + statusGroup: "[QuickEffectRack1_" + root.group + "_Effect1]" + statusKey: "enabled" + knob.arcStyle: ShapePath.DashLine + knob.arcStylePattern: [2, 2] + knob.color: Theme.eqFxColor + } + } + states: [ + State { + name: "eq" + when: stemCountControl.value == 0 + PropertyChanges { target: stem; opacity: 0; width: 0} + }, + State { + name: "stem" + when: stemCountControl.value != 0 + PropertyChanges { target: eq; opacity: 0; width: 0 } + } + ] - Skin.EqKnob { - knob.group: "[QuickEffectRack1_" + root.group + "]" - knob.key: "super1" - statusGroup: "[QuickEffectRack1_" + root.group + "_Effect1]" - statusKey: "enabled" - knob.arcStyle: ShapePath.DashLine - knob.arcStylePattern: [2, 2] - knob.color: Theme.eqFxColor + transitions: [ + Transition { + from: "eq" + to: "stem" + ParallelAnimation { + PropertyAnimation { targets: [eq, stem]; properties: "opacity,width"; duration: 1000} + } + }, + Transition { + from: "stem" + to: "eq" + ParallelAnimation { + PropertyAnimation { target: stem; properties: "opacity"; duration: 1000} + PropertyAnimation { target: eq; properties: "opacity"; duration: 1000} + } + } + ] } Skin.OrientationToggleButton { diff --git a/res/qml/Mixer.qml b/res/qml/Mixer.qml index 05e362920a8..9a6b5bd165c 100644 --- a/res/qml/Mixer.qml +++ b/res/qml/Mixer.qml @@ -1,4 +1,5 @@ import "." as Skin +import Mixxx 1.0 as Mixxx import QtQuick 2.12 Item { diff --git a/src/analyzer/analyzersilence.cpp b/src/analyzer/analyzersilence.cpp index dde96531b88..1f157434a52 100644 --- a/src/analyzer/analyzersilence.cpp +++ b/src/analyzer/analyzersilence.cpp @@ -74,10 +74,11 @@ SINT AnalyzerSilence::findLastSoundInChunk(std::span samples) { // static bool AnalyzerSilence::verifyFirstSound( std::span samples, - mixxx::audio::FramePos firstSoundFrame) { + mixxx::audio::FramePos firstSoundFrame, + mixxx::audio::ChannelCount channelCount) { const SINT firstSoundSample = findFirstSoundInChunk(samples); if (firstSoundSample < static_cast(samples.size())) { - return mixxx::audio::FramePos::fromEngineSamplePos(firstSoundSample) + return mixxx::audio::FramePos::fromEngineSamplePos(firstSoundSample, channelCount) .toLowerFrameBoundary() == firstSoundFrame.toLowerFrameBoundary(); } return false; diff --git a/src/analyzer/analyzersilence.h b/src/analyzer/analyzersilence.h index 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/engine/bufferscalers/enginebufferscale.cpp b/src/engine/bufferscalers/enginebufferscale.cpp index adf203ad5e6..bb513c2db29 100644 --- a/src/engine/bufferscalers/enginebufferscale.cpp +++ b/src/engine/bufferscalers/enginebufferscale.cpp @@ -2,24 +2,26 @@ #include "engine/engine.h" #include "moc_enginebufferscale.cpp" +#include "soundio/soundmanagerconfig.h" EngineBufferScale::EngineBufferScale() : m_outputSignal( mixxx::audio::SignalInfo( mixxx::kEngineChannelCount, - mixxx::audio::SampleRate())), + SoundManagerConfig::kMixxxDefaultSampleRate)), 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_outputSignal.isValid()); } void EngineBufferScale::setOutputSignal( mixxx::audio::SampleRate sampleRate, mixxx::audio::ChannelCount channelCount) { DEBUG_ASSERT(sampleRate.isValid()); + DEBUG_ASSERT(channelCount.isValid()); bool changed = false; if (sampleRate != m_outputSignal.getSampleRate()) { m_outputSignal.setSampleRate(sampleRate); diff --git a/src/engine/cachingreader/cachingreaderworker.cpp b/src/engine/cachingreader/cachingreaderworker.cpp index 46d081d34cc..e7db73a8107 100644 --- a/src/engine/cachingreader/cachingreaderworker.cpp +++ b/src/engine/cachingreader/cachingreaderworker.cpp @@ -280,7 +280,8 @@ void CachingReaderWorker::verifyFirstSound(const CachingReaderChunk* pChunk, mixxx::IndexRange::forward(end - 1, kNumSoundFrameToVerify)); // TODO support multi channel if (AnalyzerSilence::verifyFirstSound(sampleBuffer.span(), - mixxx::audio::FramePos(1))) { + 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/channels/enginedeck.cpp b/src/engine/channels/enginedeck.cpp index 94aff40268d..ed4496c26ee 100644 --- a/src/engine/channels/enginedeck.cpp +++ b/src/engine/channels/enginedeck.cpp @@ -7,8 +7,13 @@ #include "engine/enginepregain.h" #include "engine/enginevumeter.h" #include "moc_enginedeck.cpp" +#include "track/track.h" #include "util/sample.h" +namespace { +constexpr int kMaxSupportedStem = 4; +} + EngineDeck::EngineDeck( const ChannelHandleAndGroup& handleGroup, UserSettingsPointer pConfig, @@ -20,6 +25,7 @@ EngineDeck::EngineDeck( /*isTalkoverChannel*/ false, primaryDeck), m_pConfig(pConfig), + m_pStemCount(std::make_unique(ConfigKey(getGroup(), "stem_count"))), m_pInputConfigured(new ControlObject(ConfigKey(getGroup(), "input_configured"))), m_pPassing(new ControlPushButton(ConfigKey(getGroup(), "passthrough"))) { m_pInputConfigured->setReadOnly(); @@ -36,6 +42,23 @@ EngineDeck::EngineDeck( m_pPregain = new EnginePregain(getGroup()); m_pBuffer = new EngineBuffer(getGroup(), pConfig, this, pMixingEngine); + connect(m_pBuffer, &EngineBuffer::trackLoaded, this, &EngineDeck::slotTrackLoaded); + + m_stemGain.reserve(kMaxSupportedStem); + for (int i = 0; i < kMaxSupportedStem; i++) { + m_stemGain.emplace_back(std::make_unique( + ConfigKey(getGroup(), QString("stem_%1_volume").arg(i)), + true, + false, + false, + 1.0)); + } +} + +void EngineDeck::slotTrackLoaded(TrackPointer pNewTrack, + TrackPointer) { + int stemCount = pNewTrack->getStemInfo().size(); + m_pStemCount->set(stemCount); } EngineDeck::~EngineDeck() { @@ -58,14 +81,21 @@ void EngineDeck::processStem(CSAMPLE* pOut, const int iBufferSize) { for (int c = 0; c < stereoChannelCount; c++) { // TODO(XXX): apply stem gain or skip muted stem if (!c) { - pOut[2 * i] = m_stemBuffer.data()[2 * stereoChannelCount * i]; - pOut[2 * i + 1] = m_stemBuffer.data()[2 * stereoChannelCount * i + 1]; + pOut[2 * i] = m_stemBuffer.data()[2 * stereoChannelCount * i] * + m_stemGain[c]->get(); + pOut[2 * i + 1] = + m_stemBuffer.data()[2 * stereoChannelCount * i + 1] * + m_stemGain[c]->get(); } else { - pOut[2 * i] += m_stemBuffer.data()[2 * stereoChannelCount * i + 2 * c]; + pOut[2 * i] += + m_stemBuffer + .data()[2 * stereoChannelCount * i + 2 * c] * + m_stemGain[c]->get(); pOut[2 * i + 1] += m_stemBuffer .data()[2 * stereoChannelCount * i + - 2 * c + 1]; + 2 * c + 1] * + m_stemGain[c]->get(); } } } diff --git a/src/engine/channels/enginedeck.h b/src/engine/channels/enginedeck.h index 3c7cb996b70..8e7cb37261c 100644 --- a/src/engine/channels/enginedeck.h +++ b/src/engine/channels/enginedeck.h @@ -5,6 +5,7 @@ #include "engine/channels/enginechannel.h" #include "preferences/usersettings.h" #include "soundio/soundmanagerutil.h" +#include "track/track_decl.h" #include "util/samplebuffer.h" class EnginePregain; @@ -70,6 +71,7 @@ class EngineDeck : public EngineChannel, public AudioDestination { public slots: void slotPassthroughToggle(double v); void slotPassthroughChangeRequest(double v); + void slotTrackLoaded(TrackPointer pNewTrack, TrackPointer); private: UserSettingsPointer m_pConfig; @@ -77,6 +79,8 @@ class EngineDeck : public EngineChannel, public AudioDestination { EnginePregain* m_pPregain; mixxx::SampleBuffer m_stemBuffer; + std::unique_ptr m_pStemCount; + std::vector> m_stemGain; // Begin vinyl passthrough fields QScopedPointer m_pInputConfigured; diff --git a/src/engine/enginebuffer.cpp b/src/engine/enginebuffer.cpp index bb67ba9936f..516446635a4 100644 --- a/src/engine/enginebuffer.cpp +++ b/src/engine/enginebuffer.cpp @@ -884,13 +884,18 @@ void EngineBuffer::processTrackLocked(CSAMPLE* pOutput, // (1.0 being normal rate. 2.0 plays at 2x speed -- 2 track seconds // pass for every 1 real second). Depending on whether // keylock is enabled, this is applied to either the rate or the tempo. - int stereoPairCount = m_channelCount / mixxx::audio::ChannelCount::stereo(); + int outputBufferSize = iBufferSize, + stereoPairCount = m_channelCount / mixxx::audio::ChannelCount::stereo(); + // The speed is calculated out of the buffer size for the stereo channel + // output, after mixing multi channel (stem) together + if (stereoPairCount > 1) { + outputBufferSize = iBufferSize / stereoPairCount; + } double speed = m_pRateControl->calculateSpeed( baserate, tempoRatio, paused, - // The speed is calculate out of the buffer size for the stereo channel output channel - iBufferSize / stereoPairCount, + outputBufferSize, &is_scratching, &is_reverse); diff --git a/src/qml/qmlplayerproxy.cpp b/src/qml/qmlplayerproxy.cpp index f15a1197489..97676b097a6 100644 --- a/src/qml/qmlplayerproxy.cpp +++ b/src/qml/qmlplayerproxy.cpp @@ -31,7 +31,12 @@ QmlPlayerProxy::QmlPlayerProxy(BaseTrackPlayer* pTrackPlayer, QObject* parent) : QObject(parent), m_pTrackPlayer(pTrackPlayer), m_pBeatsModel(new QmlBeatsModel(this)), - m_pHotcuesModel(new QmlCuesModel(this)) { + m_pHotcuesModel(new QmlCuesModel(this)) +#ifdef __STEM__ + , + m_pStemsModel(std::make_unique(this)) +#endif +{ connect(m_pTrackPlayer, &BaseTrackPlayer::loadingTrack, this, @@ -129,8 +134,17 @@ void QmlPlayerProxy::slotTrackLoaded(TrackPointer pTrack) { &Track::cuesUpdated, this, &QmlPlayerProxy::slotHotcuesChanged); +#ifdef __STEM__ + connect(pTrack.get(), + &Track::stemsUpdated, + this, + &QmlPlayerProxy::slotStemsChanged); +#endif slotBeatsChanged(); slotHotcuesChanged(); +#ifdef __STEM__ + slotStemsChanged(); +#endif } emit trackChanged(); emit trackLoaded(); @@ -175,6 +189,9 @@ void QmlPlayerProxy::slotTrackChanged() { emit colorChanged(); emit coverArtUrlChanged(); emit trackLocationUrlChanged(); +#ifdef __STEM__ + emit stemsChanged(); +#endif emit waveformLengthChanged(); emit waveformTextureChanged(); @@ -219,6 +236,22 @@ void QmlPlayerProxy::slotBeatsChanged() { } } +#ifdef __STEM__ +void QmlPlayerProxy::slotStemsChanged() { + VERIFY_OR_DEBUG_ASSERT(m_pStemsModel != nullptr) { + return; + } + + QList stems; + + const TrackPointer pTrack = m_pCurrentTrack; + if (pTrack) { + m_pStemsModel->setStems(pTrack->getStemInfo()); + emit stemsChanged(); + } +} +#endif + void QmlPlayerProxy::slotHotcuesChanged() { VERIFY_OR_DEBUG_ASSERT(m_pHotcuesModel != nullptr) { return; diff --git a/src/qml/qmlplayerproxy.h b/src/qml/qmlplayerproxy.h index 7bcc20fa558..a23d3882108 100644 --- a/src/qml/qmlplayerproxy.h +++ b/src/qml/qmlplayerproxy.h @@ -9,6 +9,7 @@ #include "mixer/basetrackplayer.h" #include "qml/qmlbeatsmodel.h" #include "qml/qmlcuesmodel.h" +#include "qml/qmlstemsmodel.h" #include "track/cueinfo.h" #include "track/track.h" @@ -47,6 +48,9 @@ class QmlPlayerProxy : public QObject { Q_PROPERTY(mixxx::qml::QmlBeatsModel* beatsModel MEMBER m_pBeatsModel CONSTANT); Q_PROPERTY(mixxx::qml::QmlCuesModel* hotcuesModel MEMBER m_pHotcuesModel CONSTANT); +#ifdef __STEM__ + Q_PROPERTY(mixxx::qml::QmlStemsModel* stemsModel READ getStemsModel CONSTANT); +#endif public: explicit QmlPlayerProxy(BaseTrackPlayer* pTrackPlayer, QObject* parent = nullptr); @@ -82,6 +86,10 @@ class QmlPlayerProxy : public QObject { Q_INVOKABLE void loadTrackFromLocation(const QString& trackLocation, bool play = false); Q_INVOKABLE void loadTrackFromLocationUrl(const QUrl& trackLocationUrl, bool play = false); + QmlStemsModel* getStemsModel() const { + return m_pStemsModel.get(); + } + public slots: void slotTrackLoaded(TrackPointer pTrack); void slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack); @@ -89,6 +97,9 @@ class QmlPlayerProxy : public QObject { void slotWaveformChanged(); void slotBeatsChanged(); void slotHotcuesChanged(); +#ifdef __STEM__ + void slotStemsChanged(); +#endif void setArtist(const QString& artist); void setTitle(const QString& title); @@ -126,6 +137,9 @@ class QmlPlayerProxy : public QObject { void coverArtUrlChanged(); void trackLocationUrlChanged(); void cuesChanged(); +#ifdef __STEM__ + void stemsChanged(); +#endif void loadTrackFromLocationRequested(const QString& trackLocation, bool play); @@ -140,6 +154,9 @@ class QmlPlayerProxy : public QObject { TrackPointer m_pCurrentTrack; QmlBeatsModel* m_pBeatsModel; QmlCuesModel* m_pHotcuesModel; +#ifdef __STEM__ + std::unique_ptr m_pStemsModel; +#endif }; } // namespace qml diff --git a/src/qml/qmlstemsmodel.cpp b/src/qml/qmlstemsmodel.cpp new file mode 100644 index 00000000000..730a5df76b7 --- /dev/null +++ b/src/qml/qmlstemsmodel.cpp @@ -0,0 +1,67 @@ +#include "qml/qmlstemsmodel.h" + +#include + +#include "moc_qmlstemsmodel.cpp" + +namespace mixxx { +namespace qml { +namespace { +const QHash kRoleNames = { + {QmlStemsModel::LabelRole, "label"}, + {QmlStemsModel::ColorRole, "color"}, +}; +} + +QmlStemsModel::QmlStemsModel( + QObject* pParent) + : QAbstractListModel(pParent) { +} + +void QmlStemsModel::setStems(QList stems) { + beginResetModel(); + m_stems = QList(std::move(stems)); + endResetModel(); +} + +QVariant QmlStemsModel::data(const QModelIndex& index, int role) const { + if (index.row() < 0 || index.row() >= m_stems.size()) { + return QVariant(); + } + + const StemInfo& stemInfo = m_stems.at(index.row()); + + switch (role) { + case QmlStemsModel::LabelRole: { + return stemInfo.getLabel(); + } + case QmlStemsModel::ColorRole: + return stemInfo.getColor(); + default: + return QVariant(); + } +} + +int QmlStemsModel::rowCount(const QModelIndex& parent) const { + if (parent.isValid()) { + return 0; + } + + return m_stems.size(); +} + +QHash QmlStemsModel::roleNames() const { + return kRoleNames; +} + +QVariant QmlStemsModel::get(int row) const { + QModelIndex idx = index(row, 0); + QVariantMap dataMap; + for (auto it = kRoleNames.constBegin(); it != kRoleNames.constEnd(); it++) { + dataMap.insert(it.value(), data(idx, it.key())); + } + return dataMap; +} + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmlstemsmodel.h b/src/qml/qmlstemsmodel.h new file mode 100644 index 00000000000..a6db729b08d --- /dev/null +++ b/src/qml/qmlstemsmodel.h @@ -0,0 +1,32 @@ +#pragma once +#include +#include + +#include "track/steminfo.h" + +namespace mixxx { +namespace qml { + +class QmlStemsModel : public QAbstractListModel { + Q_OBJECT + public: + enum Roles { + LabelRole, + ColorRole, + }; + Q_ENUM(Roles) + explicit QmlStemsModel(QObject* pParent = nullptr); + + void setStems(QList stems); + + QVariant data(const QModelIndex& index, int role) const override; + int rowCount(const QModelIndex& parent) const override; + QHash roleNames() const override; + Q_INVOKABLE QVariant get(int row) const; + + private: + QList m_stems; +}; + +} // namespace qml +} // namespace mixxx diff --git a/src/sources/soundsourcestem.cpp b/src/sources/soundsourcestem.cpp index 488b02b6c94..b316b407d6a 100644 --- a/src/sources/soundsourcestem.cpp +++ b/src/sources/soundsourcestem.cpp @@ -1,9 +1,4 @@ #include "sources/soundsourcestem.h" - -#include -#include -#include - #include "sources/readaheadframebuffer.h" extern "C" { @@ -23,10 +18,6 @@ extern "C" { #define VERBOSE_DEBUG_LOG false #endif -#define ATOM_TYPE(value) \ - (uint32_t)(((uint8_t)(value)[0] << 24) | ((uint8_t)(value)[1] << 16) | \ - ((uint8_t)(value)[2] << 8) | (uint8_t)(value)[3]) - namespace mixxx { namespace { @@ -34,47 +25,9 @@ namespace { // STEM constants constexpr int kNumStreams = 5; constexpr int kRequiredStreamCount = kNumStreams - 1; // Stem count doesn't include the main mix -constexpr int kSupportedStemVersion = 1; const Logger kLogger("SoundSourceSTEM"); -const uint32_t kAtomHeaderSize = 8; // 4 bytes (unsigned integer) + 4 char -const uint32_t kStemManifestAtomPath[] = { - ATOM_TYPE("moov"), - ATOM_TYPE("udta"), - ATOM_TYPE("stem"), - 0 // This indicate end of the path -}; - -/// @brief Seek the reader the atom requested. -/// @param reader The IODevice to search MP4 atom in -/// @param path The list of atom to traverse in the tree, from top to bottom. -/// Must be null terminated -/// @param box_size The size of the box currently under the cursor to focus the -/// search in -/// @param pathIdx The level of the tree currently search (index of path) -/// @return the size of the box data if found, -1 otherwise -uint32_t seekTillAtom(QIODevice& reader, - const uint32_t path[], - uint32_t box_size = 0, - uint32_t pathIdx = 0) { - if (!path[pathIdx]) { - return box_size; - } - - uint32_t byteRead = 0; - char buffer[kAtomHeaderSize]; - while (!reader.atEnd() || (box_size && byteRead >= box_size)) { - reader.read(buffer, kAtomHeaderSize); - byteRead += box_size; - if (ATOM_TYPE(buffer + 4) == path[pathIdx]) { - return seekTillAtom(reader, path, ATOM_TYPE(buffer) - kAtomHeaderSize, pathIdx + 1); - } - reader.skip(ATOM_TYPE(buffer) - kAtomHeaderSize); - } - return -1; -} - } // anonymous namespace const QString SoundSourceProviderSTEM::kDisplayName = QStringLiteral("STEM"); @@ -338,39 +291,6 @@ SoundSource::OpenResult SoundSourceSTEM::tryOpen( << getLocalFileName(); return OpenResult::Failed; } - // Fetch the STEM manifest which contain stream details - auto file = QFile(getLocalFileName()); - if (!file.open(QIODeviceBase::ReadOnly)) { - kLogger.warning() - << "Failed to open input file" - << getLocalFileName(); - return OpenResult::Failed; - } - - uint32_t manifestSize; - if (!(manifestSize = seekTillAtom(file, kStemManifestAtomPath))) { - kLogger.warning() - << "Failed to find the steam manifest in the file" - << getLocalFileName(); - return OpenResult::Failed; - } - - auto jsonData = QJsonDocument::fromJson(file.read(manifestSize)); - VERIFY_OR_DEBUG_ASSERT(jsonData.isObject()) { - kLogger.warning() - << "Failed to extract the manifest data" - << getLocalFileName(); - return OpenResult::Failed; - }; - m_manifest = SoundSourceSTEM::Manifest::fromJson(jsonData.object()); - VERIFY_OR_DEBUG_ASSERT(m_manifest.isValid()) { - kLogger.warning() - << "Failed to parse the manifest data" - << getLocalFileName(); - return OpenResult::Failed; - }; - - file.close(); // Open input AVFormatContext* pavInputFormatContext = SoundSourceFFmpeg::openInputFile(getLocalFileName()); @@ -547,45 +467,4 @@ ReadableSampleFrames SoundSourceSTEM::readSampleFramesClamped( return read; } -bool SoundSourceSTEM::Manifest::isValid() const { - return m_version == kSupportedStemVersion && m_streams.size() == kRequiredStreamCount; -} - -// Static -SoundSourceSTEM::Manifest SoundSourceSTEM::Manifest::fromJson(const QJsonObject& json) { - Manifest ret; - auto stems = json.value("stems"); - if (!stems.isArray()) { - qDebug() << "Unexpected or missing stems value in STEM manifest"; - return ret; - } - auto stemArray = stems.toArray(); - ret.m_streams.reserve(stemArray.size()); - for (const auto& stemRef : stemArray) { - if (!stemRef.isObject()) { - qDebug() << "Unexpected or missing stems value in STEM manifest"; - return ret; - } - auto stem = stemRef.toObject(); - auto color = QColor(stem.value("color").toString()); - auto name = stem.value("name").toString(); - if (!color.isValid() || name.isEmpty()) { - qDebug() << "Unexpected or missing stem name or attribute in STEM manifest"; - return ret; - } - ret.m_streams.append(Manifest::Stream{ - color, - name, - }); - } - - auto version = json.value("version").toInteger(-1); - if (version <= 0) { - qDebug() << "Unexpected or missing version value in STEM manifest"; - return ret; - } - ret.m_version = version; - return ret; -} - } // namespace mixxx diff --git a/src/sources/soundsourcestem.h b/src/sources/soundsourcestem.h index 6b162f38afc..7fb1b71a1d2 100644 --- a/src/sources/soundsourcestem.h +++ b/src/sources/soundsourcestem.h @@ -1,9 +1,5 @@ #pragma once -#include -#include -#include - #include "sources/soundsourceffmpeg.h" #include "sources/soundsourceprovider.h" #include "util/samplebuffer.h" @@ -32,34 +28,9 @@ class SoundSourceSTEM : public SoundSource { public: explicit SoundSourceSTEM(const QUrl& url); - class Manifest { - public: - Manifest() - : m_streams(0), m_version() { - } - - bool isValid() const; - static Manifest fromJson(const QJsonObject& json); - - private: - struct Stream { - QColor color; - QString name; - }; - - Manifest(uint version, const QList& streams) - : m_streams(streams), m_version(version) { - } - - QList m_streams; - // TODO(XXX): store the DSP parameters for post processing effect - uint m_version; - }; - void close() override; private: - Manifest m_manifest; // Contains each stem source, or the main mix if opened in stereo mode std::vector> m_pStereoStreams; SampleBuffer m_buffer; diff --git a/src/track/steminfo.cpp b/src/track/steminfo.cpp new file mode 100644 index 00000000000..ff556c7cf81 --- /dev/null +++ b/src/track/steminfo.cpp @@ -0,0 +1,23 @@ +#include "track/steminfo.h" + +#include + +std::ostream& operator<<(std::ostream& os, const StemInfo& stemInfo) { + return os + << "StemInfo{" + << stemInfo.getLabel() + << ',' + << stemInfo.getColor().name() + << '}'; +} + +QDebug operator<<(QDebug dbg, const StemInfo& stemInfo) { + const QDebugStateSaver saver(dbg); + dbg = dbg.maybeSpace() << "StemInfo"; + return dbg.nospace() + << '{' + << stemInfo.getLabel() + << ',' + << stemInfo.getColor().name() + << '}'; +} diff --git a/src/track/steminfo.h b/src/track/steminfo.h new file mode 100644 index 00000000000..886073cfc14 --- /dev/null +++ b/src/track/steminfo.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +class StemInfo { + public: + StemInfo(const QString& label = QString(), const QColor& color = QColor()) + : m_label(label), m_color(color) { + } + StemInfo(const StemInfo&) = default; + + const QString& getLabel() const { + return m_label; + } + void setLabel(const QString& label) { + m_label = label; + } + + const QColor& getColor() const { + return m_color; + } + void setColor(const QColor& color) { + m_color = color; + } + + bool isValid() const { + return !m_label.isEmpty() && m_color.isValid(); + } + + private: + QString m_label; + QColor m_color; +}; +Q_DECLARE_METATYPE(StemInfo); + +std::ostream& operator<<(std::ostream& os, const StemInfo& stemInfo); + +QDebug operator<<(QDebug debug, const StemInfo& stemInfo); diff --git a/src/track/steminfoimporter.cpp b/src/track/steminfoimporter.cpp new file mode 100644 index 00000000000..df042a4c219 --- /dev/null +++ b/src/track/steminfoimporter.cpp @@ -0,0 +1,139 @@ +#include "track/steminfoimporter.h" + +#include +#include +#include +#include +#include + +#include "util/logger.h" + +#define ATOM_TYPE(value) \ + (uint32_t)(((uint8_t)(value)[0] << 24) | ((uint8_t)(value)[1] << 16) | \ + ((uint8_t)(value)[2] << 8) | (uint8_t)(value)[3]) + +namespace mixxx { + +namespace { + +const mixxx::Logger kLogger("StemInfoImporter"); +constexpr int kSupportedStemVersion = 1; +const QString kStemFileExtension = QStringLiteral(".stem.mp4"); + +const uint32_t kAtomHeaderSize = 8; // 4 bytes (unsigned integer) + 4 char +const uint32_t kStemManifestAtomPath[] = { + ATOM_TYPE("moov"), + ATOM_TYPE("udta"), + ATOM_TYPE("stem"), + 0 // This indicate end of the path +}; + +/// @brief Seek the reader the atom requested. +/// @param reader The IODevice to search MP4 atom in +/// @param path The list of atom to traverse in the tree, from top to bottom. +/// Must be null terminated +/// @param box_size The size of the box currently under the cursor to focus the +/// search in +/// @param pathIdx The level of the tree currently search (index of path) +/// @return the size of the box data if found, -1 otherwise +uint32_t seekTillAtom(QIODevice& reader, + const uint32_t path[], + uint32_t box_size = 0, + uint32_t pathIdx = 0) { + if (!path[pathIdx]) { + return box_size; + } + + uint32_t byteRead = 0; + char buffer[kAtomHeaderSize]; + while (!reader.atEnd() || (box_size && byteRead >= box_size)) { + reader.read(buffer, kAtomHeaderSize); + byteRead += box_size; + if (ATOM_TYPE(buffer + 4) == path[pathIdx]) { + return seekTillAtom(reader, path, ATOM_TYPE(buffer) - kAtomHeaderSize, pathIdx + 1); + } + reader.skip(ATOM_TYPE(buffer) - kAtomHeaderSize); + } + return -1; +} + +} // anonymous namespace + +// static +bool StemInfoImporter::isStemFile( + const QString& aFileName) { + return aFileName.endsWith(kStemFileExtension); +} + +QList StemInfoImporter::importStemInfos( + const QString& filePath) { + // Fetch the STEM manifest which contain stream details + auto file = QFile(filePath); + if (!file.open(QIODeviceBase::ReadOnly)) { + kLogger.warning() + << "Failed to open input file" + << filePath; + return {}; + } + + uint32_t manifestSize; + if (!(manifestSize = seekTillAtom(file, kStemManifestAtomPath))) { + kLogger.warning() + << "Failed to find the steam manifest in the file" + << filePath; + return {}; + } + + auto jsonData = QJsonDocument::fromJson(file.read(manifestSize)); + VERIFY_OR_DEBUG_ASSERT(jsonData.isObject()) { + kLogger.warning() + << "Failed to extract the manifest data" + << filePath; + return {}; + }; + + auto json = jsonData.object(); + + // Check the manifest version + auto version = json.value("version").toInteger(-1); + if (version <= 0) { + kLogger.debug() << "Unexpected or missing version value in STEM manifest"; + return {}; + } else if (version > kSupportedStemVersion) { + kLogger.debug() << "Unsupported stem version" << version << "but trying anyway"; + } + + // Extract stem metadata + auto stems = json.value("stems"); + if (!stems.isArray()) { + kLogger.debug() << "Unexpected or missing stems value in STEM manifest"; + return {}; + } + auto stemArray = stems.toArray(); + QList stemsList; + stemsList.reserve(stemArray.size()); + for (const auto& stemRef : stemArray) { + if (!stemRef.isObject()) { + kLogger.debug() << "Unexpected or missing stems value in STEM manifest"; + return {}; + } + auto stem = stemRef.toObject(); + auto color = QColor(stem.value("color").toString()); + auto name = stem.value("name").toString(); + if (!color.isValid() || name.isEmpty()) { + kLogger.debug() << "Unexpected or missing stem name or attribute in STEM manifest"; + return {}; + } + stemsList.append(std::move(StemInfo(name, color))); + } + + // Extract DSP information + // TODO(XXX) DSP only contains limit and a compressor, which aren't + // supported by Mixxx yet. parse and implement when supported + + file.close(); + + return stemsList; +} + +} // namespace mixxx diff --git a/src/track/steminfoimporter.h b/src/track/steminfoimporter.h new file mode 100644 index 00000000000..4ccae632b3c --- /dev/null +++ b/src/track/steminfoimporter.h @@ -0,0 +1,18 @@ +#pragma once + +#include "track/steminfo.h" + +namespace mixxx { + +/// Importer class for StemInfo objects that can correct timing offsets when the +/// signal info (channel number, sample rate, bitrate) is known. +class StemInfoImporter { + public: + static QList importStemInfos( + const QString& filePath); + + static bool isStemFile( + const QString& aFileName); +}; + +} // namespace mixxx diff --git a/src/track/track.cpp b/src/track/track.cpp index 659f37ce717..f1a1a4010ea 100644 --- a/src/track/track.cpp +++ b/src/track/track.cpp @@ -1296,6 +1296,21 @@ bool Track::importPendingCueInfosWhileLocked() { return setCuePointsWhileLocked(cuePoints); } +#ifdef __STEM__ +bool Track::setStemPointsWhileLocked(const QList& stemInfos) { + m_stemInfo = stemInfos; + return true; +} + +bool Track::importPendingStemInfosWhileLocked() { + const QList stemInfos = + mixxx::StemInfoImporter::importStemInfos( + getLocation()); + + return setStemPointsWhileLocked(stemInfos); +} +#endif + void Track::importPendingCueInfosMarkDirtyAndUnlock( QT_RECURSIVE_MUTEX_LOCKER* pLock) { DEBUG_ASSERT(pLock); @@ -1716,8 +1731,15 @@ void Track::updateStreamInfoFromSource( const bool importBeats = m_pBeatsImporterPending && !m_pBeatsImporterPending->isEmpty(); const bool importCueInfos = m_pCueInfoImporterPending && !m_pCueInfoImporterPending->isEmpty(); - - if (!importBeats && !importCueInfos) { +#ifdef __STEM__ + const bool importStemInfos = mixxx::StemInfoImporter::isStemFile(getLocation()); +#endif + + if (!importBeats && !importCueInfos +#ifdef __STEM__ + && !importStemInfos +#endif + ) { // Nothing more to do if (updated) { markDirtyAndUnlock(&locked); @@ -1742,7 +1764,20 @@ void Track::updateStreamInfoFromSource( cuesImported = importPendingCueInfosWhileLocked(); } - if (!beatsImported && !cuesImported) { +#ifdef __STEM__ + auto stemsImported = false; + if (importStemInfos) { + kLogger.debug() + << "Importing stem(s) info"; + stemsImported = importPendingStemInfosWhileLocked(); + } +#endif + + if (!beatsImported && !cuesImported +#ifdef __STEM__ + && !stemsImported +#endif + ) { return; } @@ -1755,6 +1790,11 @@ void Track::updateStreamInfoFromSource( if (cuesImported) { emit cuesUpdated(); } +#ifdef __STEM__ + if (stemsImported) { + emit stemsUpdated(); + } +#endif } QString Track::getGenre() const { diff --git a/src/track/track.h b/src/track/track.h index 17ecb259242..0c9a8346dad 100644 --- a/src/track/track.h +++ b/src/track/track.h @@ -9,6 +9,10 @@ #include "track/beats.h" #include "track/cue.h" #include "track/cueinfoimporter.h" +#ifdef __STEM__ +#include "track/steminfo.h" +#include "track/steminfoimporter.h" +#endif #include "track/track_decl.h" #include "track/trackrecord.h" #include "util/color/predefinedcolorpalettes.h" @@ -326,6 +330,15 @@ class Track : public QObject { void setCuePoints(const QList& cuePoints); +#ifdef __STEM__ + QList getStemInfo() const { + const QMutexLocker lock(&m_qMutex); + // lock thread-unsafe copy constructors of QList + return m_stemInfo; + } + // Setter is only available internally +#endif + enum class ImportStatus { Pending, Complete, @@ -441,6 +454,9 @@ class Track : public QObject { void colorUpdated(const mixxx::RgbColor::optional_t& color); void ratingUpdated(int rating); void cuesUpdated(); +#ifdef __STEM__ + void stemsUpdated(); +#endif void loopRemove(); void analyzed(); @@ -495,6 +511,15 @@ class Track : public QObject { /// caller guards this a lock. bool importPendingCueInfosWhileLocked(); + /// Sets stem info and returns a boolean to indicate if stems were updated. + /// Only supposed to be called while the caller guards this a lock. + bool setStemPointsWhileLocked(const QList& stemInfo); + + /// Imports pending stem info from a stemInfoImporter and returns a boolean to + /// indicate if stems were updated. Only supposed to be called while the + /// caller guards this a lock. + bool importPendingStemInfosWhileLocked(); + mixxx::Bpm getBpmWhileLocked() const; bool trySetBpmWhileLocked(mixxx::Bpm bpm); bool trySetBeatsWhileLocked( @@ -559,6 +584,11 @@ class Track : public QObject { // The list of cue points for the track QList m_cuePoints; +#ifdef __STEM__ + // The list of stem info + QList m_stemInfo; +#endif + // Storage for the track's beats mixxx::BeatsPointer m_pBeats;