diff --git a/CMakeLists.txt b/CMakeLists.txt index 72a7897de61..8f5793ae2fb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1066,6 +1066,8 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/preferences/dialog/dlgpreflibrarydlg.ui src/preferences/dialog/dlgprefmixer.cpp src/preferences/dialog/dlgprefmixerdlg.ui + src/preferences/dialog/dlgprefosc.cpp + src/preferences/dialog/dlgprefoscdlg.ui src/preferences/dialog/dlgprefrecord.cpp src/preferences/dialog/dlgprefrecorddlg.ui src/preferences/dialog/dlgprefreplaygain.cpp @@ -2556,6 +2558,42 @@ add_library(rekordbox_metadata STATIC EXCLUDE_FROM_ALL target_include_directories(rekordbox_metadata SYSTEM PUBLIC lib/rekordbox-metadata) target_link_libraries(mixxx-lib PRIVATE rekordbox_metadata) +IF(WIN32) + set(IpSystemTypePath src/osc/ip/win32) +# set(LIBS ${LIBS} Ws2_32 winmm) + ELSE(WIN32) + set(IpSystemTypePath src/osc/ip/posix) + ENDIF(WIN32) + +#eve osc +ADD_LIBRARY(oscpack + src/osc/ip/IpEndpointName.cpp + src/osc/ip/IpEndpointName.h + src/osc/ip/NetworkingUtils.h + ${IpSystemTypePath}/NetworkingUtils.cpp + src/osc/ip/PacketListener.h + src/osc/ip/TimerListener.h + src/osc/ip/UdpSocket.h + ${IpSystemTypePath}/UdpSocket.cpp + src/osc/osc/MessageMappingOscPacketListener.h + src/osc/osc/OscException.h + src/osc/osc/OscHostEndianness.h + src/osc/osc/OscOutboundPacketStream.cpp + src/osc/osc/OscOutboundPacketStream.h + src/osc/osc/OscPacketListener.h + src/osc/osc/OscPrintReceivedElements.cpp + src/osc/osc/OscPrintReceivedElements.h + src/osc/osc/OscReceivedElements.cpp + src/osc/osc/OscReceivedElements.h + src/osc/osc/OscTypes.cpp + src/osc/osc/OscTypes.h + ) +INCLUDE_DIRECTORIES(${CMAKE_SOURCE_DIR}) +#target_link_libraries(mixxx oscpack ${LIBS}) +target_include_directories(mixxx-lib SYSTEM PRIVATE ip) +target_include_directories(mixxx-lib SYSTEM PRIVATE osc) +target_link_libraries(mixxx-lib PRIVATE oscpack) + #silence "enumeration values not handled in switch" in generated code if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") target_compile_options(rekordbox_metadata PRIVATE -Wno-switch) @@ -3467,6 +3505,8 @@ if (STEM) src/sources/soundsourcestem.cpp src/track/steminfoimporter.cpp src/track/steminfo.cpp + src/widget/wstemlabel.cpp + src/widget/wtrackstemmenu.cpp ) if(QOPENGL) target_sources(mixxx-lib PRIVATE diff --git a/res/skins/LateNight/classic/buttons/btn__stem_controls_collapse.svg b/res/skins/LateNight/classic/buttons/btn__stem_controls_collapse.svg new file mode 100644 index 00000000000..f602b10c819 --- /dev/null +++ b/res/skins/LateNight/classic/buttons/btn__stem_controls_collapse.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/res/skins/LateNight/classic/buttons/btn__stem_controls_expand.svg b/res/skins/LateNight/classic/buttons/btn__stem_controls_expand.svg new file mode 100644 index 00000000000..049b7875b56 --- /dev/null +++ b/res/skins/LateNight/classic/buttons/btn__stem_controls_expand.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/res/skins/LateNight/classic/buttons/btn__stem_mute.svg b/res/skins/LateNight/classic/buttons/btn__stem_mute.svg new file mode 100644 index 00000000000..a335dbbf4ba --- /dev/null +++ b/res/skins/LateNight/classic/buttons/btn__stem_mute.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/res/skins/LateNight/palemoon/buttons/btn__stem_controls_collapse.svg b/res/skins/LateNight/palemoon/buttons/btn__stem_controls_collapse.svg new file mode 100644 index 00000000000..f602b10c819 --- /dev/null +++ b/res/skins/LateNight/palemoon/buttons/btn__stem_controls_collapse.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/res/skins/LateNight/palemoon/buttons/btn__stem_controls_expand.svg b/res/skins/LateNight/palemoon/buttons/btn__stem_controls_expand.svg new file mode 100644 index 00000000000..049b7875b56 --- /dev/null +++ b/res/skins/LateNight/palemoon/buttons/btn__stem_controls_expand.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/res/skins/LateNight/palemoon/buttons/btn__stem_mute.svg b/res/skins/LateNight/palemoon/buttons/btn__stem_mute.svg new file mode 100644 index 00000000000..5a99e4d21fa --- /dev/null +++ b/res/skins/LateNight/palemoon/buttons/btn__stem_mute.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/res/skins/LateNight/stem_channel.xml b/res/skins/LateNight/stem_channel.xml new file mode 100644 index 00000000000..3af404857aa --- /dev/null +++ b/res/skins/LateNight/stem_channel.xml @@ -0,0 +1,125 @@ + diff --git a/res/skins/LateNight/style.qss b/res/skins/LateNight/style.qss index 20f09b6729f..6cccc0ca1fe 100644 --- a/res/skins/LateNight/style.qss +++ b/res/skins/LateNight/style.qss @@ -402,6 +402,13 @@ WOverview QLabel { +/************** Waveforms *****************************************************/ + +#StemChannel_ControlContainer { + margin: 0px 3px 0px 2px; +} + + /************** Decks *********************************************************/ #DeckRows12345 { @@ -660,8 +667,14 @@ WEffectChainPresetSelector { padding: 0px -1px 0px -5px; margin: 0px; text-align: right; + } + WEffectChainPresetSelector#StemQuickEffectSelector { + padding: 0px -5px 0px 2px; + margin: 0px; + text-align: left; } - WEffectChainPresetSelector#QuickEffectSelectorRight::drop-down { + WEffectChainPresetSelector#QuickEffectSelectorRight::drop-down, + WEffectChainPresetSelector#StemQuickEffectSelector::drop-down { subcontrol-origin: margin; subcontrol-position: right center; } diff --git a/res/skins/LateNight/style_classic.qss b/res/skins/LateNight/style_classic.qss index 2172ef3585b..e62dfdfeab8 100644 --- a/res/skins/LateNight/style_classic.qss +++ b/res/skins/LateNight/style_classic.qss @@ -195,6 +195,16 @@ WSearchLineEdit, border: 1px solid red; } +#BeatgridControls, +#StemControls { + border-top: 1px solid #222; + border-left: 1px solid #222; + border-bottom: 1px solid #111; + border-right: 1px solid #111; + border-radius: 2px; + background-color: #171717; + } + #VuMainCover { background-color: rgba(21, 21, 21, 150); } @@ -1371,6 +1381,7 @@ WEffectSelector[highlight="1"] { /* } #BeatgridControls WPushButton, WPushButton#BeatgridControlsToggle, +#StemControls WPushButton, WPushButton#StemControlsToggle, #DeckRow_5_LoopCuesTransport WPushButton, WPushButton#PlayDeck, WPushButton#PreviewIndicator, @@ -1420,6 +1431,7 @@ WPushButton#LibExpand, WPushButton#CueDeck[displayValue="0"], WPushButton#PlayIndicator[value="0"], WPushButton#LoopActivate[displayValue="0"], +WPushButton#StemMuteButton[displayValue="0"], #FxAssignButtons WPushButton[displayValue="0"], #VinylControls WPushButton[displayValue="0"], #KeyControls WPushButton[displayValue="0"], @@ -1483,6 +1495,10 @@ QPushButton#pushButtonRecording:checked { border-radius: 0px; } +/* Orange for Stem Ch Mute buttons */ +WPushButton#StemMuteButton{ + background-color: #db7700; +} /* Green for Fx buttons: QuickEffect + Fx 1/2 */ #FxUnit1 #FxToggleButton[displayValue="1"], #FxUnit2 #FxToggleButton[displayValue="1"], @@ -1626,6 +1642,7 @@ WPushButton#FxExpand[displayValue="0"], WPushButton#FxExpandOverlay[displayValue="0"], WPushButton#SamplerExpand[displayValue="0"], #BeatgridControlsToggle, +#StemControlsToggle, #SamplerControlsMini WPushButton, #RecDot, /* transparent buttons, 0-4 (max button state we have currently) */ @@ -1815,6 +1832,10 @@ WPushButton#BpmLockToggle[value="1"] { image: url(skins:LateNight/classic/buttons/btn__pfl.svg) no-repeat center center; } +#StemMuteButton[displayValue="0"] { + image: url(skins:LateNight/classic/buttons/btn__stem_mute.svg) no-repeat center center; +} + #QuickEffectButton[displayValue="0"] { image: url(skins:LateNight/classic/buttons/btn__star.svg) no-repeat center center; } @@ -1940,6 +1961,14 @@ WPushButton#BpmLockToggle[value="1"] { #BeatgridControlsToggle[displayValue="1"] { image: url(skins:LateNight/classic/buttons/btn__beatgrid_controls_collapse.svg) no-repeat center center; } + +#StemControlsToggle[displayValue="0"] { + image: url(skins:LateNight/classic/buttons/btn__stem_controls_expand.svg) no-repeat center center; + } + #StemControlsToggle[displayValue="1"] { + image: url(skins:LateNight/classic/buttons/btn__stem_controls_collapse.svg) no-repeat center center; + } + #BpmLockToggle[value="1"] { image: url(skins:LateNight/classic/buttons/btn__bpm_locked.svg); } diff --git a/res/skins/LateNight/style_palemoon.qss b/res/skins/LateNight/style_palemoon.qss index 733b14f4b45..ff4afb38c57 100644 --- a/res/skins/LateNight/style_palemoon.qss +++ b/res/skins/LateNight/style_palemoon.qss @@ -241,6 +241,17 @@ WSearchLineEdit { border-bottom: 1px solid #0c0c0c; } +#BeatgridControls, +#StemControls { + border-top: 1px solid #1c1c1c; + border-left: 1px solid #1c1c1c; + border-bottom: 1px solid #020202; + border-right: 1px solid #111; + border-radius: 1px; + background-color: #151517; + } + + #FxParametersFocusBg { border: 1px solid #257B82; background-color: rgba(0,0,1,50); @@ -900,7 +911,8 @@ WEffectSelector:!editable:on { names start getting cut off. Adding explicit padding improves this. */ padding: 3px 0px 1px 5px; margin: 0px; -} + } + #fadeModeCombobox:!editable, #fadeModeCombobox:!editable:on { min-height: 17px; @@ -1536,6 +1548,7 @@ WEffectSelector:!editable, } #BeatgridControls WPushButton, WPushButton#BeatgridControlsToggle, +#StemControls WPushButton, WPushButton#StemControlsToggle, #DeckRow_5_LoopCuesTransport WPushButton, #RateControls WPushButton, WPushButton#PlayDeck, @@ -1587,6 +1600,7 @@ WPushButton#CueDeck[displayValue="0"], WPushButton#LoopActivate[displayValue="0"], #KeyControls WPushButton[displayValue="0"], #EQKillButtonBox WPushButton[displayValue="0"], +WPushButton#StemMuteButton[displayValue="0"], WPushButton#QuickEffectButton[displayValue="0"], WPushButton#FxToggleButton[displayValue="0"], #MicAuxUnit WPushButton[displayValue="0"], @@ -1672,6 +1686,7 @@ WPushButton#Reverse[pressed="true"], #MicTalk[value="1"], #AuxPlay[value="1"], #MicDucking[value="1"], #MicDucking[value="2"], +WPushButton#StemMuteButton, #PassthroughButton[displayValue="1"], #BroadcastButton[displayValue="4"], /* warning */ QPushButton#pushButtonAutoDJ:checked, @@ -1849,6 +1864,7 @@ QPushButton#pushButtonRepeatPlaylist:checked { /* Special flat/invisible buttons */ #BeatgridControlsToggle, +#StemControlsToggle, WPushButton#PlayDeck[displayValue="0"], WPushButton#PlayDeckMini[displayValue="0"], WPushButton#PlaySampler[displayValue="0"], @@ -2218,6 +2234,10 @@ WPushButton#PlayDeck[value="0"] { image: url(skins:LateNight/palemoon/buttons/btn__pfl_active.svg) no-repeat center center; } +#StemMuteButton[displayValue="0"] { + image: url(skins:LateNight/palemoon/buttons/btn__stem_mute.svg) no-repeat center center; +} + #QuickEffectButton[displayValue="0"] { image: url(skins:LateNight/palemoon/buttons/btn__star.svg) no-repeat center center; } @@ -2352,6 +2372,13 @@ WPushButton#PlayDeck[value="0"] { image: url(skins:LateNight/palemoon/buttons/btn__keylock_active_34.svg) no-repeat center center; } +#StemControlsToggle[displayValue="0"] { + image: url(skins:LateNight/palemoon/buttons/btn__stem_controls_expand.svg) no-repeat center center; + } + #StemControlsToggle[displayValue="1"] { + image: url(skins:LateNight/palemoon/buttons/btn__stem_controls_collapse.svg) no-repeat center center; + } + #BeatgridControlsToggle[displayValue="0"] { image: url(skins:LateNight/palemoon/buttons/btn__beatgrid_controls_expand.svg) no-repeat center center; } diff --git a/res/skins/LateNight/waveform.xml b/res/skins/LateNight/waveform.xml index 2af82f4188b..131260ec97b 100644 --- a/res/skins/LateNight/waveform.xml +++ b/res/skins/LateNight/waveform.xml @@ -5,12 +5,123 @@ BeatEditCover ,bpmlock - + regular + regular + + + + + + + + Waveform horizontal me,me - + + StemControls + vertical + max,me + + + 0me,1me + + + + + horizontal + f,f + + + + 1f,0min + + + + + vertical + + + + horizontal + + + 1min,2me + + + + 1min,3f + + + + + + horizontal + + + 1min,2me + + + + 1min,3f + + + + + + horizontal + + + 1min,2me + + + + 1min,3f + + + + + + horizontal + + + 1min,2me + + + + 1min,3f + + + + + + + + + + + + + 1me,1me + + + + [Skin],show_stem_controls + visible + + + + + WaveformBox horizontal me,me diff --git a/res/skins/LateNight/waveforms_container.xml b/res/skins/LateNight/waveforms_container.xml index 86800623a93..324858a0379 100644 --- a/res/skins/LateNight/waveforms_container.xml +++ b/res/skins/LateNight/waveforms_container.xml @@ -4,19 +4,26 @@ horizontal me,min - - + WaveformsContainer horizontal me,min - + + + vertical 100me,40me - + horizontal me,me diff --git a/src/20240918 BU mixxxmainwindowy.cpp b/src/20240918 BU mixxxmainwindowy.cpp new file mode 100644 index 00000000000..d419e7bdc4f --- /dev/null +++ b/src/20240918 BU mixxxmainwindowy.cpp @@ -0,0 +1,1482 @@ +#include "mixxxmainwindow.h" + +#include +#include +#include +#include +#include +#include + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +#include +#endif + +#ifdef __LINUX__ +#include +#endif + +#ifdef MIXXX_USE_QOPENGL +#include "widget/tooltipqopengl.h" +#include "widget/winitialglwidget.h" +#endif + +#include "controllers/keyboard/keyboardeventfilter.h" +#include "coreservices.h" +#include "defs_urls.h" +#include "dialog/dlgabout.h" +#include "dialog/dlgdevelopertools.h" +#include "dialog/dlgkeywheel.h" +#include "moc_mixxxmainwindow.cpp" +#include "preferences/dialog/dlgpreferences.h" +#ifdef __BROADCAST__ +#include "broadcast/broadcastmanager.h" +#endif +#include "control/controlindicatortimer.h" +#include "library/library.h" +#include "library/library_prefs.h" +#ifdef __ENGINEPRIME__ +#include "library/export/libraryexporter.h" +#endif +#include "library/trackcollectionmanager.h" +#include "mixer/playerinfo.h" +#include "mixer/playermanager.h" +#include "recording/recordingmanager.h" +#include "skin/legacy/launchimage.h" +#include "skin/skinloader.h" +#include "soundio/soundmanager.h" +#include "sources/soundsourceproxy.h" +#include "track/track.h" +#include "util/debug.h" +#include "util/desktophelper.h" +#include "util/sandbox.h" +#include "util/timer.h" +#include "util/versionstore.h" +#include "waveform/guitick.h" +#include "waveform/sharedglcontext.h" +#include "waveform/visualsmanager.h" +#include "waveform/waveformwidgetfactory.h" +#include "widget/wglwidget.h" +#include "widget/wmainmenubar.h" + +#ifdef __VINYLCONTROL__ +#include "vinylcontrol/vinylcontrolmanager.h" +#endif + +// EveOSC +#include "osc/oscfunctions.h" +#include "osc/oscreceiver.cpp" +// EveOSC + +namespace { +#ifdef __LINUX__ +// Detect if the desktop supports a global menu to decide whether we need to rebuild +// and reconnect the menu bar when switching to/from fullscreen mode. +// Compared to QMenuBar::isNativeMenuBar() (requires a set menu bar) and +// Qt::AA_DontUseNativeMenuBar, which may both change, this is way more reliable +// since it's rather unlikely that the Appmenu.Registrar service is unloaded/stopped +// while Mixxx is running. +// This is a reimplementation of QGenericUnixTheme > checkDBusGlobalMenuAvailable() +inline bool supportsGlobalMenu() { +#ifndef QT_NO_DBUS + QDBusConnection conn = QDBusConnection::sessionBus(); + if (const auto* pIface = conn.interface()) { + return pIface->isServiceRegistered("com.canonical.AppMenu.Registrar"); + } +#endif + return false; +} +#endif + +const ConfigKey kHideMenuBarConfigKey = ConfigKey("[Config]", "hide_menubar"); +const ConfigKey kMenuBarHintConfigKey = ConfigKey("[Config]", "show_menubar_hint"); +} // namespace + +MixxxMainWindow::MixxxMainWindow(std::shared_ptr pCoreServices) + : m_pCoreServices(pCoreServices), + m_pCentralWidget(nullptr), + m_pLaunchImage(nullptr), +#ifndef __APPLE__ + m_prevState(Qt::WindowNoState), +#endif + m_pGuiTick(nullptr), +#ifdef __LINUX__ + m_supportsGlobalMenuBar(supportsGlobalMenu()), +#endif + m_pDeveloperToolsDlg(nullptr), + m_pPrefDlg(nullptr), + m_toolTipsCfg(mixxx::preferences::Tooltips::On) { + DEBUG_ASSERT(pCoreServices); + // These depend on the settings +#ifdef __LINUX__ + // If the desktop features a global menubar and we'll go fullscreen during + // startup, set Qt::AA_DontUseNativeMenuBar so the menubar is placed in the + // window like it's done in slotViewFullScreen(). On other desktops this + // attribute has no effect. This is a safe alternative to setNativeMenuBar() + // which can cause a crash when using menu shortcuts like Alt+F after resetting + // the menubar. See https://github.com/mixxxdj/mixxx/issues/11320 + if (m_supportsGlobalMenuBar) { + bool fullscreenPref = m_pCoreServices->getSettings()->getValue( + ConfigKey("[Config]", "StartInFullscreen")); + QApplication::setAttribute( + Qt::AA_DontUseNativeMenuBar, + CmdlineArgs::Instance().getStartInFullscreen() || fullscreenPref); + } +#endif // __LINUX__ + createMenuBar(); + m_pMenuBar->hide(); + + initializeWindow(); + + // Show launch image immediately so the user knows Mixxx is starting + m_pSkinLoader = std::make_unique(m_pCoreServices->getSettings()); + m_pLaunchImage = m_pSkinLoader->loadLaunchImage(this); + m_pCentralWidget = (QWidget*)m_pLaunchImage; + setCentralWidget(m_pCentralWidget); + + show(); + + m_pGuiTick = new GuiTick(); + m_pVisualsManager = new VisualsManager(); + // EveOSC + oscEnable(); + // EveOSC +} + +#ifdef MIXXX_USE_QOPENGL +void MixxxMainWindow::initializeQOpenGL() { +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + // Qt 6 will nno longer crash if no GL is available and + // QGLFormat::hasOpenGL() has been removed. + if (!CmdlineArgs::Instance().getSafeMode() && QGLFormat::hasOpenGL()) { +#else + if (!CmdlineArgs::Instance().getSafeMode()) { +#endif + QOpenGLContext context; + context.setFormat(WaveformWidgetFactory::getSurfaceFormat(m_pCoreServices->getSettings())); + if (context.create()) { + // This widget and its QOpenGLWindow will be used to query QOpenGL + // information (version, driver, etc) in WaveformWidgetFactory. + // The "SharedGLContext" terminology here doesn't really apply, + // but allows us to take advantage of the existing classes. + WInitialGLWidget* widget = new WInitialGLWidget(this); + widget->setGeometry(QRect(0, 0, 3, 3)); + SharedGLContext::setWidget(widget); + // When the widget's QOpenGLWindow has been initialized, we continue + // with the actual initialization + connect(widget, &WInitialGLWidget::onInitialized, this, &MixxxMainWindow::initialize); + widget->show(); + return; + } + qDebug() << "QOpenGLContext::create() failed"; + } + qInfo() << "Initializing without OpenGL"; + initialize(); +} +#endif + +void MixxxMainWindow::initialize() { + qWarning() << " $ initialize"; + m_pCoreServices->getControlIndicatorTimer()->setLegacyVsyncEnabled(true); + + UserSettingsPointer pConfig = m_pCoreServices->getSettings(); + + // Set the visibility of tooltips, default "1" = ON + m_toolTipsCfg = pConfig->getValue( + ConfigKey("[Controls]", "Tooltips"), + mixxx::preferences::Tooltips::On); +#ifdef MIXXX_USE_QOPENGL + ToolTipQOpenGL::singleton().setActive( + m_toolTipsCfg == mixxx::preferences::Tooltips::On); +#endif + +#ifdef __ENGINEPRIME__ + // Initialise library exporter + // This has to be done before switching to fullscreen + m_pLibraryExporter = m_pCoreServices->getLibrary()->makeLibraryExporter(this); + connect(m_pCoreServices->getLibrary().get(), + &Library::exportLibrary, + m_pLibraryExporter.get(), + &mixxx::LibraryExporter::slotRequestExport); + connect(m_pCoreServices->getLibrary().get(), + &Library::exportCrate, + m_pLibraryExporter.get(), + &mixxx::LibraryExporter::slotRequestExportWithInitialCrate); +#endif + + // Turn on fullscreen mode + // if we were told to start in fullscreen mode on the command-line + // or if the user chose to always start in fullscreen mode. + // The Fullscreen menu item is refreshed in connectMenuBar() + bool fullscreenPref = m_pCoreServices->getSettings()->getValue( + ConfigKey("[Config]", "StartInFullscreen")); + if ((CmdlineArgs::Instance().getStartInFullscreen() || fullscreenPref) && + // could be we're fullscreen already after setGeomtery(previousGeometry) + !isFullScreen()) { + qWarning() << " init: go fullscreen"; + showFullScreen(); + } + + initializationProgressUpdate(65, tr("skin")); + + // Install an event filter to catch certain QT events, such as tooltips. + // This allows us to turn off tooltips. + installEventFilter(m_pCoreServices->getKeyboardEventFilter().get()); + + auto pPlayerManager = m_pCoreServices->getPlayerManager(); + DEBUG_ASSERT(pPlayerManager); + const QStringList visualGroups = pPlayerManager->getVisualPlayerGroups(); + for (const QString& group : visualGroups) { + m_pVisualsManager->addDeck(group); + } + connect(pPlayerManager.get(), + &PlayerManager::numberOfDecksChanged, + this, + [this](int decks) { + for (int i = 0; i < decks; ++i) { + QString group = PlayerManager::groupForDeck(i); + m_pVisualsManager->addDeckIfNotExist(group); + } + }); + connect(pPlayerManager.get(), + &PlayerManager::numberOfSamplersChanged, + this, + [this](int decks) { + for (int i = 0; i < decks; ++i) { + QString group = PlayerManager::groupForSampler(i); + m_pVisualsManager->addDeckIfNotExist(group); + } + }); + +#ifndef MIXXX_USE_QOPENGL + // Before creating the first skin we need to create a QGLWidget so that all + // the QGLWidget's we create can use it as a shared QGLContext. + if (!CmdlineArgs::Instance().getSafeMode() && QGLFormat::hasOpenGL()) { + QGLFormat glFormat; + glFormat.setDirectRendering(true); + glFormat.setDoubleBuffer(true); + glFormat.setDepth(false); + // Disable waiting for vertical Sync + // This can be enabled when using a single Threads for each QGLContext + // Setting 1 causes QGLContext::swapBuffer to sleep until the next VSync +#if defined(__APPLE__) + // On OS X, syncing to vsync has good performance FPS-wise and + // eliminates tearing. + glFormat.setSwapInterval(1); +#else + // Otherwise, turn VSync off because it could cause horrible FPS on + // Linux. + // TODO(XXX): Make this configurable. + // TODO(XXX): What should we do on Windows? + glFormat.setSwapInterval(0); +#endif + glFormat.setRgba(true); + QGLFormat::setDefaultFormat(glFormat); + + WGLWidget* pContextWidget = new WGLWidget(this); + pContextWidget->setGeometry(QRect(0, 0, 3, 3)); + pContextWidget->hide(); + SharedGLContext::setWidget(pContextWidget); + } +#endif + + WaveformWidgetFactory::createInstance(); // takes a long time + WaveformWidgetFactory::instance()->setConfig(m_pCoreServices->getSettings()); + WaveformWidgetFactory::instance()->startVSync(m_pGuiTick, m_pVisualsManager); + + connect(this, + &MixxxMainWindow::skinLoaded, + m_pCoreServices->getLibrary().get(), + &Library::onSkinLoadFinished); + + connect(this, + &MixxxMainWindow::skinLoaded, + WaveformWidgetFactory::instance(), + &WaveformWidgetFactory::slotSkinLoaded); + + // Initialize preference dialog + m_pPrefDlg = new DlgPreferences( + m_pCoreServices->getScreensaverManager(), + m_pSkinLoader, + m_pCoreServices->getSoundManager(), + m_pCoreServices->getControllerManager(), + m_pCoreServices->getVinylControlManager(), + m_pCoreServices->getEffectsManager(), + m_pCoreServices->getSettingsManager(), + m_pCoreServices->getLibrary()); + m_pPrefDlg->setWindowIcon(QIcon(MIXXX_ICON_PATH)); + m_pPrefDlg->setHidden(true); + connect(m_pPrefDlg, + &DlgPreferences::tooltipModeChanged, + this, + &MixxxMainWindow::slotTooltipModeChanged); + connect(m_pPrefDlg, + &DlgPreferences::reloadUserInterface, + this, + &MixxxMainWindow::rebootMixxxView, + Qt::DirectConnection); +#ifndef __APPLE__ + connect(m_pPrefDlg, + &DlgPreferences::menuBarAutoHideChanged, + this, + &MixxxMainWindow::slotUpdateMenuBarAltKeyConnection, + Qt::DirectConnection); +#endif + + // Connect signals to the menubar. Should be done before emit skinLoaded. + connectMenuBar(); + + QWidget* oldWidget = m_pCentralWidget; + + tryParseAndSetDefaultStyleSheet(); + + if (!loadConfiguredSkin()) { + reportCriticalErrorAndQuit( + "default skin cannot be loaded - see mixxx trace for more information"); + m_pCentralWidget = oldWidget; + // TODO (XXX) add dialog to warn user and launch skin choice page + } else { + m_pMenuBar->setStyleSheet(m_pCentralWidget->styleSheet()); + } + + // Check direct rendering and warn user if they don't have it + if (!CmdlineArgs::Instance().getSafeMode()) { + checkDirectRendering(); + } + + // Sound hardware setup + // Try to open configured devices. If that fails, display dialogs + // that allow to either retry, reconfigure devices or exit. + bool retryClicked; + do { + retryClicked = false; + SoundDeviceStatus result = m_pCoreServices->getSoundManager()->setupDevices(); + if (result == SoundDeviceStatus::ErrorDeviceCount || + result == SoundDeviceStatus::ErrorExcessiveOutputChannel) { + if (soundDeviceBusyDlg(&retryClicked) != QDialog::Accepted) { + exit(0); + } + } else if (result != SoundDeviceStatus::Ok) { + if (soundDeviceErrorMsgDlg(result, &retryClicked) != + QDialog::Accepted) { + exit(0); + } + } + } while (retryClicked); + + // Test for at least one output device. If none, display another dialog + // that says "mixxx will barely work with no outs". + // In case of persisting errors, the user has already received a message + // above. So we can just check the output count here. + while (m_pCoreServices->getSoundManager()->getConfig().getOutputs().count() == 0) { + // Exit when we press the Exit button in the noSoundDlg dialog + // only call it if result != OK + bool continueClicked = false; + if (noOutputDlg(&continueClicked) != QDialog::Accepted) { + exit(0); + } + if (continueClicked) { + break; + } + } + + // The user has either reconfigured devices or accepted no outputs, + // so it's now safe to write the new config to disk. + m_pCoreServices->getSoundManager()->getConfig().writeToDisk(); + + // this has to be after the OpenGL widgets are created or depending on a + // million different variables the first waveform may be horribly + // corrupted. See bug 521509 -- bkgood ?? -- vrince + setCentralWidget(m_pCentralWidget); + +#ifndef __APPLE__ + // Ask for permission to auto-hide the menu bar if applicable. +#ifdef __LINUX__ + // This makes no sense when starting in windowed mode with a global menu, + // we'll ask when going fullscreen. + if (!m_supportsGlobalMenuBar || isFullScreen()) { + alwaysHideMenuBarDlg(); + slotUpdateMenuBarAltKeyConnection(); + } +#else + alwaysHideMenuBarDlg(); + slotUpdateMenuBarAltKeyConnection(); +#endif +#endif + + // Show the menubar after the launch image is replaced by the skin widget, + // otherwise it would shift the launch image shortly before the skin is visible. + m_pMenuBar->show(); + + // The launch image widget is automatically disposed, but we still have a + // pointer to it. + m_pLaunchImage = nullptr; + + connect(pPlayerManager.get(), + &PlayerManager::noMicrophoneInputConfigured, + this, + &MixxxMainWindow::slotNoMicrophoneInputConfigured); + connect(pPlayerManager.get(), + &PlayerManager::noAuxiliaryInputConfigured, + this, + &MixxxMainWindow::slotNoAuxiliaryInputConfigured); + connect(pPlayerManager.get(), + &PlayerManager::noDeckPassthroughInputConfigured, + this, + &MixxxMainWindow::slotNoDeckPassthroughInputConfigured); + connect(pPlayerManager.get(), + &PlayerManager::noVinylControlInputConfigured, + this, + &MixxxMainWindow::slotNoVinylControlInputConfigured); + + connect(&PlayerInfo::instance(), + &PlayerInfo::currentPlayingTrackChanged, + this, + &MixxxMainWindow::slotUpdateWindowTitle); + + // Start Auto DJ if the cmdline arg is passed. + if (CmdlineArgs::Instance().getStartAutoDJ()) { + qDebug("Enabling Auto DJ from CLI flag."); + ControlObject::set(ConfigKey("[AutoDJ]", "enabled"), 1.0); + } +} + +MixxxMainWindow::~MixxxMainWindow() { + Timer t("~MixxxMainWindow"); + t.start(); + + // Save the current window state (position, maximized, etc) + // Note(ronso0): Unfortunately saveGeometry() also stores the fullscreen state. + // On next start restoreGeometry would enable fullscreen mode even though that + // might not be requested (no '--fullscreen' command line arg and + // [Config],StartInFullscreen is '0'. + // https://github.com/mixxxdj/mixxx/issues/10005 + // So let's quit fullscreen if StartInFullscreen is not checked in Preferences. + bool fullscreenPref = m_pCoreServices->getSettings()->getValue( + ConfigKey("[Config]", "StartInFullscreen")); + if (isFullScreen() && !fullscreenPref) { + // Simply maximize the window so we can store a geometry that fits the screen. + // Don't call slotViewFullScreen(false) (calls showNormal()) because that + // can make the main window incl. window decoration too large for the screen. +#ifndef __APPLE__ + // Before, store the expected window state so eventFilter() will ignore + // the following QWindowChangeEvent and not recreate & re-sync the menu bar. + m_prevState = Qt::WindowMaximized; +#endif + showMaximized(); + } + m_pCoreServices->getSettings()->set(ConfigKey("[MainWindow]", "geometry"), + QString(saveGeometry().toBase64())); + m_pCoreServices->getSettings()->set(ConfigKey("[MainWindow]", "state"), + QString(saveState().toBase64())); + + // GUI depends on KeyboardEventFilter, PlayerManager, Library + qDebug() << t.elapsed(false).debugMillisWithUnit() << "deleting skin"; + m_pCentralWidget = nullptr; + QPointer pSkin(centralWidget()); + setCentralWidget(nullptr); + if (!pSkin.isNull()) { + QCoreApplication::sendPostedEvents(pSkin, QEvent::DeferredDelete); + } + // Our central widget is now deleted. + VERIFY_OR_DEBUG_ASSERT(pSkin.isNull()) { + qWarning() << "Central widget was not deleted by our sendPostedEvents trick."; + } + + // Delete Controls created by skins + qDeleteAll(m_skinCreatedControls); + m_skinCreatedControls.clear(); + + // TODO() Verify if this comment still applies: + // WMainMenuBar holds references to controls so we need to delete it + // before MixxxMainWindow is destroyed. QMainWindow calls deleteLater() in + // setMenuBar() but we need to delete it now so we can ask for + // DeferredDelete events to be processed for it. Once Mixxx shutdown lives + // outside of MixxxMainWindow the parent relationship will directly destroy + // the WMainMenuBar and this will no longer be a problem. + qDebug() << t.elapsed(false).debugMillisWithUnit() << "deleting menubar"; + + QPointer pMenuBar = m_pMenuBar.toWeakRef(); + DEBUG_ASSERT(menuBar() == m_pMenuBar.get()); + // We need to reset the parented pointer here that it does not become a + // dangling pointer after the object has been deleted. + m_pMenuBar = nullptr; + setMenuBar(nullptr); + if (!pMenuBar.isNull()) { + QCoreApplication::sendPostedEvents(pMenuBar, QEvent::DeferredDelete); + } + // Our main menu is now deleted. + VERIFY_OR_DEBUG_ASSERT(pMenuBar.isNull()) { + qWarning() << "WMainMenuBar was not deleted by our sendPostedEvents trick."; + } + + qDebug() << t.elapsed(false).debugMillisWithUnit() << "deleting DeveloperToolsDlg"; + delete m_pDeveloperToolsDlg; + +#ifdef __ENGINEPRIME__ + qDebug() << t.elapsed(false).debugMillisWithUnit() << "deleting LibraryExporter"; + m_pLibraryExporter.reset(); +#endif + + qDebug() << t.elapsed(false).debugMillisWithUnit() << "deleting DlgPreferences"; + delete m_pPrefDlg; + + m_pCoreServices->getControlIndicatorTimer()->setLegacyVsyncEnabled(false); + + WaveformWidgetFactory::destroy(); + + delete m_pGuiTick; + delete m_pVisualsManager; +} + +void MixxxMainWindow::initializeWindow() { + qWarning() << " $ initializeWindow"; + // be sure createMenuBar() is called first + DEBUG_ASSERT(m_pMenuBar); + + QPalette Pal(palette()); + // safe default QMenuBar background + QColor MenuBarBackground(m_pMenuBar->palette().color(QPalette::Window)); + Pal.setColor(QPalette::Window, QColor(0x202020)); + setAutoFillBackground(true); + setPalette(Pal); + // restore default QMenuBar background + Pal.setColor(QPalette::Window, MenuBarBackground); + m_pMenuBar->setPalette(Pal); + + // Restore the current window state (position, maximized, etc). + // This will also restore fullscreen and thereby create a seamless + // start if we did shut down while in fullscreen mode and with + // [Config],StartInFullscreen = 1 + // (slotViewFullScreen(true) in initialize() is a no-op then) + restoreGeometry(QByteArray::fromBase64( + m_pCoreServices->getSettings() + ->getValueString(ConfigKey("[MainWindow]", "geometry")) + .toUtf8())); + restoreState(QByteArray::fromBase64( + m_pCoreServices->getSettings() + ->getValueString(ConfigKey("[MainWindow]", "state")) + .toUtf8())); + + setWindowIcon(QIcon(MIXXX_ICON_PATH)); + slotUpdateWindowTitle(TrackPointer()); +} + +#ifndef __APPLE__ +void MixxxMainWindow::alwaysHideMenuBarDlg() { + // Don't show the dialog if the user unchecked "Ask me again" + if (!m_pCoreServices->getSettings()->getValue( + kMenuBarHintConfigKey, true)) { + return; + } + QString title = tr("Allow Mixxx to hide the menu bar?"); + //: Always show the menu bar? + QString hideBtnLabel = tr("Hide"); + QString showBtnLabel = tr("Always show"); + //: Keep formatting tags (bold text) and
(linebreak). + //: %1 is the placeholder for the 'Always show' button label + QString desc = tr( + "The Mixxx menu bar is hidden and can be toggled with a single press " + "of the Alt key.

" + "Click %1 to agree.

" + "Click %2 to disable that, for example if you don't use Mixxx " + "with a keyboard.

" + "You can change this setting any time in Preferences -> Interface." + "
") // line break for some extra margin to the checkbox + .arg(hideBtnLabel, showBtnLabel); + + QMessageBox msg; + msg.setIcon(QMessageBox::Question); + msg.setWindowTitle(title); + msg.setText(desc); + QCheckBox askAgainCheckBox; + askAgainCheckBox.setText(tr("Ask me again")); + askAgainCheckBox.setCheckState(Qt::Checked); + msg.setCheckBox(&askAgainCheckBox); + QPushButton* pHideBtn = msg.addButton(hideBtnLabel, QMessageBox::AcceptRole); + QPushButton* pShowBtn = msg.addButton(showBtnLabel, QMessageBox::RejectRole); + msg.setDefaultButton(pShowBtn); + msg.exec(); + + m_pCoreServices->getSettings()->setValue( + kMenuBarHintConfigKey, + askAgainCheckBox.checkState() == Qt::Checked ? 1 : 0); + + m_pCoreServices->getSettings()->setValue( + kHideMenuBarConfigKey, + msg.clickedButton() == pHideBtn ? 1 : 0); +} +#endif + +QDialog::DialogCode MixxxMainWindow::soundDeviceErrorDlg( + const QString& title, const QString& text, bool* retryClicked) { + QMessageBox msgBox; + msgBox.setIcon(QMessageBox::Warning); + msgBox.setWindowTitle(title); + msgBox.setText(text); + + QPushButton* retryButton = + msgBox.addButton(tr("Retry"), QMessageBox::ActionRole); + QPushButton* reconfigureButton = + msgBox.addButton(tr("Reconfigure"), QMessageBox::ActionRole); + QPushButton* wikiButton = + msgBox.addButton(tr("Help"), QMessageBox::ActionRole); + QPushButton* exitButton = + msgBox.addButton(tr("Exit"), QMessageBox::ActionRole); + + while (true) { + msgBox.exec(); + + if (msgBox.clickedButton() == retryButton) { + m_pCoreServices->getSoundManager()->clearAndQueryDevices(); + *retryClicked = true; + return QDialog::Accepted; + } else if (msgBox.clickedButton() == wikiButton) { + mixxx::DesktopHelper::openUrl(QUrl(MIXXX_WIKI_TROUBLESHOOTING_SOUND_URL)); + wikiButton->setEnabled(false); + } else if (msgBox.clickedButton() == reconfigureButton) { + msgBox.hide(); + + m_pCoreServices->getSoundManager()->clearAndQueryDevices(); + // This way of opening the dialog allows us to use it synchronously + m_pPrefDlg->setWindowModality(Qt::ApplicationModal); + // Open preferences, sound hardware page is selected (default on first call) + m_pPrefDlg->exec(); + if (m_pPrefDlg->result() == QDialog::Accepted) { + return QDialog::Accepted; + } + + msgBox.show(); + } else if (msgBox.clickedButton() == exitButton) { + // Will finally quit Mixxx + return QDialog::Rejected; + } + } +} + +QDialog::DialogCode MixxxMainWindow::soundDeviceBusyDlg(bool* retryClicked) { + QString title(tr("Sound Device Busy")); + QString text( + "

" % + tr("Mixxx was unable to open all the configured sound devices.") + + "

" % + m_pCoreServices->getSoundManager()->getErrorDeviceName() % + " is used by another application or not plugged in." + "

    " + "
  • " % + tr("Retry after closing the other application " + "or reconnecting a sound device") % + "
  • " + "
  • " % + tr("Reconfigure Mixxx's sound device settings.") % + "
  • " + "
  • " % + tr("Get Help from the Mixxx Wiki.") % + "
  • " + "
  • " % + tr("Exit Mixxx.") % + "
  • " + "
"); + return soundDeviceErrorDlg(title, text, retryClicked); +} + +QDialog::DialogCode MixxxMainWindow::soundDeviceErrorMsgDlg( + SoundDeviceStatus status, bool* retryClicked) { + QString title(tr("Sound Device Error")); + QString text("

" % + tr("Mixxx was unable to open all the configured sound " + "devices.") + + "

" % + m_pCoreServices->getSoundManager() + ->getLastErrorMessage(status) + .replace("\n", "
") % + "

    " + "
  • " % + tr("Retry after fixing an issue") % + "
  • " + "
  • " % + tr("Reconfigure Mixxx's sound device settings.") % + "
  • " + "
  • " % + tr("Get Help from the Mixxx Wiki.") % + "
  • " + "
  • " % + tr("Exit Mixxx.") % + "
  • " + "
"); + return soundDeviceErrorDlg(title, text, retryClicked); +} + +QDialog::DialogCode MixxxMainWindow::noOutputDlg(bool* continueClicked) { + QMessageBox msgBox; + msgBox.setIcon(QMessageBox::Warning); + msgBox.setWindowTitle(tr("No Output Devices")); + msgBox.setText( + "" + tr("Mixxx was configured without any output sound devices. " + "Audio processing will be disabled without a configured output device.") + + "
    " + "
  • " + + tr("Continue without any outputs.") + + "
  • " + "
  • " + + tr("Reconfigure Mixxx's sound device settings.") + + "
  • " + "
  • " + + tr("Exit Mixxx.") + + "
  • " + "
"); + + QPushButton* continueButton = + msgBox.addButton(tr("Continue"), QMessageBox::ActionRole); + QPushButton* reconfigureButton = + msgBox.addButton(tr("Reconfigure"), QMessageBox::ActionRole); + QPushButton* exitButton = + msgBox.addButton(tr("Exit"), QMessageBox::ActionRole); + + while (true) { + msgBox.exec(); + + if (msgBox.clickedButton() == continueButton) { + *continueClicked = true; + return QDialog::Accepted; + } else if (msgBox.clickedButton() == reconfigureButton) { + msgBox.hide(); + + // This way of opening the dialog allows us to use it synchronously + m_pPrefDlg->setWindowModality(Qt::ApplicationModal); + m_pPrefDlg->exec(); + if (m_pPrefDlg->result() == QDialog::Accepted) { + return QDialog::Accepted; + } + + msgBox.show(); + + } else if (msgBox.clickedButton() == exitButton) { + // Will finally quit Mixxx + return QDialog::Rejected; + } + } +} + +void MixxxMainWindow::slotUpdateWindowTitle(TrackPointer pTrack) { + QString appTitle = VersionStore::applicationName(); + QString filePath; + + // If we have a track, use getInfo() to format a summary string and prepend + // it to the title. + // TODO(rryan): Does this violate Mac App Store policies? + if (pTrack) { + QString trackInfo = pTrack->getInfo(); + if (!trackInfo.isEmpty()) { + appTitle = QString("%1 | %2").arg(trackInfo, appTitle); + } + filePath = pTrack->getLocation(); + } + setWindowTitle(appTitle); + + // Display a draggable proxy icon for the track in the title bar on + // platforms that support it, e.g. macOS + setWindowFilePath(filePath); +} + +void MixxxMainWindow::createMenuBar() { + qWarning() << " $ createMenuBar"; + ScopedTimer t(QStringLiteral("MixxxMainWindow::createMenuBar")); + DEBUG_ASSERT(m_pCoreServices->getKeyboardConfig()); + m_pMenuBar = make_parented( + this, m_pCoreServices->getSettings(), m_pCoreServices->getKeyboardConfig().get()); + if (m_pCentralWidget) { + m_pMenuBar->setStyleSheet(m_pCentralWidget->styleSheet()); + } + setMenuBar(m_pMenuBar); +} + +void MixxxMainWindow::connectMenuBar() { + // This function might be invoked multiple times on startup + // so all connections must be unique! + qWarning() << " $ connectMenuBar"; + + ScopedTimer t(QStringLiteral("MixxxMainWindow::connectMenuBar")); + connect(this, + &MixxxMainWindow::skinLoaded, + m_pMenuBar, + &WMainMenuBar::onNewSkinLoaded, + Qt::UniqueConnection); + + // Misc + connect(m_pMenuBar, + &WMainMenuBar::quit, + this, + &MixxxMainWindow::close, + Qt::UniqueConnection); + connect(m_pMenuBar, + &WMainMenuBar::showPreferences, + this, + &MixxxMainWindow::slotOptionsPreferences, + Qt::UniqueConnection); + connect(m_pMenuBar, + &WMainMenuBar::loadTrackToDeck, + this, + &MixxxMainWindow::slotFileLoadSongPlayer, + Qt::UniqueConnection); + + connect(m_pMenuBar, + &WMainMenuBar::showKeywheel, + this, + &MixxxMainWindow::slotShowKeywheel, + Qt::UniqueConnection); + + // Fullscreen + connect(m_pMenuBar, + &WMainMenuBar::toggleFullScreen, + this, + &MixxxMainWindow::slotViewFullScreen, + Qt::UniqueConnection); + connect(this, + &MixxxMainWindow::fullScreenChanged, + m_pMenuBar, + &WMainMenuBar::onFullScreenStateChange, + Qt::UniqueConnection); + // Refresh the Fullscreen checkbox for the case we went fullscreen earlier + m_pMenuBar->onFullScreenStateChange(isFullScreen()); + + // Keyboard shortcuts + connect(m_pMenuBar, + &WMainMenuBar::toggleKeyboardShortcuts, + m_pCoreServices.get(), + &mixxx::CoreServices::slotOptionsKeyboard, + Qt::UniqueConnection); + + // Help + connect(m_pMenuBar, + &WMainMenuBar::showAbout, + this, + &MixxxMainWindow::slotHelpAbout, + Qt::UniqueConnection); + + // Developer + connect(m_pMenuBar, + &WMainMenuBar::reloadSkin, + this, + &MixxxMainWindow::rebootMixxxView, + Qt::UniqueConnection); + connect(m_pMenuBar, + &WMainMenuBar::toggleDeveloperTools, + this, + &MixxxMainWindow::slotDeveloperTools, + Qt::UniqueConnection); + + if (m_pCoreServices->getRecordingManager()) { + connect(m_pCoreServices->getRecordingManager().get(), + &RecordingManager::isRecording, + m_pMenuBar, + &WMainMenuBar::onRecordingStateChange, + Qt::UniqueConnection); + connect(m_pMenuBar, + &WMainMenuBar::toggleRecording, + m_pCoreServices->getRecordingManager().get(), + &RecordingManager::slotSetRecording, + Qt::UniqueConnection); + m_pMenuBar->onRecordingStateChange( + m_pCoreServices->getRecordingManager()->isRecordingActive()); + } + +#ifdef __BROADCAST__ + if (m_pCoreServices->getBroadcastManager()) { + connect(m_pCoreServices->getBroadcastManager().get(), + &BroadcastManager::broadcastEnabled, + m_pMenuBar, + &WMainMenuBar::onBroadcastingStateChange, + Qt::UniqueConnection); + connect(m_pMenuBar, + &WMainMenuBar::toggleBroadcasting, + m_pCoreServices->getBroadcastManager().get(), + &BroadcastManager::setEnabled, + Qt::UniqueConnection); + m_pMenuBar->onBroadcastingStateChange(m_pCoreServices->getBroadcastManager()->isEnabled()); + } +#endif + +#ifdef __VINYLCONTROL__ + if (m_pCoreServices->getVinylControlManager()) { + connect(m_pMenuBar, + &WMainMenuBar::toggleVinylControl, + m_pCoreServices->getVinylControlManager().get(), + &VinylControlManager::toggleVinylControl, + Qt::UniqueConnection); + connect(m_pCoreServices->getVinylControlManager().get(), + &VinylControlManager::vinylControlDeckEnabled, + m_pMenuBar, + &WMainMenuBar::onVinylControlDeckEnabledStateChange, + Qt::UniqueConnection); + } +#endif + + auto pPlayerManager = m_pCoreServices->getPlayerManager(); + if (pPlayerManager) { + connect(pPlayerManager.get(), + &PlayerManager::numberOfDecksChanged, + m_pMenuBar, + &WMainMenuBar::onNumberOfDecksChanged, + Qt::UniqueConnection); + m_pMenuBar->onNumberOfDecksChanged(pPlayerManager->numberOfDecks()); + } + + if (m_pCoreServices->getTrackCollectionManager()) { + connect(m_pMenuBar, + &WMainMenuBar::rescanLibrary, + m_pCoreServices->getTrackCollectionManager().get(), + &TrackCollectionManager::startLibraryScan, + Qt::UniqueConnection); + connect(m_pCoreServices->getTrackCollectionManager().get(), + &TrackCollectionManager::libraryScanStarted, + m_pMenuBar, + &WMainMenuBar::onLibraryScanStarted, + Qt::UniqueConnection); + connect(m_pCoreServices->getTrackCollectionManager().get(), + &TrackCollectionManager::libraryScanFinished, + m_pMenuBar, + &WMainMenuBar::onLibraryScanFinished, + Qt::UniqueConnection); + } + + if (m_pCoreServices->getLibrary()) { + connect(m_pMenuBar, + &WMainMenuBar::createCrate, + m_pCoreServices->getLibrary().get(), + &Library::slotCreateCrate, + Qt::UniqueConnection); + connect(m_pMenuBar, + &WMainMenuBar::createPlaylist, + m_pCoreServices->getLibrary().get(), + &Library::slotCreatePlaylist, + Qt::UniqueConnection); + } + +#ifdef __ENGINEPRIME__ + DEBUG_ASSERT(m_pLibraryExporter); + connect(m_pMenuBar, + &WMainMenuBar::exportLibrary, + m_pLibraryExporter.get(), + &mixxx::LibraryExporter::slotRequestExport, + Qt::UniqueConnection); +#endif +} + +/// Enable/disable listening to Alt key press for toggling the menubar. +#ifndef __APPLE__ +void MixxxMainWindow::slotUpdateMenuBarAltKeyConnection() { + if (!m_pCoreServices->getKeyboardEventFilter() || !m_pMenuBar) { + return; + } + + if (m_pCoreServices->getSettings()->getValue(kHideMenuBarConfigKey, false)) { + // with Qt::UniqueConnection we don't need to check whether we're already connected + connect(m_pCoreServices->getKeyboardEventFilter().get(), + &KeyboardEventFilter::altPressedWithoutKeys, + m_pMenuBar, + &WMainMenuBar::slotToggleMenuBar, + Qt::UniqueConnection); + m_pMenuBar->hideMenuBar(); + } else { + disconnect(m_pCoreServices->getKeyboardEventFilter().get(), + &KeyboardEventFilter::altPressedWithoutKeys, + m_pMenuBar, + &WMainMenuBar::slotToggleMenuBar); + m_pMenuBar->showMenuBar(); + } +} +#endif + +void MixxxMainWindow::slotFileLoadSongPlayer(int deck) { + QString group = PlayerManager::groupForDeck(deck - 1); + + QString loadTrackText = tr("Load track to Deck %1").arg(QString::number(deck)); + QString deckWarningMessage = tr("Deck %1 is currently playing a track.") + .arg(QString::number(deck)); + QString areYouSure = tr("Are you sure you want to load a new track?"); + + if (ControlObject::get(ConfigKey(group, "play")) > 0.0) { + int ret = QMessageBox::warning(this, + VersionStore::applicationName(), + deckWarningMessage + "\n" + areYouSure, + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); + + if (ret != QMessageBox::Yes) { + return; + } + } + + UserSettingsPointer pConfig = m_pCoreServices->getSettings(); + QString trackPath = + QFileDialog::getOpenFileName( + this, + loadTrackText, + pConfig->getValueString(mixxx::library::prefs::kLegacyDirectoryConfigKey), + QString("Audio (%1)") + .arg(SoundSourceProxy::getSupportedFileNamePatterns().join(" "))); + + if (!trackPath.isNull()) { + // The user has picked a file via a file dialog. This means the system + // sandboxer (if we are sandboxed) has granted us permission to this + // folder. Create a security bookmark while we have permission so that + // we can access the folder on future runs. We need to canonicalize the + // path so we first wrap the directory string with a QDir. + mixxx::FileInfo fileInfo(trackPath); + Sandbox::createSecurityToken(&fileInfo); + + m_pCoreServices->getPlayerManager()->slotLoadToDeck(trackPath, deck); + } +} + +void MixxxMainWindow::slotDeveloperTools(bool visible) { + if (visible) { + if (m_pDeveloperToolsDlg == nullptr) { + UserSettingsPointer pConfig = m_pCoreServices->getSettings(); + m_pDeveloperToolsDlg = new DlgDeveloperTools(this, pConfig); + connect(m_pDeveloperToolsDlg, + &DlgDeveloperTools::destroyed, + this, + &MixxxMainWindow::slotDeveloperToolsClosed); + connect(this, + &MixxxMainWindow::closeDeveloperToolsDlgChecked, + m_pDeveloperToolsDlg, + &DlgDeveloperTools::done); + connect(m_pDeveloperToolsDlg, + &DlgDeveloperTools::destroyed, + m_pMenuBar, + &WMainMenuBar::onDeveloperToolsHidden); + } + m_pMenuBar->onDeveloperToolsShown(); + m_pDeveloperToolsDlg->show(); + m_pDeveloperToolsDlg->activateWindow(); + } else { + emit closeDeveloperToolsDlgChecked(0); + } +} + +void MixxxMainWindow::slotDeveloperToolsClosed() { + m_pDeveloperToolsDlg = nullptr; +} + +void MixxxMainWindow::slotViewFullScreen(bool toggle) { + qWarning() << " $ slotViewFullScreen" << toggle; + if (isFullScreen() == toggle) { + qWarning() << " (no-op)"; + return; + } + + // Just switch the window state here. eventFilter() will catch the + // QWindowStateChangeEvent and inform the menu bar that fullscreen changed. + if (toggle) { + qWarning() << " > showFullScreen()"; + showFullScreen(); + } else { + qWarning() << " > showNormal()"; + showNormal(); + } +} + +void MixxxMainWindow::slotOptionsPreferences() { + m_pPrefDlg->show(); + m_pPrefDlg->raise(); + m_pPrefDlg->activateWindow(); +} + +void MixxxMainWindow::slotNoVinylControlInputConfigured() { + QMessageBox::StandardButton btn = QMessageBox::warning( + this, + VersionStore::applicationName(), + tr("There is no input device selected for this vinyl control.\n" + "Please select an input device in the sound hardware preferences first."), + QMessageBox::Ok | QMessageBox::Cancel, + QMessageBox::Cancel); + if (btn == QMessageBox::Ok) { + m_pPrefDlg->show(); + m_pPrefDlg->showSoundHardwarePage(); + } +} + +void MixxxMainWindow::slotNoDeckPassthroughInputConfigured() { + QMessageBox::StandardButton btn = QMessageBox::warning( + this, + VersionStore::applicationName(), + tr("There is no input device selected for this passthrough control.\n" + "Please select an input device in the sound hardware preferences first."), + QMessageBox::Ok | QMessageBox::Cancel, + QMessageBox::Cancel); + if (btn == QMessageBox::Ok) { + m_pPrefDlg->show(); + m_pPrefDlg->showSoundHardwarePage(); + } +} + +void MixxxMainWindow::slotNoMicrophoneInputConfigured() { + QMessageBox::StandardButton btn = QMessageBox::question( + this, + VersionStore::applicationName(), + tr("There is no input device selected for this microphone.\n" + "Do you want to select an input device?"), + QMessageBox::Ok | QMessageBox::Cancel, + QMessageBox::Cancel); + if (btn == QMessageBox::Ok) { + m_pPrefDlg->show(); + m_pPrefDlg->showSoundHardwarePage(); + } +} + +void MixxxMainWindow::slotNoAuxiliaryInputConfigured() { + QMessageBox::StandardButton btn = QMessageBox::question( + this, + VersionStore::applicationName(), + tr("There is no input device selected for this auxiliary.\n" + "Do you want to select an input device?"), + QMessageBox::Ok | QMessageBox::Cancel, + QMessageBox::Cancel); + if (btn == QMessageBox::Ok) { + m_pPrefDlg->show(); + m_pPrefDlg->showSoundHardwarePage(); + } +} + +void MixxxMainWindow::slotHelpAbout() { + DlgAbout* about = new DlgAbout; + about->show(); +} + +void MixxxMainWindow::slotShowKeywheel(bool toggle) { + if (!m_pKeywheel) { + m_pKeywheel = make_parented(this, m_pCoreServices->getSettings()); + // uncheck the menu item on window close + connect(m_pKeywheel.get(), + &DlgKeywheel::finished, + m_pMenuBar, + &WMainMenuBar::onKeywheelChange); + } + if (toggle) { + m_pKeywheel->show(); + m_pKeywheel->raise(); + } else { + m_pKeywheel->hide(); + } +} + +void MixxxMainWindow::slotTooltipModeChanged(mixxx::preferences::Tooltips tt) { + m_toolTipsCfg = tt; +#ifdef MIXXX_USE_QOPENGL + ToolTipQOpenGL::singleton().setActive( + m_toolTipsCfg == mixxx::preferences::Tooltips::On); +#endif +} + +void MixxxMainWindow::rebootMixxxView() { + qDebug() << "Now in rebootMixxxView..."; + + // safe geometry for later restoration + const QRect initGeometry = geometry(); + + // We need to tell the menu bar that we are about to delete the old skin and + // create a new one. It holds "visibility" controls (e.g. "Show Samplers") + // that need to be deleted -- otherwise we can't tell what features the skin + // supports since the controls from the previous skin will be left over. + m_pMenuBar->onNewSkinAboutToLoad(); + + if (m_pCentralWidget) { + m_pCentralWidget->hide(); + WaveformWidgetFactory::instance()->destroyWidgets(); + delete m_pCentralWidget; + m_pCentralWidget = nullptr; + } + + // Workaround for changing skins while fullscreen, just go out of fullscreen + // mode. If you change skins while in fullscreen (on Linux, at least) the + // window returns to 0,0 but and the backdrop disappears so it looks as if + // it is not fullscreen, but acts as if it is. + bool wasFullScreen = isFullScreen(); + if (wasFullScreen) { + showMaximized(); + } + + tryParseAndSetDefaultStyleSheet(); + + if (!loadConfiguredSkin()) { + QMessageBox::critical(this, + tr("Error in skin file"), + tr("The selected skin cannot be loaded.")); + // m_pWidgetParent is NULL, we can't continue. + return; + } + m_pMenuBar->setStyleSheet(m_pCentralWidget->styleSheet()); + + setCentralWidget(m_pCentralWidget); +#ifdef __LINUX__ + // don't adjustSize() on Linux as this wouldn't use the entire available area + // to paint the new skin with X11 + // https://github.com/mixxxdj/mixxx/issues/9309 +#else + adjustSize(); +#endif + + if (wasFullScreen) { + showFullScreen(); + } else { + // Programmatic placement at this point is very problematic. + // The screen() method returns stale data (primary screen) + // until the user interacts with mixxx again. Keyboard shortcuts + // do not count, moving window, opening menu etc does + // Therefore the placement logic was removed by a simple geometry restore. + // If the minimum size of the new skin is larger then the restored + // geometry, the window will be enlarged right & bottom which is + // safe as the menu is still reachable. + setGeometry(initGeometry); + } + + qDebug() << "rebootMixxxView DONE"; +} + +bool MixxxMainWindow::loadConfiguredSkin() { + // TODO: use std::shared_ptr throughout skin widgets instead of these hacky get() calls + m_pCentralWidget = m_pSkinLoader->loadConfiguredSkin(this, + &m_skinCreatedControls, + m_pCoreServices.get()); + if (centralWidget() == m_pLaunchImage) { + initializationProgressUpdate(100, ""); + } + emit skinLoaded(); + return m_pCentralWidget != nullptr; +} + +/// Try to load default styles that can be overridden by skins +void MixxxMainWindow::tryParseAndSetDefaultStyleSheet() { + const QString resPath = m_pCoreServices->getSettings()->getResourcePath(); + QFile file(resPath + "/skins/default.qss"); + if (file.open(QIODevice::ReadOnly)) { + QByteArray fileBytes = file.readAll(); + QString style = QString::fromUtf8(fileBytes); + setStyleSheet(style); + } else { + qWarning() << "Failed to load default skin styles /skins/default.qss!"; + } +} + +/// Catch ToolTip and WindowStateChange events +bool MixxxMainWindow::eventFilter(QObject* obj, QEvent* event) { + if (event->type() == QEvent::ToolTip) { + // always show tooltips in the preferences window + QWidget* activeWindow = QApplication::activeWindow(); + if (activeWindow && + QLatin1String(activeWindow->metaObject()->className()) != + "DlgPreferences") { + // return true for no tool tips + switch (m_toolTipsCfg) { + case mixxx::preferences::Tooltips::OnlyInLibrary: + if (dynamic_cast(obj) != nullptr) { + return true; + } + break; + case mixxx::preferences::Tooltips::On: + break; + case mixxx::preferences::Tooltips::Off: + return true; + default: + DEBUG_ASSERT(!"m_toolTipsCfg value unknown"); + return true; + } + } + } else if (event->type() == QEvent::WindowStateChange) { +#ifndef __APPLE__ + qWarning() << "$ WindowStateChange:" << windowState(); + if (windowState() == m_prevState) { + // Ignore no-op. This happens if another window is raised above + // MixxxMianWindow, e.g. DlgPeferences. In such a case event->oldState() + // will be Qt::WindowNoState which is wrong anyway, so there is nothing + // to do internally. + qWarning() << "$ WindowStateChange IGNORE"; + return QMainWindow::eventFilter(obj, event); + } + m_prevState = windowState(); +#endif + // Detect if we entered or quit fullscreen mode. + QWindowStateChangeEvent* changeEvent = + static_cast(event); + const bool wasFullScreen = changeEvent->oldState() & Qt::WindowFullScreen; + const bool isFullScreenNow = windowState() & Qt::WindowFullScreen; + if ((isFullScreenNow && !wasFullScreen) || + (!isFullScreenNow && wasFullScreen)) { + qWarning() << "$ fullscreen changed, now" + << (isFullScreenNow ? "fullscreen" : "window"); +#ifdef __LINUX__ + // Fix for "No menu bar with ubuntu unity in full screen mode" + // (issues #6072 and #6689). Before touching anything here, please + // read those bugs. + // Set this attribute instead of calling setNativeMenuBar(false), + // see https://github.com/mixxxdj/mixxx/issues/11320 + if (m_supportsGlobalMenuBar) { + qWarning() << "$ global menu > rebuild"; + QApplication::setAttribute(Qt::AA_DontUseNativeMenuBar, isFullScreenNow); + createMenuBar(); + connectMenuBar(); + } +#endif + +#ifndef __APPLE__ +#ifdef __LINUX__ + // Only show the dialog if we are able to have the menubar in the + // main window, only then we're able to hide it. + if (!m_supportsGlobalMenuBar || isFullScreenNow) +#endif + { + alwaysHideMenuBarDlg(); + slotUpdateMenuBarAltKeyConnection(); + } +#endif + + // This will toggle the Fullscreen checkbox and hide the menubar if + // we go fullscreen. + // Skip this during startup or the launchimage will be shifted + // up & down when the menu is shown menu and 'hidden'. The menu + // will be updated when the skin finished loading. + if (centralWidget() != m_pLaunchImage) { + emit fullScreenChanged(isFullScreen()); + } + } + } + // standard event processing + return QMainWindow::eventFilter(obj, event); +} + +void MixxxMainWindow::closeEvent(QCloseEvent* event) { + // WARNING: We can receive a CloseEvent while only partially + // initialized. This is because we call QApplication::processEvents to + // render LaunchImage progress in the constructor. + if (!confirmExit()) { + event->ignore(); + return; + } + QMainWindow::closeEvent(event); +} + +void MixxxMainWindow::checkDirectRendering() { + // IF + // * A waveform viewer exists + // AND + // * The waveform viewer is an OpenGL waveform viewer + // AND + // * The waveform viewer does not have direct rendering enabled. + // THEN + // * Warn user + + WaveformWidgetFactory* factory = WaveformWidgetFactory::instance(); + if (!factory) { + return; + } + + UserSettingsPointer pConfig = m_pCoreServices->getSettings(); + + if (!factory->isOpenGlAvailable() && !factory->isOpenGlesAvailable() && + pConfig->getValueString(ConfigKey("[Direct Rendering]", "Warned")) != QString("yes")) { + QMessageBox::warning(nullptr, + tr("OpenGL Direct Rendering"), + tr("Direct rendering is not enabled on your machine.

" + "This means that the waveform displays will be very
" + "slow and may tax your CPU heavily. Either update " + "your
" + "configuration to enable direct rendering, or disable
" + "the waveform displays in the Mixxx preferences by " + "selecting
" + "\"Empty\" as the waveform display in the 'Interface' " + "section.")); + pConfig->set(ConfigKey("[Direct Rendering]", "Warned"), QString("yes")); + } +} + +bool MixxxMainWindow::confirmExit() { + bool playing(false); + bool playingSampler(false); + auto pPlayerManager = m_pCoreServices->getPlayerManager(); + unsigned int deckCount = pPlayerManager->numDecks(); + unsigned int samplerCount = pPlayerManager->numSamplers(); + for (unsigned int i = 0; i < deckCount; ++i) { + if (ControlObject::toBool( + ConfigKey(PlayerManager::groupForDeck(i), "play"))) { + playing = true; + break; + } + } + for (unsigned int i = 0; i < samplerCount; ++i) { + if (ControlObject::toBool( + ConfigKey(PlayerManager::groupForSampler(i), "play"))) { + playingSampler = true; + break; + } + } + if (playing) { + QMessageBox::StandardButton btn = QMessageBox::question(this, + tr("Confirm Exit"), + tr("A deck is currently playing. Exit Mixxx?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); + if (btn == QMessageBox::No) { + return false; + } + } else if (playingSampler) { + QMessageBox::StandardButton btn = QMessageBox::question(this, + tr("Confirm Exit"), + tr("A sampler is currently playing. Exit Mixxx?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); + if (btn == QMessageBox::No) { + return false; + } + } + if (m_pPrefDlg && m_pPrefDlg->isVisible()) { + QMessageBox::StandardButton btn = QMessageBox::question( + this, tr("Confirm Exit"), tr("The preferences window is still open.") + "
" + tr("Discard any changes and exit Mixxx?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (btn == QMessageBox::No) { + return false; + } else { + m_pPrefDlg->close(); + } + } + + return true; +} + +void MixxxMainWindow::initializationProgressUpdate(int progress, const QString& serviceName) { + if (m_pLaunchImage) { + m_pLaunchImage->progress(progress, serviceName); + } + qApp->processEvents(); +} + +void MixxxMainWindow::oscEnable() { + UserSettingsPointer pConfig; + QString MixxxOSCStatusFilePath = m_pCoreServices->getSettings()->getSettingsPath(); + QString MixxxOSCStatusFileLocation = MixxxOSCStatusFilePath + "/MixxxOSCStatus.txt"; + QFile MixxxOSCStatusFile(MixxxOSCStatusFileLocation); + MixxxOSCStatusFile.remove(); + MixxxOSCStatusFile.open(QIODevice::ReadWrite | QIODevice::Append); + QTextStream MixxxOSCStatusTxt(&MixxxOSCStatusFile); + if (m_pCoreServices->getSettings()->getValue(ConfigKey("[OSC]", "OscEnabled"))) { + MixxxOSCStatusTxt << QString("OSC enabled") << "\n"; +// OscFunctionsSendPtrChar(m_pCoreServices->getSettings(), "[Master]", "StartReset", "1"); +// enum DefOscBodyType OscBodyType = INTBODY; + OscFunctionsSendPtrType(m_pCoreServices->getSettings(), "[Master]", "StartReset", INTBODY, "", 1, 0, 0); + + //void OscFunctionsSendPtrType(UserSettingsPointer m_pConfig, +// QString OscGroup, +// QString OscKey, +// enum DefOscBodyType OscBodyType, +// QString OscMessageBodyQString, +// int OscMessageBodyInt, +// double OscMessageBodyDouble, +// float OscMessageBodyFloat) { + + + OscReceiverMain(m_pCoreServices->getSettings()); + + } else { + MixxxOSCStatusTxt << QString("OSC NOT enabled") << "\n"; + } + MixxxOSCStatusFile.close(); +} diff --git a/src/analyzer/constants.h b/src/analyzer/constants.h index 42d84db7217..5e670a37dc0 100644 --- a/src/analyzer/constants.h +++ b/src/analyzer/constants.h @@ -10,7 +10,6 @@ namespace mixxx { // fixed number of channels like the engine does, usually 2 = stereo. constexpr audio::ChannelCount kAnalysisChannels = mixxx::kEngineChannelOutputCount; constexpr audio::ChannelCount kAnalysisMaxChannels = mixxx::kMaxEngineChannelInputCount; -constexpr int kMaxSupportedStems = 4; constexpr SINT kAnalysisFramesPerChunk = 4096; constexpr SINT kAnalysisSamplesPerChunk = kAnalysisFramesPerChunk * kAnalysisMaxChannels; diff --git a/src/engine/20240918 BU enginebuffer.cpp b/src/engine/20240918 BU enginebuffer.cpp new file mode 100644 index 00000000000..5244db43087 --- /dev/null +++ b/src/engine/20240918 BU enginebuffer.cpp @@ -0,0 +1,1886 @@ +#include "engine/enginebuffer.h" + +#include + +#include "control/controllinpotmeter.h" +#include "control/controlpotmeter.h" +#include "control/controlproxy.h" +#include "control/controlpushbutton.h" +#include "engine/bufferscalers/enginebufferscalelinear.h" +#include "engine/bufferscalers/enginebufferscalest.h" +#include "engine/cachingreader/cachingreader.h" +#include "engine/channels/enginechannel.h" +#include "engine/controls/bpmcontrol.h" +#include "engine/controls/clockcontrol.h" +#include "engine/controls/cuecontrol.h" +#include "engine/controls/enginecontrol.h" +#include "engine/controls/keycontrol.h" +#include "engine/controls/loopingcontrol.h" +#include "engine/controls/quantizecontrol.h" +#include "engine/controls/ratecontrol.h" +#include "engine/enginemixer.h" +#include "engine/readaheadmanager.h" +#include "engine/sync/enginesync.h" +#include "engine/sync/synccontrol.h" +#include "moc_enginebuffer.cpp" +#include "preferences/usersettings.h" +#include "track/track.h" +#include "util/assert.h" +#include "util/compatibility/qatomic.h" +#include "util/defs.h" +#include "util/logger.h" +#include "util/sample.h" +#include "util/timer.h" +#include "waveform/visualplayposition.h" + +#ifdef __RUBBERBAND__ +#include "engine/bufferscalers/enginebufferscalerubberband.h" +#endif + +#ifdef __VINYLCONTROL__ +#include "engine/controls/vinylcontrolcontrol.h" +#endif + +// EVE OSC +//#include +//#include + +//#include +//#include + +//#include "osc/ip/UdpSocket.h" +//#include "osc/osc/OscOutboundPacketStream.h" + +//#define oscClientAddress "192.168.0.125" +//#define oscPortOut 9000 +//#define OUTPUT_BUFFER_SIZE 1024 +//#define IP_MTU_SIZE 1536 +//#include "osc/oscfunctions.h" + +namespace { +const mixxx::Logger kLogger("EngineBuffer"); + +constexpr double kLinearScalerElipsis = + 1.00058; // 2^(0.01/12): changes < 1 cent allows a linear scaler + +// Rate at which the playpos slider is updated +constexpr int kPlaypositionUpdateRate = 15; // updates per second + +const QString kAppGroup = QStringLiteral("[App]"); + +} // anonymous namespace + +//EveOSC +enum DefOscBodyType { + STRINGBODY = 1, + INTBODY = 2, + DOUBLEBODY = 3, + FLOATBODY = 4 +}; + +void OscFunctionsSendChar(QString Oscgroup, QString OscKey, QString OscMessageBody); +void OscFunctionsSendFloat(QString OscGroup, QString OscKey, float OscMessageBody); +void OscFunctionsSendPtrType(UserSettingsPointer m_pConfig, + QString OscGroup, + QString OscKey, + enum DefOscBodyType OscBodyType, + QString OscMessageBodyQString, + int OscMessageBodyInt, + double OscMessageBodyDouble, + float OscMessageBodyFloat); +// EveOSC + +EngineBuffer::EngineBuffer(const QString& group, + UserSettingsPointer pConfig, + EngineChannel* pChannel, + EngineMixer* pMixingEngine, + mixxx::audio::ChannelCount maxSupportedChannel) + : m_group(group), + m_pConfig(pConfig), + m_pLoopingControl(nullptr), + m_pSyncControl(nullptr), + m_pVinylControlControl(nullptr), + m_pRateControl(nullptr), + m_pBpmControl(nullptr), + m_pKeyControl(nullptr), + m_pReadAheadManager(nullptr), + m_pReader(nullptr), + m_playPos(kInitialPlayPosition), + m_speed_old(0), + m_actual_speed(0), + m_tempo_ratio_old(1.), + m_scratching_old(false), + m_reverse_old(false), + m_pitch_old(0), + m_baserate_old(0), + m_rate_old(0.), + m_trackEndPositionOld(mixxx::audio::kInvalidFramePos), + m_slipPos(mixxx::audio::kStartFramePos), + m_dSlipRate(1.0), + m_bSlipEnabledProcessing(false), + m_slipModeState(SlipModeState::Disabled), + m_pRepeat(nullptr), + m_startButton(nullptr), + m_endButton(nullptr), + m_bScalerOverride(false), + m_iSeekPhaseQueued(0), + m_iEnableSyncQueued(SYNC_REQUEST_NONE), + m_iSyncModeQueued(static_cast(SyncMode::Invalid)), + m_bPlayAfterLoading(false), + 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. + DEBUG_ASSERT(kInitialPlayPosition.isValid()); + + m_queuedSeek.setValue(kNoQueuedSeek); + + // zero out crossfade buffer + SampleUtil::clear(m_pCrossfadeBuffer, kMaxEngineFrames * mixxx::kMaxEngineChannelInputCount); + + m_pReader = new CachingReader(group, pConfig, maxSupportedChannel); + connect(m_pReader, &CachingReader::trackLoading, + this, &EngineBuffer::slotTrackLoading, + Qt::DirectConnection); + connect(m_pReader, &CachingReader::trackLoaded, + this, &EngineBuffer::slotTrackLoaded, + Qt::DirectConnection); + connect(m_pReader, &CachingReader::trackLoadFailed, + this, &EngineBuffer::slotTrackLoadFailed, + Qt::DirectConnection); + + // Play button + m_playButton = new ControlPushButton(ConfigKey(m_group, "play")); + m_playButton->setButtonMode(mixxx::control::ButtonMode::Toggle); + m_playButton->connectValueChangeRequest( + this, &EngineBuffer::slotControlPlayRequest, + Qt::DirectConnection); + + //Play from Start Button (for sampler) + m_playStartButton = new ControlPushButton(ConfigKey(m_group, "start_play")); + connect(m_playStartButton, &ControlObject::valueChanged, + this, &EngineBuffer::slotControlPlayFromStart, + Qt::DirectConnection); + + // Jump to start and stop button + m_stopStartButton = new ControlPushButton(ConfigKey(m_group, "start_stop")); + connect(m_stopStartButton, &ControlObject::valueChanged, + this, &EngineBuffer::slotControlJumpToStartAndStop, + Qt::DirectConnection); + + //Stop playback (for sampler) + m_stopButton = new ControlPushButton(ConfigKey(m_group, "stop")); + connect(m_stopButton, &ControlObject::valueChanged, + this, &EngineBuffer::slotControlStop, + Qt::DirectConnection); + + // Start button + m_startButton = new ControlPushButton(ConfigKey(m_group, "start")); + m_startButton->setButtonMode(mixxx::control::ButtonMode::Trigger); + connect(m_startButton, &ControlObject::valueChanged, + this, &EngineBuffer::slotControlStart, + Qt::DirectConnection); + + // End button + m_endButton = new ControlPushButton(ConfigKey(m_group, "end")); + connect(m_endButton, &ControlObject::valueChanged, + this, &EngineBuffer::slotControlEnd, + Qt::DirectConnection); + + m_pSlipButton = new ControlPushButton(ConfigKey(m_group, "slip_enabled")); + m_pSlipButton->setButtonMode(mixxx::control::ButtonMode::Toggle); + + m_playposSlider = new ControlLinPotmeter( + ConfigKey(m_group, "playposition"), 0.0, 1.0, 0, 0, true); + connect(m_playposSlider, &ControlObject::valueChanged, + this, &EngineBuffer::slotControlSeek, + Qt::DirectConnection); + + // Control used to communicate ratio playpos to GUI thread + m_visualPlayPos = VisualPlayPosition::getVisualPlayPosition(m_group); + + m_pRepeat = new ControlPushButton(ConfigKey(m_group, "repeat")); + m_pRepeat->setButtonMode(mixxx::control::ButtonMode::Toggle); + + m_pSampleRate = new ControlProxy(kAppGroup, QStringLiteral("samplerate"), this); + + m_pTrackSamples = new ControlObject(ConfigKey(m_group, "track_samples")); + m_pTrackSampleRate = new ControlObject(ConfigKey(m_group, "track_samplerate")); + + m_pTrackType = new ControlObject(ConfigKey(m_group, "track_type")); + m_pTrackTypeLength = new ControlObject(ConfigKey(m_group, "track_type_length")); + m_pTrackArtistLength = new ControlObject(ConfigKey(m_group, "track_artist_length")); + m_pTrackArtist_1 = new ControlObject(ConfigKey(m_group, "track_artist_1")); + m_pTrackArtist_2 = new ControlObject(ConfigKey(m_group, "track_artist_2")); + m_pTrackArtist_3 = new ControlObject(ConfigKey(m_group, "track_artist_3")); + m_pTrackArtist_4 = new ControlObject(ConfigKey(m_group, "track_artist_4")); + m_pTrackArtist_5 = new ControlObject(ConfigKey(m_group, "track_artist_5")); + + m_pTrackTitleLength = new ControlObject(ConfigKey(m_group, "track_title_length")); + m_pTrackTitle_1 = new ControlObject(ConfigKey(m_group, "track_title_1")); + m_pTrackTitle_2 = new ControlObject(ConfigKey(m_group, "track_title_2")); + m_pTrackTitle_3 = new ControlObject(ConfigKey(m_group, "track_title_3")); + m_pTrackTitle_4 = new ControlObject(ConfigKey(m_group, "track_title_4")); + m_pTrackTitle_5 = new ControlObject(ConfigKey(m_group, "track_title_5")); + + m_pKeylock = new ControlPushButton(ConfigKey(m_group, "keylock"), true); + m_pKeylock->setButtonMode(mixxx::control::ButtonMode::Toggle); + + m_pReplayGain = new ControlProxy(m_group, QStringLiteral("replaygain"), this); + + m_pTrackLoaded = new ControlObject(ConfigKey(m_group, "track_loaded"), false); + m_pTrackLoaded->setReadOnly(); + + // Quantization Controller for enabling and disabling the + // quantization (alignment) of loop in/out positions and (hot)cues with + // beats. + QuantizeControl* quantize_control = new QuantizeControl(group, pConfig); + addControl(quantize_control); + m_pQuantize = ControlObject::getControl(ConfigKey(group, "quantize")); + + // Create the Loop Controller + m_pLoopingControl = new LoopingControl(group, pConfig); + addControl(m_pLoopingControl); + + m_pEngineSync = pMixingEngine->getEngineSync(); + + m_pSyncControl = new SyncControl(group, pConfig, pChannel, m_pEngineSync); + +#ifdef __VINYLCONTROL__ + m_pVinylControlControl = new VinylControlControl(group, pConfig); + addControl(m_pVinylControlControl); +#endif + + // Create the Rate Controller + m_pRateControl = new RateControl(group, pConfig); + // Add the Rate Controller + addControl(m_pRateControl); + // Looping Control needs Rate Control for Reverse Button + m_pLoopingControl->setRateControl(m_pRateControl); + + // Create the BPM Controller + m_pBpmControl = new BpmControl(group, pConfig); + addControl(m_pBpmControl); + + // TODO(rryan) remove this dependence? + m_pRateControl->setBpmControl(m_pBpmControl); + m_pSyncControl->setEngineControls(m_pRateControl, m_pBpmControl); + pMixingEngine->getEngineSync()->addSyncableDeck(m_pSyncControl); + addControl(m_pSyncControl); + + m_pKeyControl = new KeyControl(group, pConfig); + addControl(m_pKeyControl); + + // Create the clock controller + m_pClockControl = new ClockControl(group, pConfig); + addControl(m_pClockControl); + + // Create the cue controller + m_pCueControl = new CueControl(group, pConfig); + addControl(m_pCueControl); + + connect(m_pLoopingControl, + &LoopingControl::loopReset, + m_pCueControl, + &CueControl::slotLoopReset, + Qt::DirectConnection); + connect(m_pLoopingControl, + &LoopingControl::loopUpdated, + m_pCueControl, + &CueControl::slotLoopUpdated, + Qt::DirectConnection); + connect(m_pLoopingControl, + &LoopingControl::loopEnabledChanged, + m_pCueControl, + &CueControl::slotLoopEnabledChanged, + Qt::DirectConnection); + connect(m_pCueControl, + &CueControl::loopRemove, + m_pLoopingControl, + &LoopingControl::slotLoopRemove, + Qt::DirectConnection); + + m_pReadAheadManager = new ReadAheadManager(m_pReader, + m_pLoopingControl); + m_pReadAheadManager->addRateControl(m_pRateControl); + + m_pKeylockEngine = new ControlProxy(kAppGroup, QStringLiteral("keylock_engine"), this); + m_pKeylockEngine->connectValueChanged(this, + &EngineBuffer::slotKeylockEngineChanged, + Qt::DirectConnection); + // Construct scaling objects + m_pScaleLinear = new EngineBufferScaleLinear(m_pReadAheadManager); + m_pScaleST = new EngineBufferScaleST(m_pReadAheadManager); +#ifdef __RUBBERBAND__ + m_pScaleRB = new EngineBufferScaleRubberBand(m_pReadAheadManager); +#endif + slotKeylockEngineChanged(m_pKeylockEngine->get()); + m_pScaleVinyl = m_pScaleLinear; + m_pScale = m_pScaleVinyl; + m_pScale->clear(); + m_bScalerChanged = true; + + m_pPassthroughEnabled = new ControlProxy(group, "passthrough", this); + m_pPassthroughEnabled->connectValueChanged(this, &EngineBuffer::slotPassthroughChanged, + Qt::DirectConnection); + +#ifdef __SCALER_DEBUG__ + df.setFileName("mixxx-debug.csv"); + df.open(QIODevice::WriteOnly | QIODevice::Text); + writer.setDevice(&df); +#endif + + // Now that all EngineControls have been created call setEngineMixer. + // TODO(XXX): Get rid of EngineControl::setEngineMixer and + // EngineControl::setEngineBuffer entirely and pass them through the + // constructor. + setEngineMixer(pMixingEngine); +} + +EngineBuffer::~EngineBuffer() { +#ifdef __SCALER_DEBUG__ + //close the writer + df.close(); +#endif + delete m_pReadAheadManager; + delete m_pReader; + + delete m_playButton; + delete m_playStartButton; + delete m_stopStartButton; + + delete m_startButton; + delete m_endButton; + delete m_stopButton; + delete m_playposSlider; + + delete m_pSlipButton; + delete m_pRepeat; + delete m_pSampleRate; + + delete m_pTrackLoaded; + delete m_pTrackSamples; + delete m_pTrackSampleRate; + + delete m_pTrackType; + delete m_pTrackTypeLength; + delete m_pTrackArtistLength; + delete m_pTrackArtist_1; + delete m_pTrackArtist_2; + delete m_pTrackArtist_3; + delete m_pTrackArtist_4; + delete m_pTrackArtist_5; + + delete m_pTrackTitleLength; + delete m_pTrackTitle_1; + delete m_pTrackTitle_2; + delete m_pTrackTitle_3; + delete m_pTrackTitle_4; + delete m_pTrackTitle_5; + + delete m_pScaleLinear; + delete m_pScaleST; +#ifdef __RUBBERBAND__ + delete m_pScaleRB; +#endif + + delete m_pKeylock; + delete m_pReplayGain; + + SampleUtil::free(m_pCrossfadeBuffer); + + qDeleteAll(m_engineControls); +} + +void EngineBuffer::bindWorkers(EngineWorkerScheduler* pWorkerScheduler) { + m_pReader->setScheduler(pWorkerScheduler); +} + +void EngineBuffer::enableIndependentPitchTempoScaling(bool bEnable, + const int iBufferSize) { + // MUST ACQUIRE THE PAUSE MUTEX BEFORE CALLING THIS METHOD + + // When no time-stretching or pitch-shifting is needed we use our own linear + // interpolation code (EngineBufferScaleLinear). It is faster and sounds + // much better for scratching. + + // m_pScaleKeylock and m_pScaleVinyl could change out from under us, + // so cache it. + EngineBufferScale* keylock_scale = m_pScaleKeylock; + EngineBufferScale* vinyl_scale = m_pScaleVinyl; + + if (bEnable && m_pScale != keylock_scale) { + if (m_speed_old != 0.0) { + // Crossfade if we are not paused. + // If we start from zero a ramping gain is + // applied later + readToCrossfadeBuffer(iBufferSize); + } + m_pScale = keylock_scale; + m_pScale->clear(); + m_bScalerChanged = true; + } else if (!bEnable && m_pScale != vinyl_scale) { + if (m_speed_old != 0.0) { + // Crossfade if we are not paused + // (for slow speeds below 0.1 the vinyl_scale is used) + readToCrossfadeBuffer(iBufferSize); + } + m_pScale = vinyl_scale; + m_pScale->clear(); + m_bScalerChanged = true; + } +} + +mixxx::Bpm EngineBuffer::getBpm() const { + return m_pBpmControl->getBpm(); +} + +mixxx::Bpm EngineBuffer::getLocalBpm() const { + return m_pBpmControl->getLocalBpm(); +} + +void EngineBuffer::setBeatLoop(mixxx::audio::FramePos startPosition, bool enabled) { + m_pLoopingControl->setBeatLoop(startPosition, enabled); +} + +void EngineBuffer::setLoop(mixxx::audio::FramePos startPosition, + mixxx::audio::FramePos endPositon, + bool enabled) { + m_pLoopingControl->setLoop(startPosition, endPositon, enabled); +} + +void EngineBuffer::setEngineMixer(EngineMixer* pEngineMixer) { + for (const auto& pControl : std::as_const(m_engineControls)) { + pControl->setEngineMixer(pEngineMixer); + } +} + +void EngineBuffer::queueNewPlaypos(mixxx::audio::FramePos position, enum SeekRequest seekType) { + // All seeks need to be done in the Engine thread so queue it up. + // Write the position before the seek type, to reduce a possible race + // condition effect + VERIFY_OR_DEBUG_ASSERT(seekType != SEEK_PHASE) { + // SEEK_PHASE with a position is not supported + // use SEEK_STANDARD for that + seekType = SEEK_STANDARD; + } + m_queuedSeek.setValue({position, seekType}); +} + +void EngineBuffer::requestSyncPhase() { + // Don't overwrite m_iSeekQueued + m_iSeekPhaseQueued = 1; +} + +void EngineBuffer::requestEnableSync(bool enabled) { + // If we're not playing, the queued event won't get processed so do it now. + if (m_playButton->get() == 0.0) { + if (enabled) { + m_pEngineSync->requestSyncMode(m_pSyncControl, SyncMode::Follower); + } else { + m_pEngineSync->requestSyncMode(m_pSyncControl, SyncMode::None); + } + return; + } + SyncRequestQueued enable_request = + static_cast(atomicLoadRelaxed(m_iEnableSyncQueued)); + if (enabled) { + m_iEnableSyncQueued = SYNC_REQUEST_ENABLE; + } else { + // If sync is enabled and disabled very quickly, it's is a one-shot + // sync event and needs to be handled specially. Otherwise the sync + // state will get stuck on or won't go on at all. + if (enable_request == SYNC_REQUEST_ENABLE) { + m_iEnableSyncQueued = SYNC_REQUEST_ENABLEDISABLE; + } else { + // Note that there is no DISABLEENABLE, because that's an irrelevant + // queuing. Moreover, ENABLEDISABLEENABLE is also redundant, so + // we don't have to handle any special cases. + m_iEnableSyncQueued = SYNC_REQUEST_DISABLE; + } + } +} + +void EngineBuffer::requestSyncMode(SyncMode mode) { + if (kLogger.traceEnabled()) { + kLogger.trace() << getGroup() << "EngineBuffer::requestSyncMode"; + } + if (m_playButton->get() == 0.0) { + // If we're not playing, the queued event won't get processed so do it now. + m_pEngineSync->requestSyncMode(m_pSyncControl, mode); + } else { + m_iSyncModeQueued = static_cast(mode); + } +} + +void EngineBuffer::readToCrossfadeBuffer(const int iBufferSize) { + if (!m_bCrossfadeReady) { + // Read buffer, as if there where no parameter change + // (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.toSamplePos(m_channelCount)); + m_bCrossfadeReady = true; + } +} + +// WARNING: This method is not thread safe and must not be called from outside +// the engine callback! +void EngineBuffer::setNewPlaypos(mixxx::audio::FramePos position) { + if (kLogger.traceEnabled()) { + kLogger.trace() << m_group << "EngineBuffer::setNewPlaypos" << position; + } + + m_playPos = position; + + if (m_rate_old != 0.0) { + // Before seeking, read extra buffer for crossfading + // this also sets m_pReadAheadManager to newpos + readToCrossfadeBuffer(m_iLastBufferSize); + } else { + m_pReadAheadManager->notifySeek(m_playPos.toSamplePos(m_channelCount)); + } + m_pScale->clear(); + + // Ensures that the playpos slider gets updated in next process call + m_iSamplesSinceLastIndicatorUpdate = 1000000; + + // Must hold the engineLock while using m_engineControls + for (const auto& pControl : std::as_const(m_engineControls)) { + pControl->notifySeek(m_playPos); + } + + verifyPlay(); // verify or update play button and indicator +} + +QString EngineBuffer::getGroup() const { + return m_group; +} + +double EngineBuffer::getSpeed() const { + return m_speed_old; +} + +bool EngineBuffer::getScratching() const { + return m_scratching_old; +} + +bool EngineBuffer::isReverse() const { + return m_reverse_old; +} + +// WARNING: Always called from the EngineWorker thread pool +void EngineBuffer::slotTrackLoading() { + // Pause EngineBuffer from processing frames + m_pause.lock(); + // Setting m_iTrackLoading inside a m_pause.lock ensures that + // track buffer is not processed when starting to load a new one + m_iTrackLoading = 1; + m_pause.unlock(); + + // Set play here, to signal the user that the play command is adopted + m_playButton->set((double)m_bPlayAfterLoading); + setTrackEndPosition(mixxx::audio::kInvalidFramePos); // Stop renderer +} + +void EngineBuffer::loadFakeTrack(TrackPointer pTrack, bool bPlay) { + if (bPlay) { + m_playButton->set((double)bPlay); + } + slotTrackLoaded(pTrack, + pTrack->getSampleRate(), + 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, + mixxx::audio::ChannelCount trackChannelCount, + mixxx::audio::FramePos trackNumFrame) { + if (kLogger.traceEnabled()) { + kLogger.trace() << getGroup() << "EngineBuffer::slotTrackLoaded"; + } + TrackPointer pOldTrack = m_pCurrentTrack; + m_pause.lock(); + + m_visualPlayPos->setInvalid(); + m_playPos = kInitialPlayPosition; // for execute seeks to 0.0 + m_pCurrentTrack = pTrack; + + 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); + + // Eve start + // Type + QString TrackInfoType = pTrack->getType(); + QString TrackInfoTypeTest = TrackInfoType; + int TrackInfoTypeTestLength = TrackInfoTypeTest.length(); + if (TrackInfoTypeTestLength > 5) { + TrackInfoType = TrackInfoTypeTest.mid(0, 5); + }; + m_pTrackTypeLength->set(TrackInfoTypeTestLength); + + int CharType[5]; + for (int i = 1; i <= 5; i++) { + CharType[i - 1] = 0; + } + + for (int i = 1; i <= TrackInfoType.length(); i++) { + if ((TrackInfoType.at(i - 1).toLatin1()) < 0) { + CharType[i - 1] = ((TrackInfoType.at(i - 1).toLatin1()) + 300); + } else { + CharType[i - 1] = (TrackInfoType.at(i - 1).toLatin1()); + }; + } + + double TrackTypePart = 0.0; + TrackTypePart = (1.0 * CharType[0] * 1000000000000) + (1.0 * CharType[1] * 1000000000) + (1.0 * CharType[2] * 1000000) + (1.0 * CharType[3] * 1000) + (1.0 * CharType[4] * 1); + m_pTrackType->set(TrackTypePart); + + // Title + QString TrackInfoTitle = pTrack->getTitle(); + QString TrackInfoTitleTest = TrackInfoTitle; + int TrackInfoTitleTestLength = TrackInfoTitleTest.length(); + if (TrackInfoTitleTestLength > 200) { + TrackInfoTitle = TrackInfoTitleTest.mid(0, 200); + }; + m_pTrackTitleLength->set(TrackInfoTitleTestLength); + + int CharTitle[200]; + for (int i = 1; i <= 200; i++) { + CharTitle[i - 1] = 0; + } + + // for (int i = 1; i <= TrackInfoTitle.length(); i++) { + for (int i = 1; i <= 25; i++) { + if ((TrackInfoTitle.at(i - 1).toLatin1()) < 0) { + CharTitle[i - 1] = ((TrackInfoTitle.at(i - 1).toLatin1()) + 300); + } else { + CharTitle[i - 1] = (TrackInfoTitle.at(i - 1).toLatin1()); + }; + } + + // Artist + QString TrackInfoArtist = pTrack->getArtist(); + QString TrackInfoArtistTest = TrackInfoArtist; + int TrackInfoArtistTestLength = TrackInfoArtistTest.length(); + if (TrackInfoArtistTestLength > 200) { + TrackInfoArtist = TrackInfoArtist.mid(0, 200); + }; + m_pTrackArtistLength->set(TrackInfoArtistTestLength); + + int CharArtist[200]; + for (int i = 1; i <= 200; i++) { + CharArtist[i - 1] = 0; + } + + // for (int i = 1; i <= TrackInfoArtist.length(); i++) { + for (int i = 1; i <= 25; i++) { + if ((TrackInfoArtist.at(i - 1).toLatin1()) < 0) { + CharArtist[i - 1] = ((TrackInfoArtist.at(i - 1).toLatin1()) + 300); + } else { + CharArtist[i - 1] = (TrackInfoArtist.at(i - 1).toLatin1()); + }; + } + + double TrackTitlePart_1 = 0.0; + double TrackTitlePart_2 = 0.0; + double TrackTitlePart_3 = 0.0; + double TrackTitlePart_4 = 0.0; + double TrackTitlePart_5 = 0.0; + + TrackTitlePart_1 = (1.0 * CharTitle[0] * 1000000000000) + (1.0 * CharTitle[1] * 1000000000) + (1.0 * CharTitle[2] * 1000000) + (1.0 * CharTitle[3] * 1000) + (1.0 * CharTitle[4] * 1); + TrackTitlePart_2 = (1.0 * CharTitle[5] * 1000000000000) + (1.0 * CharTitle[6] * 1000000000) + (1.0 * CharTitle[7] * 1000000) + (1.0 * CharTitle[8] * 1000) + (1.0 * CharTitle[9] * 1); + TrackTitlePart_3 = (1.0 * CharTitle[10] * 1000000000000) + (1.0 * CharTitle[11] * 1000000000) + (1.0 * CharTitle[12] * 1000000) + (1.0 * CharTitle[13] * 1000) + (1.0 * CharTitle[14] * 1); + TrackTitlePart_4 = (1.0 * CharTitle[15] * 1000000000000) + (1.0 * CharTitle[16] * 1000000000) + (1.0 * CharTitle[17] * 1000000) + (1.0 * CharTitle[18] * 1000) + (1.0 * CharTitle[19] * 1); + TrackTitlePart_5 = (1.0 * CharTitle[20] * 1000000000000) + (1.0 * CharTitle[21] * 1000000000) + (1.0 * CharTitle[22] * 1000000) + (1.0 * CharTitle[23] * 1000) + (1.0 * CharTitle[24] * 1); + + m_pTrackTitle_1->set(TrackTitlePart_1); + m_pTrackTitle_2->set(TrackTitlePart_2); + m_pTrackTitle_3->set(TrackTitlePart_3); + m_pTrackTitle_4->set(TrackTitlePart_4); + m_pTrackTitle_5->set(TrackTitlePart_5); + + double TrackArtistPart_1 = 0.0; + double TrackArtistPart_2 = 0.0; + double TrackArtistPart_3 = 0.0; + double TrackArtistPart_4 = 0.0; + double TrackArtistPart_5 = 0.0; + + TrackArtistPart_1 = (1.0 * CharArtist[0] * 1000000000000) + (1.0 * CharArtist[1] * 1000000000) + (1.0 * CharArtist[2] * 1000000) + (1.0 * CharArtist[3] * 1000) + (1.0 * CharArtist[4] * 1); + TrackArtistPart_2 = (1.0 * CharArtist[5] * 1000000000000) + (1.0 * CharArtist[6] * 1000000000) + (1.0 * CharArtist[7] * 1000000) + (1.0 * CharArtist[8] * 1000) + (1.0 * CharArtist[9] * 1); + TrackArtistPart_3 = (1.0 * CharArtist[10] * 1000000000000) + (1.0 * CharArtist[11] * 1000000000) + (1.0 * CharArtist[12] * 1000000) + (1.0 * CharArtist[13] * 1000) + (1.0 * CharArtist[14] * 1); + TrackArtistPart_4 = (1.0 * CharArtist[15] * 1000000000000) + (1.0 * CharArtist[16] * 1000000000) + (1.0 * CharArtist[17] * 1000000) + (1.0 * CharArtist[18] * 1000) + (1.0 * CharArtist[19] * 1); + TrackArtistPart_5 = (1.0 * CharArtist[20] * 1000000000000) + (1.0 * CharArtist[21] * 1000000000) + (1.0 * CharArtist[22] * 1000000) + (1.0 * CharArtist[23] * 1000) + (1.0 * CharArtist[24] * 1); + + m_pTrackArtist_1->set(TrackArtistPart_1); + m_pTrackArtist_2->set(TrackArtistPart_2); + m_pTrackArtist_3->set(TrackArtistPart_3); + m_pTrackArtist_4->set(TrackArtistPart_4); + m_pTrackArtist_5->set(TrackArtistPart_5); + +// EveOSC begin + if (m_pConfig->getValue(ConfigKey("[OSC]", "OscEnabled"))) { +// OscFunctionsSendChar(getGroup(), "TrackArtist", pTrack->getArtist().toLatin1()); +// OscFunctionsSendChar(getGroup(), "TrackTitle", pTrack->getTitle().toLatin1()); + enum DefOscBodyType OscBodyType = STRINGBODY; + OscFunctionsSendPtrType(m_pConfig, getGroup(), "TrackArtist", STRINGBODY, pTrack->getArtist().toLatin1(), 0, 0, 0); + OscFunctionsSendPtrType(m_pConfig, getGroup(), "TrackTitle", STRINGBODY, pTrack->getTitle().toLatin1(), 0, 0, 0); + + float oscTrackInfoDuration = pTrack->getDuration(); + int oscTrackInfoDurationCalcMin = oscTrackInfoDuration / 60; + int oscTrackInfoDurationCalcSec = oscTrackInfoDuration - (oscTrackInfoDurationCalcMin * 60); + + QString oscTrackInfoDurationCalc; + + if (oscTrackInfoDurationCalcSec < 10) { + oscTrackInfoDurationCalc = QString("%1:0%2").arg(oscTrackInfoDurationCalcMin).arg(oscTrackInfoDurationCalcSec); + } else { + oscTrackInfoDurationCalc = QString("%1:%2").arg(oscTrackInfoDurationCalcMin).arg(oscTrackInfoDurationCalcSec); + }; + + QByteArray oscTrackInfoDurationBa = oscTrackInfoDurationCalc.toLocal8Bit(); + const char* oscBodyMessageDuration = oscTrackInfoDurationBa.data(); + + //OscFunctionsSendChar(getGroup(), "Duration", oscBodyMessageDuration); + OscFunctionsSendPtrType(m_pConfig, getGroup(), "TDuration", STRINGBODY, oscBodyMessageDuration, 0, 0, 0); + } +// EveOSC end + + // Reset slip mode + m_pSlipButton->set(0); + m_bSlipEnabledProcessing = false; + m_slipPos = mixxx::audio::kStartFramePos; + m_dSlipRate = 0; + m_slipModeState = SlipModeState::Disabled; + + m_pReplayGain->set(pTrack->getReplayGain().getRatio()); + + m_queuedSeek.setValue(kNoQueuedSeek); + + // Reset the pitch value for the new track. + m_pause.unlock(); + + notifyTrackLoaded(pTrack, pOldTrack); + + // Check if we are cloning another channel before doing any seeking. + // This replaces m_queuedSeek populated form CueControl + EngineChannel* pChannel = atomicLoadRelaxed(m_pChannelToCloneFrom); + if (pChannel) { + m_queuedSeek.setValue(kCloneSeek); + m_iSeekPhaseQueued = 0; + } + + // Start buffer processing after all EngineContols are up to date + // with the current track e.g track is seeked to Cue + m_iTrackLoading = 0; +} + +// WARNING: Always called from the EngineWorker thread pool +void EngineBuffer::slotTrackLoadFailed(TrackPointer pTrack, + const QString& reason) { + m_iTrackLoading = 0; + m_pChannelToCloneFrom = nullptr; + + // Loading of a new track failed. + // eject the currently loaded track (the old Track) as well + ejectTrack(); + emit trackLoadFailed(pTrack, reason); +} + +void EngineBuffer::ejectTrack() { + // clear track values in any case, may fix https://github.com/mixxxdj/mixxx/issues/8000 + if (kLogger.traceEnabled()) { + kLogger.trace() << "EngineBuffer::ejectTrack()"; + } + TrackPointer pOldTrack = m_pCurrentTrack; + m_pause.lock(); + + m_visualPlayPos->set(0.0, + 0.0, + 0.0, + 0.0, + 0.0, + SlipModeState::Disabled, + false, + false, + false, + 0.0, + 0.0, + 0.0, + 0.0); + doSeekPlayPos(mixxx::audio::kStartFramePos, SEEK_EXACT); + + m_pCurrentTrack.reset(); + setTrackEndPosition(mixxx::audio::kInvalidFramePos); + m_pTrackSampleRate->set(0); + m_pTrackLoaded->forceSet(0); + + m_pTrackType->set(0); + m_pTrackTypeLength->set(0); + m_pTrackArtistLength->set(0); + m_pTrackArtist_1->set(0); + m_pTrackArtist_2->set(0); + m_pTrackArtist_3->set(0); + m_pTrackArtist_4->set(0); + m_pTrackArtist_5->set(0); + + m_pTrackTitleLength->set(0); + m_pTrackTitle_1->set(0); + m_pTrackTitle_2->set(0); + m_pTrackTitle_3->set(0); + m_pTrackTitle_4->set(0); + m_pTrackTitle_5->set(0); + + // EveOSC begin + //std::mutex mmm; + //std::lock_guard lock(mmm); + if (m_pConfig->getValue(ConfigKey("[OSC]", "OscEnabled"))) { +// OscFunctionsSendChar(getGroup(), "TrackArtist", "no track loaded"); +// OscFunctionsSendChar(getGroup(), "TrackTitle", "no track loaded"); +// OscFunctionsSendChar(getGroup(), "Duration", "0:00"); + OscFunctionsSendPtrType(m_pConfig, getGroup(), "TrackArtist", STRINGBODY, "no track loaded", 0, 0, 0); + OscFunctionsSendPtrType(m_pConfig, getGroup(), "TrackTitle", STRINGBODY, "no track loaded", 0, 0, 0); + OscFunctionsSendPtrType(m_pConfig, getGroup(), "Duration", STRINGBODY, "0:00", 0, 0, 0); + + + } + //std::lock_guard unlock(mmm); + // EveOSC end + + m_playButton->set(0.0); + m_playposSlider->set(0); + m_pCueControl->resetIndicators(); + + m_pReplayGain->set(0.0); + + m_queuedSeek.setValue(kNoQueuedSeek); + + m_pause.unlock(); + + // Close open file handles by unloading the current track + m_pReader->newTrack(TrackPointer()); + + if (pOldTrack) { + notifyTrackLoaded(TrackPointer(), pOldTrack); + } + m_iTrackLoading = 0; + m_pChannelToCloneFrom = nullptr; +} + +void EngineBuffer::notifyTrackLoaded( + TrackPointer pNewTrack, TrackPointer pOldTrack) { + if (pOldTrack) { + disconnect( + pOldTrack.get(), + &Track::beatsUpdated, + this, + &EngineBuffer::slotUpdatedTrackBeats); + } + + // First inform engineControls directly + // Note: we are still in a worker thread. + const auto trackEndPosition = getTrackEndPosition(); + const auto sampleRate = mixxx::audio::SampleRate::fromDouble(m_pTrackSampleRate->get()); + for (const auto& pControl : std::as_const(m_engineControls)) { + pControl->setFrameInfo(m_playPos, trackEndPosition, sampleRate); + pControl->trackLoaded(pNewTrack); + } + + if (pNewTrack) { + connect(pNewTrack.get(), + &Track::beatsUpdated, + this, + &EngineBuffer::slotUpdatedTrackBeats, + Qt::DirectConnection); + connect(pNewTrack.get(), + &Track::bpmLockChanged, + m_pBpmControl, + &BpmControl::trackBpmLockChanged, + Qt::DirectConnection); + bool bpmLocked = pNewTrack.get()->isBpmLocked(); + m_pBpmControl->trackBpmLockChanged(bpmLocked); + } + + // Inform BaseTrackPlayer via a queued connection + emit trackLoaded(pNewTrack, pOldTrack); +} + +void EngineBuffer::slotPassthroughChanged(double enabled) { + if (enabled != 0) { + // If passthrough was enabled, stop playing the current track. + slotControlStop(1.0); + // Disable CUE and Play indicators + m_pCueControl->resetIndicators(); + } else { + // Update CUE and Play indicators. Note: m_pCueControl->updateIndicators() + // is not sufficient. + updateIndicatorsAndModifyPlay(false, false); + } +} + +// WARNING: This method runs in both the GUI thread and the Engine Thread +void EngineBuffer::slotControlSeek(double fractionalPos) { + doSeekFractional(fractionalPos, SEEK_STANDARD); +} + +// WARNING: This method is called by EngineControl and runs in the engine thread +void EngineBuffer::seekAbs(mixxx::audio::FramePos position) { + DEBUG_ASSERT(position.isValid()); + doSeekPlayPos(position, SEEK_STANDARD); +} + +// WARNING: This method is called by EngineControl and runs in the engine thread +void EngineBuffer::seekExact(mixxx::audio::FramePos position) { + DEBUG_ASSERT(position.isValid()); + doSeekPlayPos(position, SEEK_EXACT); +} + +double EngineBuffer::fractionalPlayposFromAbsolute(mixxx::audio::FramePos absolutePlaypos) { + if (!m_trackEndPositionOld.isValid()) { + return 0.0; + } + + const auto position = std::min(absolutePlaypos, m_trackEndPositionOld); + return position.value() / m_trackEndPositionOld.value(); +} + +void EngineBuffer::doSeekFractional(double fractionalPos, enum SeekRequest seekType) { + // Prevent NaN's from sneaking into the engine. + VERIFY_OR_DEBUG_ASSERT(!util_isnan(fractionalPos)) { + return; + } + + // FIXME: Use maybe invalid here + const mixxx::audio::FramePos trackEndPosition = getTrackEndPosition(); + VERIFY_OR_DEBUG_ASSERT(trackEndPosition.isValid()) { + return; + } + const auto seekPosition = trackEndPosition * fractionalPos; + doSeekPlayPos(seekPosition, seekType); +} + +void EngineBuffer::doSeekPlayPos(mixxx::audio::FramePos position, enum SeekRequest seekType) { +#ifdef __VINYLCONTROL__ + // Notify the vinyl control that a seek has taken place in case it is in + // absolute mode and needs be switched to relative. + if (m_pVinylControlControl) { + m_pVinylControlControl->notifySeekQueued(); + } +#endif + + queueNewPlaypos(position, seekType); +} + +bool EngineBuffer::updateIndicatorsAndModifyPlay(bool newPlay, bool oldPlay) { + // If no track is currently loaded, turn play off. If a track is loading + // allow the set since it might apply to a track we are loading due to the + // asynchrony. + bool playPossible = true; + const QueuedSeek queuedSeek = m_queuedSeek.getValue(); + if ((!m_pCurrentTrack && atomicLoadRelaxed(m_iTrackLoading) == 0) || + (m_pCurrentTrack && atomicLoadRelaxed(m_iTrackLoading) == 0 && + m_playPos >= getTrackEndPosition() && + queuedSeek.seekType == SEEK_NONE) || + m_pPassthroughEnabled->toBool()) { + // play not possible + playPossible = false; + } + + return m_pCueControl->updateIndicatorsAndModifyPlay(newPlay, oldPlay, playPossible); +} + +void EngineBuffer::verifyPlay() { + bool play = m_playButton->toBool(); + bool verifiedPlay = updateIndicatorsAndModifyPlay(play, play); + if (play != verifiedPlay) { + m_playButton->setAndConfirm(verifiedPlay ? 1.0 : 0.0); + } +} + +void EngineBuffer::slotControlPlayRequest(double v) { + bool oldPlay = m_playButton->toBool(); + bool verifiedPlay = updateIndicatorsAndModifyPlay(v > 0.0, oldPlay); + + if (!oldPlay && verifiedPlay) { + if (m_pQuantize->toBool() +#ifdef __VINYLCONTROL__ + && m_pVinylControlControl && !m_pVinylControlControl->isEnabled() +#endif + ) { + requestSyncPhase(); + } + } + + // set and confirm must be called here in any case to update the widget toggle state + m_playButton->setAndConfirm(verifiedPlay ? 1.0 : 0.0); +} + +void EngineBuffer::slotControlStart(double v) +{ + if (v > 0.0) { + doSeekFractional(0., SEEK_EXACT); + } +} + +void EngineBuffer::slotControlEnd(double v) +{ + if (v > 0.0) { + doSeekFractional(1., SEEK_EXACT); + } +} + +void EngineBuffer::slotControlPlayFromStart(double v) +{ + if (v > 0.0) { + doSeekFractional(0., SEEK_EXACT); + m_playButton->set(1); + } +} + +void EngineBuffer::slotControlJumpToStartAndStop(double v) +{ + if (v > 0.0) { + doSeekFractional(0., SEEK_EXACT); + m_playButton->set(0); + } +} + +void EngineBuffer::slotControlStop(double v) +{ + if (v > 0.0) { + m_playButton->set(0); + } +} + +void EngineBuffer::slotKeylockEngineChanged(double dIndex) { + if (m_bScalerOverride) { + return; + } + const KeylockEngine engine = static_cast(dIndex); + switch (engine) { + case KeylockEngine::SoundTouch: + m_pScaleKeylock = m_pScaleST; + break; +#ifdef __RUBBERBAND__ + case KeylockEngine::RubberBandFaster: + m_pScaleRB->useEngineFiner(false); + m_pScaleKeylock = m_pScaleRB; + break; + case KeylockEngine::RubberBandFiner: + m_pScaleRB->useEngineFiner( + true); // in case of Rubberband V2 it falls back to RUBBERBAND_FASTER + m_pScaleKeylock = m_pScaleRB; + break; +#endif + default: + slotKeylockEngineChanged(static_cast(defaultKeylockEngine())); + break; + } +} + +void EngineBuffer::processTrackLocked( + CSAMPLE* pOutput, const int iBufferSize, mixxx::audio::SampleRate sampleRate) { + ScopedTimer t(QStringLiteral("EngineBuffer::process_pauselock")); + + m_trackSampleRateOld = mixxx::audio::SampleRate::fromDouble(m_pTrackSampleRate->get()); + m_trackEndPositionOld = getTrackEndPosition(); + + double baseSampleRate = 0.0; + if (sampleRate.isValid()) { + baseSampleRate = m_trackSampleRateOld / sampleRate; + } + + // Sync requests can affect rate, so process those first. + processSyncRequests(); + + // Note: play is also active during cue preview + bool paused = !m_playButton->toBool(); + KeyControl::PitchTempoRatio pitchTempoRatio = m_pKeyControl->getPitchTempoRatio(); + + // The pitch adjustment in Ratio (1.0 being normal + // pitch. 2.0 is a full octave shift up). + double pitchRatio = pitchTempoRatio.pitchRatio; + double tempoRatio = pitchTempoRatio.tempoRatio; + const bool keylock_enabled = pitchTempoRatio.keylock; + + bool is_scratching = false; + bool is_reverse = false; + + // Update the slipped position and seek to it if slip mode was disabled. + processSlip(iBufferSize); + + // Note: This may affect the m_playPos, play, scaler and crossfade buffer + processSeek(paused); + + // speed is the ratio between track-time and real-time + // (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, + outputBufferSize, + &is_scratching, + &is_reverse); + + bool useIndependentPitchAndTempoScaling = false; + + // TODO(owen): Maybe change this so that rubberband doesn't disable + // keylock on scratch. (just check m_pScaleKeylock == m_pScaleST) + if (is_scratching || fabs(speed) > 1.9) { + // Scratching and high speeds with always disables keylock + // because Soundtouch sounds terrible in these conditions. Rubberband + // sounds better, but still has some problems (it may reallocate in + // a party-crashing manner at extremely slow speeds). + // High seek speeds also disables keylock. Our pitch slider could go + // to 90%, so that's the cutoff point. + + // Force pitchRatio to the linear pitch set by speed + pitchRatio = speed; + // This is for the natural speed pitch found on turn tables + } else if (fabs(speed) < 0.1) { + // We have pre-allocated big buffers in Rubberband and Soundtouch for + // a minimum speed of 0.1. Slower speeds will re-allocate much bigger + // buffers which may cause underruns. + // Disable keylock under these conditions. + + // Force pitchRatio to the linear pitch set by speed + pitchRatio = speed; + } else if (keylock_enabled) { + // always use IndependentPitchAndTempoScaling + // to avoid clicks when crossing the linear pitch + // in this case it is most likely that the user + // will have an non linear pitch + // Note: We have undesired noise when cossfading between scalers + useIndependentPitchAndTempoScaling = true; + } else { + // We might have have temporary speed change, so adjust pitch if not locked + // Note: This will not update key and tempo widgets + if (tempoRatio != 0) { + pitchRatio *= (speed / tempoRatio); + } + + // Check if we are off-linear (musical key has been adjusted + // independent from speed) to determine if the keylock scaler + // should be used even though keylock is disabled. + if (speed != 0.0) { + double offlinear = pitchRatio / speed; + if (offlinear > kLinearScalerElipsis || + offlinear < 1 / kLinearScalerElipsis) { + // only enable keylock scaler if pitch adjustment is at + // least 1 cent. Everything below is not hear-able. + useIndependentPitchAndTempoScaling = true; + } + } + } + + if (speed != 0.0) { + // Do not switch scaler when we have no transport + enableIndependentPitchTempoScaling(useIndependentPitchAndTempoScaling, + iBufferSize); + } else if (m_speed_old != 0 && !is_scratching) { + // we are stopping, collect samples for fade out + readToCrossfadeBuffer(iBufferSize); + // Clear the scaler information + m_pScale->clear(); + } + + // How speed/tempo/pitch are related: + // Processing is done in two parts, the first part is calculated inside + // the KeyKontrol class and effects the visual key/pitch widgets. + // The Speed slider controls the tempoRatio and a speedSliderPitchRatio, + // the pitch amount caused by it. + // By default the speed slider controls pitch and tempo with the same + // value. + // If key lock is enabled, the speedSliderPitchRatio is decoupled from + // the speed slider (const). + // + // With preference mode KeylockMode = kLockOriginalKey + // the speedSliderPitchRatio is reset to 1 and back to the tempoRatio + // (natural vinyl Pitch) when keylock is disabled and enabled. + // + // With preference mode KeylockMode = kCurrentKey + // the speedSliderPitchRatio is not reset when keylock is enabled. + // This mode allows to enable keylock + // while the track is already played. You can reset to the tracks + // original pitch by resetting the pitch knob to center. When disabling + // keylock the pitch is reset to the linear vinyl pitch. + + // The Pitch knob turns if the speed slider is moved without keylock. + // This is useful to get always an analog impression of current pitch, + // and its distance to the original track pitch + // + // The Pitch_Adjust knob does not reflect the speedSliderPitchRatio. + // So it is is useful for controller mappings, because it is not + // changed by the speed slider or keylock. + + // In the second part all other speed changing controls are processed. + // They may produce an additional pitch if keylock is disabled or + // override the pitch in scratching case. + // If pitch ratio and tempo ratio are equal, a linear scaler is used, + // otherwise tempo and pitch are processed individual + + double rate = 0; + // If the base samplerate, speed, or pitch has changed, we need to update the + // scaler. Also, if we have changed scalers then we need to update the + // scaler. + if (baseSampleRate != m_baserate_old || speed != m_speed_old || + pitchRatio != m_pitch_old || tempoRatio != m_tempo_ratio_old || + m_bScalerChanged) { + // The rate returned by the scale object can be different from the + // wanted rate! Make sure new scaler has proper position. This also + // crossfades between the old scaler and new scaler to prevent + // clicks. + + // Handle direction change. + // The linear scaler supports ramping though zero. + // This is used for scratching, but not for reverse + // For the other, crossfade forward and backward samples + if ((m_speed_old * speed < 0) && // Direction has changed! + (m_pScale != m_pScaleVinyl || // only m_pScaleLinear supports going though 0 + m_reverse_old != is_reverse)) { // no pitch change when reversing + //XXX: Trying to force RAMAN to read from correct + // playpos when rate changes direction - Albert + readToCrossfadeBuffer(iBufferSize); + // Clear the scaler information + m_pScale->clear(); + } + + m_baserate_old = baseSampleRate; + m_speed_old = speed; + m_pitch_old = pitchRatio; + m_tempo_ratio_old = tempoRatio; + m_reverse_old = is_reverse; + + // Now we need to update the scaler with the main sample rate, the + // base rate (ratio between sample rate of the source audio and the + // main samplerate), the deck speed, the pitch shift, and whether + // the deck speed should affect the pitch. + + m_pScale->setScaleParameters(baseSampleRate, + &speed, + &pitchRatio); + + // The way we treat rate inside of EngineBuffer is actually a + // description of "sample consumption rate" or percentage of samples + // consumed relative to playing back the track at its native sample + // rate and normal speed. pitch_adjust does not change the playback + // rate. + rate = baseSampleRate * speed; + + // Scaler is up to date now. + m_bScalerChanged = false; + } else { + // Scaler did not need updating. By definition this means we are at + // our old rate. + rate = m_rate_old; + } + + const mixxx::audio::FramePos playpos_old = m_playPos; + bool bCurBufferPaused = false; + bool atEnd = false; + bool backwards = rate < 0; + const mixxx::audio::FramePos trackEndPosition = getTrackEndPosition(); + if (trackEndPosition.isValid()) { + atEnd = m_playPos >= trackEndPosition; + if (atEnd && !backwards) { + // do not play past end + bCurBufferPaused = true; + } else if (rate == 0 && !is_scratching) { + // do not process samples if have no transport + // the linear scaler supports ramping down to 0 + // this is used for pause by scratching only + bCurBufferPaused = true; + } + } else { + // Track has already been ejected. + bCurBufferPaused = true; + } + + m_rate_old = rate; + + // If the buffer is not paused, then scale the audio. + if (!bCurBufferPaused) { + // Perform scaling of Reader buffer into buffer. + const double framesRead = m_pScale->scaleBuffer(pOutput, iBufferSize); + + // TODO(XXX): The result framesRead might not be an integer value. + // Converting to samples here does not make sense. All positional + // calculations should be done in frames instead of samples! Otherwise + // rounding errors might occur when converting from samples back to + // frames later. + + if (m_bScalerOverride) { + // If testing, we don't have a real log so we fake the position. + m_playPos += framesRead; + } else { + // Adjust filepos_play by the amount we processed. + 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 + // callback and the m_playPos is advanced behind the end of the track. + // If repeat is enabled, scaler->scaleBuffer() wraps around at end/start + // and fills the buffer with samples from the other end of the track. + + if (m_bCrossfadeReady) { + // Bring pOutput with the new parameters in and fade out the old one, + // stored with the old parameters in m_pCrossfadeBuffer + SampleUtil::linearCrossfadeBuffersIn( + pOutput, m_pCrossfadeBuffer, iBufferSize, m_channelCount); + } + // Note: we do not fade here if we pass the end or the start of + // the track in reverse direction + // because we assume that the track samples itself start and stop + // towards zero. + // If it turns out that ramping is required be aware that the end + // or start may pass in the middle of the buffer. + } else { + // Pause + if (m_bCrossfadeReady) { + // We don't ramp here, since EnginePregain handles fades + // from and to speed == 0 + SampleUtil::copy(pOutput, m_pCrossfadeBuffer, iBufferSize); + } else { + SampleUtil::clear(pOutput, iBufferSize); + } + } + + m_actual_speed = (m_playPos - playpos_old) / (iBufferSize / 2); + // qDebug() << "Ramped Speed" << m_actual_speed / m_speed_old; + + for (const auto& pControl : std::as_const(m_engineControls)) { + // m_playPos is already updated here and points to the end of the played buffer + pControl->setFrameInfo(m_playPos, trackEndPosition, m_trackSampleRateOld); + pControl->process(rate, m_playPos, iBufferSize); + } + + m_scratching_old = is_scratching; + + // If we're repeating and crossed the track boundary, ReadAheadManager already + // wrapped around the playposition. + // To ensure quantize is respected we request a phase sync. + // TODO(ronso) This just restores previous repeat+quantize behaviour. I'm not + // sure whether that was actually desired or just a side effect of seeking. + // Ife it's really desired, should this be moved to looping control in order + // to set the sync'ed playposition right away and fill the wrap-around buffer + // with correct samples from the sync'ed loop in / track start position? + if (m_pRepeat->toBool() && m_pQuantize->toBool() && + (m_playPos > playpos_old) == backwards) { + // TODO() The resulting seek is processed in the following callback + // That is to late + requestSyncPhase(); + } + + bool end_of_track = atEnd && !backwards; + + // If playbutton is pressed and we're at the end of track release play button + if (m_playButton->toBool() && end_of_track) { + m_playButton->set(0.); + } + + // Give the Reader hints as to which chunks of the current song we + // really care about. It will try very hard to keep these in memory + hintReader(rate); +} + +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 % m_channelCount) == 0) { + return; + } + m_pReader->process(); + // Steps: + // - Lookup new reader information + // - Calculate current rate + // - Scale the audio with m_pScale, copy the resulting samples into the + // output buffer + // - Give EngineControl's a chance to do work / request seeks, etc + // - Process repeat mode if we're at the end or beginning of a track + // - Set last sample value (m_fLastSampleValue) so that rampOut works? Other + // miscellaneous upkeep issues. + + m_sampleRate = mixxx::audio::SampleRate::fromDouble(m_pSampleRate->get()); + + // 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->setSignal(m_sampleRate, m_channelCount); + m_pScaleST->setSignal(m_sampleRate, m_channelCount); +#ifdef __RUBBERBAND__ + m_pScaleRB->setSignal(m_sampleRate, m_channelCount); +#endif + + bool hasStableTrack = m_pTrackLoaded->toBool() && m_iTrackLoading.loadAcquire() == 0; + if (hasStableTrack && m_pause.tryLock()) { + processTrackLocked(pOutput, iBufferSize, m_sampleRate); + // release the pauselock + m_pause.unlock(); + } else { + // We are loading a new Track + + // Here the old track was playing and loading the new track is in + // progress. We can't predict when it happens, so we are not able + // to collect old samples. New samples are also not in place and + // we can't predict when they will be in place. + // If one does this, a click from breaking the last track is somehow + // natural and he should know that such sound should not be played to + // the main (audience). + // Workaround: Simply pause the track before. + + // TODO(XXX): + // A click free solution requires more refactoring how loading a track + // is handled. For now we apply a rectangular Gain change here which + // may click. + + SampleUtil::clear(pOutput, iBufferSize); + + m_rate_old = 0; + m_speed_old = 0; + m_actual_speed = 0; + m_scratching_old = false; + } + +#ifdef __SCALER_DEBUG__ + for (int i=0; iupdateAudible(); + + m_iLastBufferSize = iBufferSize; + m_bCrossfadeReady = false; +} + +void EngineBuffer::processSlip(int iBufferSize) { + // Do a single read from m_bSlipEnabled so we don't run in to race conditions. + bool enabled = m_pSlipButton->toBool(); + if (enabled != m_bSlipEnabledProcessing) { + m_bSlipEnabledProcessing = enabled; + if (enabled) { + m_slipPos = m_playPos; + m_dSlipRate = m_rate_old; + } else { + // TODO(owen) assuming that looping will get canceled properly + seekExact(m_slipPos.toNearestFrameBoundary()); + m_slipPos = mixxx::audio::kStartFramePos; + } + } + + // 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 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 / 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 + if (m_pLoopingControl->isLoopingEnabled() && + m_pLoopingControl->loopWasEnabledBeforeSlipEnable() && + !m_pLoopingControl->isLoopRollActive()) { + const mixxx::audio::FramePos newPos = m_slipPos + slipDelta; + m_slipPos = m_pLoopingControl->adjustedPositionForCurrentLoop( + newPos, + m_dSlipRate < 0); + m_slipModeState = SlipModeState::Armed; + } else { + m_slipPos += slipDelta; + m_slipModeState = SlipModeState::Running; + } + } else { + m_slipModeState = SlipModeState::Disabled; + } +} + +void EngineBuffer::processSyncRequests() { + SyncRequestQueued enable_request = + static_cast( + m_iEnableSyncQueued.fetchAndStoreRelease(SYNC_REQUEST_NONE)); + SyncMode mode_request = + static_cast(m_iSyncModeQueued.fetchAndStoreRelease( + static_cast(SyncMode::Invalid))); + switch (enable_request) { + case SYNC_REQUEST_ENABLE: + m_pEngineSync->requestSyncMode(m_pSyncControl, SyncMode::Follower); + break; + case SYNC_REQUEST_DISABLE: + m_pEngineSync->requestSyncMode(m_pSyncControl, SyncMode::None); + break; + case SYNC_REQUEST_ENABLEDISABLE: + m_pEngineSync->requestSyncMode(m_pSyncControl, SyncMode::Follower); + m_pEngineSync->requestSyncMode(m_pSyncControl, SyncMode::None); + break; + case SYNC_REQUEST_NONE: + break; + } + if (mode_request != SyncMode::Invalid) { + m_pEngineSync->requestSyncMode(m_pSyncControl, + static_cast(mode_request)); + } +} + +void EngineBuffer::processSeek(bool paused) { + m_previousBufferSeek = false; + + const QueuedSeek queuedSeek = m_queuedSeek.getValue(); + + SeekRequests seekType = queuedSeek.seekType; + mixxx::audio::FramePos position = queuedSeek.position; + + // Add SEEK_PHASE bit, if any + if (m_iSeekPhaseQueued.fetchAndStoreRelease(0)) { + seekType |= SEEK_PHASE; + } + + switch (seekType) { + case SEEK_NONE: + return; + case SEEK_PHASE: + // only adjust phase + position = m_playPos; + break; + case SEEK_STANDARD: + if (m_pQuantize->toBool()) { + seekType |= SEEK_PHASE; + } + // new position was already set above + break; + case SEEK_EXACT: + case SEEK_EXACT_PHASE: // artificial state = SEEK_EXACT | SEEK_PHASE + case SEEK_STANDARD_PHASE: // artificial state = SEEK_STANDARD | SEEK_PHASE + // new position was already set above + break; + case SEEK_CLONE: { + // Cloning another channels position. + EngineChannel* pOtherChannel = m_pChannelToCloneFrom.fetchAndStoreRelaxed(nullptr); + VERIFY_OR_DEBUG_ASSERT(pOtherChannel) { + return; + } + position = pOtherChannel->getEngineBuffer()->getExactPlayPos(); + } break; + default: + DEBUG_ASSERT(!"Unhandled seek request type"); + m_queuedSeek.setValue(kNoQueuedSeek); + return; + } + + VERIFY_OR_DEBUG_ASSERT(position.isValid()) { + return; + } + + // Don't allow the playposition to go past the end. + position = std::min(position, m_trackEndPositionOld); + + if (!paused && (seekType & SEEK_PHASE)) { + if (kLogger.traceEnabled()) { + kLogger.trace() << "EngineBuffer::processSeek" << getGroup() << "Seeking phase"; + } + const mixxx::audio::FramePos syncPosition = + m_pBpmControl->getBeatMatchPosition(position, true, true); + position = m_pLoopingControl->getSyncPositionInsideLoop(position, syncPosition); + if (kLogger.traceEnabled()) { + kLogger.trace() + << "EngineBuffer::processSeek" << getGroup() << "seek info:" << m_playPos + << "->" << position; + } + } + if (position != m_playPos) { + if (kLogger.traceEnabled()) { + kLogger.trace() << "EngineBuffer::processSeek" << getGroup() << "Seek to" << position; + } + setNewPlaypos(position); + m_previousBufferSeek = true; + } + // Reset the m_queuedSeek value after it has been processed in + // setNewPlaypos() so that the Engine Controls have always access to the + // position of the upcoming buffer cycle (used for loop cues) + m_queuedSeek.setValue(kNoQueuedSeek); +} + +void EngineBuffer::postProcessLocalBpm() { + m_pBpmControl->updateLocalBpm(); +} + +void EngineBuffer::postProcess(const int iBufferSize) { + // The order of events here is very delicate. It's necessary to update + // some values before others, because the later updates may require + // values from the first update. Do not make calls here that could affect + // which Syncable is leader or could cause Syncables to try to match + // beat distances. During these calls those values are inconsistent. + if (kLogger.traceEnabled()) { + kLogger.trace() << getGroup() << "EngineBuffer::postProcess"; + } + const mixxx::Bpm localBpm = m_pBpmControl->getLocalBpm(); + double beatDistance = m_pBpmControl->updateBeatDistance(); + const SyncMode mode = m_pSyncControl->getSyncMode(); + if (localBpm.isValid()) { + m_pSyncControl->setLocalBpm(localBpm); + m_pSyncControl->reportPlayerSpeed(m_speed_old, m_scratching_old); + if (isLeader(mode)) { + m_pEngineSync->notifyBeatDistanceChanged(m_pSyncControl, beatDistance); + } else if (isFollower(mode)) { + m_pSyncControl->updateTargetBeatDistance(); + } + } else if (mode == SyncMode::LeaderSoft) { + // If this channel has been automatically chosen to be the leader but + // no BPM is available, another channel may take over leadership and + // this channel becomes a follower. This may happen if the track is + // analyzed upon load and avoids sudden tempo jumps on the other deck + // while the analysis is still running. + requestSyncMode(SyncMode::Follower); + } + + // Update all the indicators that EngineBuffer publishes to allow + // external parts of Mixxx to observe its status. + updateIndicators(m_speed_old, iBufferSize); +} + +mixxx::audio::FramePos EngineBuffer::queuedSeekPosition() const { + const QueuedSeek queuedSeek = m_queuedSeek.getValue(); + if (queuedSeek.seekType == SEEK_NONE) { + return {}; + } + + return queuedSeek.position; +} + +void EngineBuffer::updateIndicators(double speed, int iBufferSize) { + if (!m_playPos.isValid() || + !m_trackSampleRateOld.isValid() || + m_pPassthroughEnabled->toBool()) { + // Skip indicator updates with invalid values to prevent undefined behavior, + // e.g. in WaveformRenderBeat::draw(). + // + // This is known to happen if Deck Passthrough is active, when either no + // track is loaded or a track was loaded but processSeek() has not been + // called yet. + return; + } + + // Increase samplesCalculated by the buffer size + m_iSamplesSinceLastIndicatorUpdate += iBufferSize; + + const double fFractionalPlaypos = fractionalPlayposFromAbsolute(m_playPos); + const double fFractionalSlipPos = fractionalPlayposFromAbsolute(m_slipPos); + + auto loopInfo = m_pLoopingControl->getLoopInfo(); + + double fFractionalLoopStartPos = 0.0; + if (loopInfo.startPosition.isValid()) { + fFractionalLoopStartPos = fractionalPlayposFromAbsolute(loopInfo.startPosition); + } + double fFractionalLoopEndPos = 0.0; + if (loopInfo.endPosition.isValid()) { + fFractionalLoopEndPos = fractionalPlayposFromAbsolute(loopInfo.endPosition); + } + + const double tempoTrackSeconds = m_trackEndPositionOld.value() / + m_trackSampleRateOld / getRateRatio(); + if (speed > 0 && fFractionalPlaypos == 1.0) { + // Play pos at Track end + speed = 0; + } + + double effectiveSlipRate = m_dSlipRate; + if (effectiveSlipRate > 0.0 && fFractionalSlipPos == 1.0) { + // Slip pos at Track end + effectiveSlipRate = 0.0; + } + + // Update indicators that are only updated after every + // sampleRate/kiUpdateRate samples processed. (e.g. playposSlider) + if (m_iSamplesSinceLastIndicatorUpdate > + (mixxx::kEngineChannelOutputCount * m_pSampleRate->get() / + kPlaypositionUpdateRate)) { + m_playposSlider->set(fFractionalPlaypos); + m_pCueControl->updateIndicators(); + } + + // Update visual control object, this needs to be done more often than the + // playpos slider + m_visualPlayPos->set( + fFractionalPlaypos, + speed * m_baserate_old, + static_cast(iBufferSize) / + m_trackEndPositionOld.toEngineSamplePos(), + fFractionalSlipPos, + effectiveSlipRate, + m_slipModeState, + m_pLoopingControl->isLoopingEnabled(), + m_pLoopingControl->isAdjustLoopInActive(), + m_pLoopingControl->isAdjustLoopOutActive(), + fFractionalLoopStartPos, + fFractionalLoopEndPos, + tempoTrackSeconds, + 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. + // Via the visual play position it's possible to access to the sample that is currently played, + // and not the one that have been processed as in the current solution. + m_pClockControl->updateIndicators(speed * m_baserate_old, m_playPos, m_sampleRate); +} + +void EngineBuffer::hintReader(const double dRate) { + m_hintList.clear(); + m_pReadAheadManager->hintReader(dRate, &m_hintList, m_channelCount); + + //if slipping, hint about virtual position so we're ready for it + if (m_bSlipEnabledProcessing) { + Hint hint; + hint.frame = static_cast(m_slipPos.toLowerFrameBoundary().value()); + hint.type = Hint::Type::SlipPosition; + if (m_dSlipRate >= 0) { + hint.frameCount = Hint::kFrameCountForward; + } else { + hint.frameCount = Hint::kFrameCountBackward; + } + m_hintList.append(hint); + } + + for (const auto& pControl : std::as_const(m_engineControls)) { + pControl->hintReader(&m_hintList); + } + m_pReader->hintAndMaybeWake(m_hintList); +} + +// WARNING: This method runs in the GUI thread +#ifdef __STEM__ +void EngineBuffer::loadTrack(TrackPointer pTrack, + uint stemMask, + bool play, + EngineChannel* pChannelToCloneFrom) { +#else +void EngineBuffer::loadTrack(TrackPointer pTrack, + bool play, + EngineChannel* pChannelToCloneFrom) { +#endif + if (pTrack) { + // Signal to the reader to load the track. The reader will respond with + // trackLoading and then either with trackLoaded or trackLoadFailed signals. + m_bPlayAfterLoading = play; +#ifdef __STEM__ + m_pReader->newTrack(pTrack, stemMask); +#else + m_pReader->newTrack(pTrack); +#endif + atomicStoreRelaxed(m_pChannelToCloneFrom, pChannelToCloneFrom); + } else { + // Loading a null track means "eject" + ejectTrack(); + } +} + +void EngineBuffer::addControl(EngineControl* pControl) { + // Connect to signals from EngineControl here... + m_engineControls.push_back(pControl); + pControl->setEngineBuffer(this); +} + +bool EngineBuffer::isTrackLoaded() const { + if (m_pCurrentTrack) { + return true; + } + return false; +} + +TrackPointer EngineBuffer::getLoadedTrack() const { + return m_pCurrentTrack; +} + +mixxx::audio::FramePos EngineBuffer::getExactPlayPos() const { + // Is updated during postProcess(), after all decks already have been processed + if (!m_visualPlayPos->isValid()) { + return mixxx::audio::kStartFramePos; + } + return getTrackEndPosition() * m_visualPlayPos->getEnginePlayPos(); +} + +double EngineBuffer::getVisualPlayPos() const { + return m_visualPlayPos->getEnginePlayPos(); +} + +mixxx::audio::FramePos EngineBuffer::getTrackEndPosition() const { + return mixxx::audio::FramePos::fromEngineSamplePosMaybeInvalid( + m_pTrackSamples->get()); +} + +void EngineBuffer::setTrackEndPosition(mixxx::audio::FramePos position) { + m_pTrackSamples->set(position.toEngineSamplePosMaybeInvalid()); +} + +double EngineBuffer::getUserOffset() const { + return m_pBpmControl->getUserOffset(); +} + +double EngineBuffer::getRateRatio() const { + if (m_pBpmControl != nullptr) { + return m_pBpmControl->getRateRatio(); + } + return 1.0; +} + +void EngineBuffer::collectFeatures(GroupFeatureState* pGroupFeatures) const { + if (m_pBpmControl != nullptr) { + m_pBpmControl->collectFeatures(pGroupFeatures, m_actual_speed); + } +} + +void EngineBuffer::slotUpdatedTrackBeats() { + TrackPointer pTrack = m_pCurrentTrack; + if (pTrack) { + for (const auto& pControl : std::as_const(m_engineControls)) { + pControl->trackBeatsUpdated(pTrack->getBeats()); + } + } +} + +void EngineBuffer::setScalerForTest( + EngineBufferScale* pScaleVinyl, + EngineBufferScale* pScaleKeylock) { + m_pScaleVinyl = pScaleVinyl; + m_pScaleKeylock = pScaleKeylock; + m_pScale = m_pScaleVinyl; + m_pScale->clear(); + m_bScalerChanged = true; + // This bool is permanently set and can't be undone. + m_bScalerOverride = true; +} diff --git a/src/engine/cachingreader/cachingreader.cpp b/src/engine/cachingreader/cachingreader.cpp index d2b868fa353..eb8953611db 100644 --- a/src/engine/cachingreader/cachingreader.cpp +++ b/src/engine/cachingreader/cachingreader.cpp @@ -205,7 +205,11 @@ CachingReaderChunkForOwner* CachingReader::lookupChunkAndFreshen(SINT chunkIndex } // Invoked from the UI thread!! +#ifdef __STEM__ +void CachingReader::newTrack(TrackPointer pTrack, uint stemMask) { +#else void CachingReader::newTrack(TrackPointer pTrack) { +#endif auto newState = pTrack ? STATE_TRACK_LOADING : STATE_TRACK_UNLOADING; auto oldState = m_state.fetchAndStoreAcquire(newState); @@ -220,7 +224,11 @@ void CachingReader::newTrack(TrackPointer pTrack) { kLogger.warning() << "Loading a new track while loading a track may lead to inconsistent states"; } +#ifdef __STEM__ + m_worker.newTrack(std::move(pTrack), stemMask); +#else m_worker.newTrack(std::move(pTrack)); +#endif } // Called from the engine thread diff --git a/src/engine/cachingreader/cachingreader.h b/src/engine/cachingreader/cachingreader.h index 384f12a8047..05fc71e5c9a 100644 --- a/src/engine/cachingreader/cachingreader.h +++ b/src/engine/cachingreader/cachingreader.h @@ -116,7 +116,11 @@ class CachingReader : public QObject { // Request that the CachingReader load a new track. These requests are // processed in the work thread, so the reader must be woken up via wake() // for this to take effect. +#ifdef __STEM__ + void newTrack(TrackPointer pTrack, uint stemMask = 0); +#else void newTrack(TrackPointer pTrack); +#endif void setScheduler(EngineWorkerScheduler* pScheduler) { m_worker.setScheduler(pScheduler); diff --git a/src/engine/cachingreader/cachingreaderworker.cpp b/src/engine/cachingreader/cachingreaderworker.cpp index cb4d12488e4..bed83f053dd 100644 --- a/src/engine/cachingreader/cachingreaderworker.cpp +++ b/src/engine/cachingreader/cachingreaderworker.cpp @@ -92,10 +92,20 @@ ReaderStatusUpdate CachingReaderWorker::processReadRequest( } // WARNING: Always called from a different thread (GUI) +#ifdef __STEM__ +void CachingReaderWorker::newTrack(TrackPointer pTrack, uint stemMask) { +#else void CachingReaderWorker::newTrack(TrackPointer pTrack) { +#endif { const auto locker = lockMutex(&m_newTrackMutex); +#ifdef __STEM__ + m_pNewTrack = NewTrackRequest{ + pTrack, + stemMask}; +#else m_pNewTrack = pTrack; +#endif m_newTrackAvailable.storeRelease(1); } workReady(); @@ -113,16 +123,25 @@ void CachingReaderWorker::run() { // Request is initialized by reading from FIFO CachingReaderChunkReadRequest request; if (m_newTrackAvailable.loadAcquire()) { +#ifdef __STEM__ + NewTrackRequest pLoadTrack; +#else TrackPointer pLoadTrack; +#endif { // locking scope const auto locker = lockMutex(&m_newTrackMutex); pLoadTrack = m_pNewTrack; - m_pNewTrack.reset(); m_newTrackAvailable.storeRelease(0); } // implicitly unlocks the mutex +#ifdef __STEM__ + if (pLoadTrack.track) { + // in this case the engine is still running with the old track + loadTrack(pLoadTrack.track, pLoadTrack.stemMask); +#else if (pLoadTrack) { // in this case the engine is still running with the old track loadTrack(pLoadTrack); +#endif } else { // here, the engine is already stopped unloadTrack(); @@ -168,7 +187,11 @@ void CachingReaderWorker::unloadTrack() { m_pReaderStatusFIFO->writeBlocking(&update, 1); } +#ifdef __STEM__ +void CachingReaderWorker::loadTrack(const TrackPointer& pTrack, uint stemMask) { +#else void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { +#endif // This emit is directly connected and returns synchronized // after the engine has been stopped. emit trackLoading(); @@ -190,6 +213,9 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { mixxx::AudioSource::OpenParams config; config.setChannelCount(m_maxSupportedChannel); +#ifdef __STEM__ + config.setStemMask(stemMask); +#endif m_pAudioSource = SoundSourceProxy(pTrack).openAudioSource(config); if (!m_pAudioSource) { kLogger.warning() diff --git a/src/engine/cachingreader/cachingreaderworker.h b/src/engine/cachingreader/cachingreaderworker.h index a99bbb150a6..d4c8320fe04 100644 --- a/src/engine/cachingreader/cachingreaderworker.h +++ b/src/engine/cachingreader/cachingreaderworker.h @@ -104,7 +104,11 @@ class CachingReaderWorker : public EngineWorker { ~CachingReaderWorker() override = default; // Request to load a new track. wake() must be called afterwards. +#ifdef __STEM__ + void newTrack(TrackPointer pTrack, uint stemMask); +#else void newTrack(TrackPointer pTrack); +#endif // Run upkeep operations like loading tracks and reading from file. Run by a // thread pool via the EngineWorkerScheduler. @@ -122,6 +126,12 @@ class CachingReaderWorker : public EngineWorker { void trackLoadFailed(TrackPointer pTrack, const QString& reason); private: +#ifdef __STEM__ + struct NewTrackRequest { + TrackPointer track; + uint stemMask; + }; +#endif const QString m_group; QString m_tag; @@ -134,7 +144,11 @@ class CachingReaderWorker : public EngineWorker { // lock to touch. QMutex m_newTrackMutex; QAtomicInt m_newTrackAvailable; +#ifdef __STEM__ + NewTrackRequest m_pNewTrack; +#else TrackPointer m_pNewTrack; +#endif void discardAllPendingRequests(); @@ -147,7 +161,11 @@ class CachingReaderWorker : public EngineWorker { void unloadTrack(); /// Internal method to load a track. Emits trackLoaded when finished. +#ifdef __STEM__ + void loadTrack(const TrackPointer& pTrack, uint stemMask); +#else void loadTrack(const TrackPointer& pTrack); +#endif ReaderStatusUpdate processReadRequest( const CachingReaderChunkReadRequest& request); diff --git a/src/engine/channels/enginedeck.cpp b/src/engine/channels/enginedeck.cpp index 25e02a674c7..02ddf9a05b4 100644 --- a/src/engine/channels/enginedeck.cpp +++ b/src/engine/channels/enginedeck.cpp @@ -14,8 +14,6 @@ #ifdef __STEM__ namespace { -constexpr int kMaxSupportedStems = 4; - QString getGroupForStem(const QString& deckGroup, int stemIdx) { DEBUG_ASSERT(deckGroup.endsWith("]")); return QStringLiteral("%1Stem%2]") @@ -74,9 +72,9 @@ EngineDeck::EngineDeck( m_pStemCount = std::make_unique(ConfigKey(getGroup(), "stem_count")); m_pStemCount->setReadOnly(); - m_stemGain.reserve(kMaxSupportedStems); - m_stemMute.reserve(kMaxSupportedStems); - for (int stemIdx = 1; stemIdx <= kMaxSupportedStems; stemIdx++) { + m_stemGain.reserve(mixxx::kMaxSupportedStems); + m_stemMute.reserve(mixxx::kMaxSupportedStems); + for (int stemIdx = 1; stemIdx <= mixxx::kMaxSupportedStems; stemIdx++) { m_stemGain.emplace_back(std::make_unique( ConfigKey(getGroupForStem(getGroup(), stemIdx), QStringLiteral("volume")))); // The default value is ignored and override with the medium value by @@ -98,7 +96,7 @@ void EngineDeck::slotTrackLoaded(TrackPointer pNewTrack, } if (m_pConfig->getValue( ConfigKey("[Mixer Profile]", "stem_auto_reset"), true)) { - for (int stemIdx = 0; stemIdx < kMaxSupportedStems; stemIdx++) { + for (int stemIdx = 0; stemIdx < mixxx::kMaxSupportedStems; stemIdx++) { m_stemGain[stemIdx]->set(1.0); m_stemMute[stemIdx]->set(0.0); ; @@ -206,7 +204,7 @@ void EngineDeck::cloneStemState(const EngineDeck* deckToClone) { VERIFY_OR_DEBUG_ASSERT(deckToClone) { return; } - for (int stemIdx = 0; stemIdx < kMaxSupportedStems; stemIdx++) { + for (int stemIdx = 0; stemIdx < mixxx::kMaxSupportedStems; stemIdx++) { m_stemGain[stemIdx]->set(deckToClone->m_stemGain[stemIdx]->get()); m_stemMute[stemIdx]->set(deckToClone->m_stemMute[stemIdx]->get()); } diff --git a/src/engine/engine.h b/src/engine/engine.h index 4df4ab6c270..02bf959b4fa 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -8,6 +8,13 @@ static constexpr audio::ChannelCount kEngineChannelOutputCount = audio::ChannelCount::stereo(); static constexpr audio::ChannelCount kMaxEngineChannelInputCount = audio::ChannelCount::stem(); +// The following constant is always defined as it used for the waveform data +// struct, which must stay consistent, whether the STEM feature is enabled or +// not. +constexpr int kMaxSupportedStems = 4; +#ifdef __STEM__ +constexpr uint kNoStemSelected = 0; +#endif // Contains the information needed to process a buffer of audio class EngineParameters final { diff --git a/src/engine/enginebuffer.cpp b/src/engine/enginebuffer.cpp index 68ba74dc41f..a5b1e2aea19 100644 --- a/src/engine/enginebuffer.cpp +++ b/src/engine/enginebuffer.cpp @@ -41,6 +41,9 @@ #include "engine/controls/vinylcontrolcontrol.h" #endif +// EVE OSC +// #include "osc/oscfunctions.h" + namespace { const mixxx::Logger kLogger("EngineBuffer"); @@ -54,6 +57,24 @@ const QString kAppGroup = QStringLiteral("[App]"); } // anonymous namespace +// EveOSC +enum DefOscBodyType { + STRINGBODY = 1, + INTBODY = 2, + DOUBLEBODY = 3, + FLOATBODY = 4 +}; + +void OscFunctionsSendPtrType(UserSettingsPointer m_pConfig, + QString OscGroup, + QString OscKey, + enum DefOscBodyType OscBodyType, + QString OscMessageBodyQString, + int OscMessageBodyInt, + double OscMessageBodyDouble, + float OscMessageBodyFloat); +// EveOSC + EngineBuffer::EngineBuffer(const QString& group, UserSettingsPointer pConfig, EngineChannel* pChannel, @@ -173,6 +194,22 @@ EngineBuffer::EngineBuffer(const QString& group, m_pTrackSamples = new ControlObject(ConfigKey(m_group, "track_samples")); m_pTrackSampleRate = new ControlObject(ConfigKey(m_group, "track_samplerate")); + m_pTrackType = new ControlObject(ConfigKey(m_group, "track_type")); + m_pTrackTypeLength = new ControlObject(ConfigKey(m_group, "track_type_length")); + m_pTrackArtistLength = new ControlObject(ConfigKey(m_group, "track_artist_length")); + m_pTrackArtist_1 = new ControlObject(ConfigKey(m_group, "track_artist_1")); + m_pTrackArtist_2 = new ControlObject(ConfigKey(m_group, "track_artist_2")); + m_pTrackArtist_3 = new ControlObject(ConfigKey(m_group, "track_artist_3")); + m_pTrackArtist_4 = new ControlObject(ConfigKey(m_group, "track_artist_4")); + m_pTrackArtist_5 = new ControlObject(ConfigKey(m_group, "track_artist_5")); + + m_pTrackTitleLength = new ControlObject(ConfigKey(m_group, "track_title_length")); + m_pTrackTitle_1 = new ControlObject(ConfigKey(m_group, "track_title_1")); + m_pTrackTitle_2 = new ControlObject(ConfigKey(m_group, "track_title_2")); + m_pTrackTitle_3 = new ControlObject(ConfigKey(m_group, "track_title_3")); + m_pTrackTitle_4 = new ControlObject(ConfigKey(m_group, "track_title_4")); + m_pTrackTitle_5 = new ControlObject(ConfigKey(m_group, "track_title_5")); + m_pKeylock = new ControlPushButton(ConfigKey(m_group, "keylock"), true); m_pKeylock->setButtonMode(mixxx::control::ButtonMode::Toggle); @@ -312,6 +349,22 @@ EngineBuffer::~EngineBuffer() { delete m_pTrackSamples; delete m_pTrackSampleRate; + delete m_pTrackType; + delete m_pTrackTypeLength; + delete m_pTrackArtistLength; + delete m_pTrackArtist_1; + delete m_pTrackArtist_2; + delete m_pTrackArtist_3; + delete m_pTrackArtist_4; + delete m_pTrackArtist_5; + + delete m_pTrackTitleLength; + delete m_pTrackTitle_1; + delete m_pTrackTitle_2; + delete m_pTrackTitle_3; + delete m_pTrackTitle_4; + delete m_pTrackTitle_5; + delete m_pScaleLinear; delete m_pScaleST; #ifdef __RUBBERBAND__ @@ -563,6 +616,193 @@ void EngineBuffer::slotTrackLoaded(TrackPointer pTrack, m_pTrackSampleRate->set(trackSampleRate.toDouble()); m_pTrackLoaded->forceSet(1); + // Eve start + // Type + QString TrackInfoType = pTrack->getType(); + QString TrackInfoTypeTest = TrackInfoType; + int TrackInfoTypeTestLength = TrackInfoTypeTest.length(); + if (TrackInfoTypeTestLength > 5) { + TrackInfoType = TrackInfoTypeTest.mid(0, 5); + }; + m_pTrackTypeLength->set(TrackInfoTypeTestLength); + + int CharType[5]; + for (int i = 1; i <= 5; i++) { + CharType[i - 1] = 0; + } + + for (int i = 1; i <= TrackInfoType.length(); i++) { + if ((TrackInfoType.at(i - 1).toLatin1()) < 0) { + CharType[i - 1] = ((TrackInfoType.at(i - 1).toLatin1()) + 300); + } else { + CharType[i - 1] = (TrackInfoType.at(i - 1).toLatin1()); + }; + } + + double TrackTypePart = 0.0; + TrackTypePart = (1.0 * CharType[0] * 1000000000000) + + (1.0 * CharType[1] * 1000000000) + (1.0 * CharType[2] * 1000000) + + (1.0 * CharType[3] * 1000) + (1.0 * CharType[4] * 1); + m_pTrackType->set(TrackTypePart); + + // Title + QString TrackInfoTitle = pTrack->getTitle(); + QString TrackInfoTitleTest = TrackInfoTitle; + int TrackInfoTitleTestLength = TrackInfoTitleTest.length(); + if (TrackInfoTitleTestLength > 200) { + TrackInfoTitle = TrackInfoTitleTest.mid(0, 200); + }; + m_pTrackTitleLength->set(TrackInfoTitleTestLength); + + int CharTitle[200]; + for (int i = 1; i <= 200; i++) { + CharTitle[i - 1] = 0; + } + + // for (int i = 1; i <= TrackInfoTitle.length(); i++) { + for (int i = 1; i <= 25; i++) { + if ((TrackInfoTitle.at(i - 1).toLatin1()) < 0) { + CharTitle[i - 1] = ((TrackInfoTitle.at(i - 1).toLatin1()) + 300); + } else { + CharTitle[i - 1] = (TrackInfoTitle.at(i - 1).toLatin1()); + }; + } + + // Artist + QString TrackInfoArtist = pTrack->getArtist(); + QString TrackInfoArtistTest = TrackInfoArtist; + int TrackInfoArtistTestLength = TrackInfoArtistTest.length(); + if (TrackInfoArtistTestLength > 200) { + TrackInfoArtist = TrackInfoArtist.mid(0, 200); + }; + m_pTrackArtistLength->set(TrackInfoArtistTestLength); + + int CharArtist[200]; + for (int i = 1; i <= 200; i++) { + CharArtist[i - 1] = 0; + } + + // for (int i = 1; i <= TrackInfoArtist.length(); i++) { + for (int i = 1; i <= 25; i++) { + if ((TrackInfoArtist.at(i - 1).toLatin1()) < 0) { + CharArtist[i - 1] = ((TrackInfoArtist.at(i - 1).toLatin1()) + 300); + } else { + CharArtist[i - 1] = (TrackInfoArtist.at(i - 1).toLatin1()); + }; + } + + double TrackTitlePart_1 = 0.0; + double TrackTitlePart_2 = 0.0; + double TrackTitlePart_3 = 0.0; + double TrackTitlePart_4 = 0.0; + double TrackTitlePart_5 = 0.0; + + TrackTitlePart_1 = (1.0 * CharTitle[0] * 1000000000000) + + (1.0 * CharTitle[1] * 1000000000) + (1.0 * CharTitle[2] * 1000000) + + (1.0 * CharTitle[3] * 1000) + (1.0 * CharTitle[4] * 1); + TrackTitlePart_2 = (1.0 * CharTitle[5] * 1000000000000) + + (1.0 * CharTitle[6] * 1000000000) + (1.0 * CharTitle[7] * 1000000) + + (1.0 * CharTitle[8] * 1000) + (1.0 * CharTitle[9] * 1); + TrackTitlePart_3 = (1.0 * CharTitle[10] * 1000000000000) + + (1.0 * CharTitle[11] * 1000000000) + (1.0 * CharTitle[12] * 1000000) + + (1.0 * CharTitle[13] * 1000) + (1.0 * CharTitle[14] * 1); + TrackTitlePart_4 = (1.0 * CharTitle[15] * 1000000000000) + + (1.0 * CharTitle[16] * 1000000000) + (1.0 * CharTitle[17] * 1000000) + + (1.0 * CharTitle[18] * 1000) + (1.0 * CharTitle[19] * 1); + TrackTitlePart_5 = (1.0 * CharTitle[20] * 1000000000000) + + (1.0 * CharTitle[21] * 1000000000) + (1.0 * CharTitle[22] * 1000000) + + (1.0 * CharTitle[23] * 1000) + (1.0 * CharTitle[24] * 1); + + m_pTrackTitle_1->set(TrackTitlePart_1); + m_pTrackTitle_2->set(TrackTitlePart_2); + m_pTrackTitle_3->set(TrackTitlePart_3); + m_pTrackTitle_4->set(TrackTitlePart_4); + m_pTrackTitle_5->set(TrackTitlePart_5); + + double TrackArtistPart_1 = 0.0; + double TrackArtistPart_2 = 0.0; + double TrackArtistPart_3 = 0.0; + double TrackArtistPart_4 = 0.0; + double TrackArtistPart_5 = 0.0; + + TrackArtistPart_1 = (1.0 * CharArtist[0] * 1000000000000) + + (1.0 * CharArtist[1] * 1000000000) + + (1.0 * CharArtist[2] * 1000000) + (1.0 * CharArtist[3] * 1000) + + (1.0 * CharArtist[4] * 1); + TrackArtistPart_2 = (1.0 * CharArtist[5] * 1000000000000) + + (1.0 * CharArtist[6] * 1000000000) + + (1.0 * CharArtist[7] * 1000000) + (1.0 * CharArtist[8] * 1000) + + (1.0 * CharArtist[9] * 1); + TrackArtistPart_3 = (1.0 * CharArtist[10] * 1000000000000) + + (1.0 * CharArtist[11] * 1000000000) + + (1.0 * CharArtist[12] * 1000000) + (1.0 * CharArtist[13] * 1000) + + (1.0 * CharArtist[14] * 1); + TrackArtistPart_4 = (1.0 * CharArtist[15] * 1000000000000) + + (1.0 * CharArtist[16] * 1000000000) + + (1.0 * CharArtist[17] * 1000000) + (1.0 * CharArtist[18] * 1000) + + (1.0 * CharArtist[19] * 1); + TrackArtistPart_5 = (1.0 * CharArtist[20] * 1000000000000) + + (1.0 * CharArtist[21] * 1000000000) + + (1.0 * CharArtist[22] * 1000000) + (1.0 * CharArtist[23] * 1000) + + (1.0 * CharArtist[24] * 1); + + m_pTrackArtist_1->set(TrackArtistPart_1); + m_pTrackArtist_2->set(TrackArtistPart_2); + m_pTrackArtist_3->set(TrackArtistPart_3); + m_pTrackArtist_4->set(TrackArtistPart_4); + m_pTrackArtist_5->set(TrackArtistPart_5); + + // EveOSC begin + if (m_pConfig->getValue(ConfigKey("[OSC]", "OscEnabled"))) { + OscFunctionsSendPtrType(m_pConfig, + getGroup(), + "TrackArtist", + STRINGBODY, + pTrack->getArtist().toLatin1(), + 0, + 0, + 0); + OscFunctionsSendPtrType(m_pConfig, + getGroup(), + "TrackTitle", + STRINGBODY, + pTrack->getTitle().toLatin1(), + 0, + 0, + 0); + + float oscTrackInfoDuration = pTrack->getDuration(); + int oscTrackInfoDurationCalcMin = oscTrackInfoDuration / 60; + int oscTrackInfoDurationCalcSec = oscTrackInfoDuration - (oscTrackInfoDurationCalcMin * 60); + + QString oscTrackInfoDurationCalc; + + if (oscTrackInfoDurationCalcSec < 10) { + oscTrackInfoDurationCalc = + QString("%1:0%2") + .arg(oscTrackInfoDurationCalcMin) + .arg(oscTrackInfoDurationCalcSec); + } else { + oscTrackInfoDurationCalc = + QString("%1:%2") + .arg(oscTrackInfoDurationCalcMin) + .arg(oscTrackInfoDurationCalcSec); + }; + + QByteArray oscTrackInfoDurationBa = oscTrackInfoDurationCalc.toLocal8Bit(); + const char* oscBodyMessageDuration = oscTrackInfoDurationBa.data(); + + OscFunctionsSendPtrType(m_pConfig, + getGroup(), + "TDuration", + STRINGBODY, + oscBodyMessageDuration, + 0, + 0, + 0); + } + // EveOSC end + // Reset slip mode m_pSlipButton->set(0); m_bSlipEnabledProcessing = false; @@ -632,6 +872,50 @@ void EngineBuffer::ejectTrack() { m_pTrackSampleRate->set(0); m_pTrackLoaded->forceSet(0); + m_pTrackType->set(0); + m_pTrackTypeLength->set(0); + m_pTrackArtistLength->set(0); + m_pTrackArtist_1->set(0); + m_pTrackArtist_2->set(0); + m_pTrackArtist_3->set(0); + m_pTrackArtist_4->set(0); + m_pTrackArtist_5->set(0); + + m_pTrackTitleLength->set(0); + m_pTrackTitle_1->set(0); + m_pTrackTitle_2->set(0); + m_pTrackTitle_3->set(0); + m_pTrackTitle_4->set(0); + m_pTrackTitle_5->set(0); + + // EveOSC begin + if (m_pConfig->getValue(ConfigKey("[OSC]", "OscEnabled"))) { + OscFunctionsSendPtrType(m_pConfig, + getGroup(), + "TrackArtist", + STRINGBODY, + "no track loaded", + 0, + 0, + 0); + OscFunctionsSendPtrType(m_pConfig, + getGroup(), + "TrackTitle", + STRINGBODY, + "no track loaded", + 0, + 0, + 0); + OscFunctionsSendPtrType(m_pConfig, + getGroup(), + "Duration", + STRINGBODY, + "0:00", + 0, + 0, + 0); + } + m_playButton->set(0.0); m_playposSlider->set(0); m_pCueControl->resetIndicators(); @@ -1546,12 +1830,25 @@ void EngineBuffer::hintReader(const double dRate) { } // WARNING: This method runs in the GUI thread -void EngineBuffer::loadTrack(TrackPointer pTrack, bool play, EngineChannel* pChannelToCloneFrom) { +#ifdef __STEM__ +void EngineBuffer::loadTrack(TrackPointer pTrack, + uint stemMask, + bool play, + EngineChannel* pChannelToCloneFrom) { +#else +void EngineBuffer::loadTrack(TrackPointer pTrack, + bool play, + EngineChannel* pChannelToCloneFrom) { +#endif if (pTrack) { // Signal to the reader to load the track. The reader will respond with // trackLoading and then either with trackLoaded or trackLoadFailed signals. m_bPlayAfterLoading = play; +#ifdef __STEM__ + m_pReader->newTrack(pTrack, stemMask); +#else m_pReader->newTrack(pTrack); +#endif atomicStoreRelaxed(m_pChannelToCloneFrom, pChannelToCloneFrom); } else { // Loading a null track means "eject" diff --git a/src/engine/enginebuffer.h b/src/engine/enginebuffer.h index b0ef812686f..6495b6c958b 100644 --- a/src/engine/enginebuffer.h +++ b/src/engine/enginebuffer.h @@ -18,6 +18,8 @@ #include "track/track_decl.h" #include "util/types.h" +// #include "osc/oscfunctions.h" + #ifdef __RUBBERBAND__ #include "engine/bufferscalers/enginebufferscalerubberband.h" #endif @@ -215,7 +217,16 @@ class EngineBuffer : public EngineObject { // Request that the EngineBuffer load a track. Since the process is // asynchronous, EngineBuffer will emit a trackLoaded signal when the load // has completed. - void loadTrack(TrackPointer pTrack, bool play, EngineChannel* pChannelToCloneFrom); +#ifdef __STEM__ + void loadTrack(TrackPointer pTrack, + uint stemMask, + bool play, + EngineChannel* pChannelToCloneFrom); +#else + void loadTrack(TrackPointer pTrack, + bool play, + EngineChannel* pChannelToCloneFrom); +#endif void setChannelIndex(int channelIndex) { m_channelIndex = channelIndex; @@ -394,6 +405,26 @@ class EngineBuffer : public EngineObject { ControlObject* m_pTrackSamples; ControlObject* m_pTrackSampleRate; + // TrackType + ControlObject* m_pTrackType; + ControlObject* m_pTrackTypeLength; + + // TrackArtist + ControlObject* m_pTrackArtistLength; + ControlObject* m_pTrackArtist_1; + ControlObject* m_pTrackArtist_2; + ControlObject* m_pTrackArtist_3; + ControlObject* m_pTrackArtist_4; + ControlObject* m_pTrackArtist_5; + + // TrackTitle + ControlObject* m_pTrackTitleLength; + ControlObject* m_pTrackTitle_1; + ControlObject* m_pTrackTitle_2; + ControlObject* m_pTrackTitle_3; + ControlObject* m_pTrackTitle_4; + ControlObject* m_pTrackTitle_5; + ControlPushButton* m_playButton; ControlPushButton* m_playStartButton; ControlPushButton* m_stopStartButton; diff --git a/src/library/analysis/analysisfeature.cpp b/src/library/analysis/analysisfeature.cpp index a713f57ddfc..82b64ca3e49 100644 --- a/src/library/analysis/analysisfeature.cpp +++ b/src/library/analysis/analysisfeature.cpp @@ -81,7 +81,11 @@ void AnalysisFeature::bindLibraryWidget(WLibrary* libraryWidget, &DlgAnalysis::loadTrackToPlayer, this, [=, this](TrackPointer track, const QString& group) { - emit loadTrackToPlayer(track, group, false); + emit loadTrackToPlayer(track, group, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); }); connect(m_pAnalysisView, &DlgAnalysis::analyzeTracks, diff --git a/src/library/autodj/autodjprocessor.h b/src/library/autodj/autodjprocessor.h index a23f1ae9fe7..df8cb786acc 100644 --- a/src/library/autodj/autodjprocessor.h +++ b/src/library/autodj/autodjprocessor.h @@ -201,7 +201,11 @@ class AutoDJProcessor : public QObject { AutoDJError toggleAutoDJ(bool enable); signals: +#ifdef __STEM__ + void loadTrackToPlayer(TrackPointer pTrack, const QString& group, uint stemMask, bool play); +#else void loadTrackToPlayer(TrackPointer pTrack, const QString& group, bool play); +#endif void autoDJStateChanged(AutoDJProcessor::AutoDJState state); void autoDJError(AutoDJProcessor::AutoDJError error); void transitionTimeChanged(int time); @@ -230,7 +234,11 @@ class AutoDJProcessor : public QObject { protected: // The following virtual signal wrappers are used for testing virtual void emitLoadTrackToPlayer(TrackPointer pTrack, const QString& group, bool play) { - emit loadTrackToPlayer(pTrack, group, play); + emit loadTrackToPlayer(pTrack, group, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + play); } virtual void emitAutoDJStateChanged(AutoDJProcessor::AutoDJState state) { emit autoDJStateChanged(state); diff --git a/src/library/autodj/dlgautodj.h b/src/library/autodj/dlgautodj.h index 9ac38a1e722..f4138ec86f5 100644 --- a/src/library/autodj/dlgautodj.h +++ b/src/library/autodj/dlgautodj.h @@ -49,7 +49,11 @@ class DlgAutoDJ : public QWidget, public Ui::DlgAutoDJ, public LibraryView { signals: void addRandomTrackButton(bool buttonChecked); void loadTrack(TrackPointer tio); +#ifdef __STEM__ + void loadTrackToPlayer(TrackPointer tio, const QString& group, uint stemMask, bool); +#else void loadTrackToPlayer(TrackPointer tio, const QString& group, bool); +#endif void trackSelected(TrackPointer pTrack); private: diff --git a/src/library/library.cpp b/src/library/library.cpp index 21f82be564e..99fb2d429f3 100644 --- a/src/library/library.cpp +++ b/src/library/library.cpp @@ -552,14 +552,25 @@ void Library::slotLoadLocationToPlayer(const QString& location, const QString& g auto trackRef = TrackRef::fromFilePath(location); TrackPointer pTrack = m_pTrackCollectionManager->getOrAddTrack(trackRef); if (pTrack) { +#ifdef __STEM__ + emit loadTrackToPlayer(pTrack, group, mixxx::kNoStemSelected, play); +#else emit loadTrackToPlayer(pTrack, group, play); +#endif } } +#ifdef __STEM__ +void Library::slotLoadTrackToPlayer( + TrackPointer pTrack, const QString& group, uint stemMask, bool play) { + emit loadTrackToPlayer(pTrack, group, stemMask, play); +} +#else void Library::slotLoadTrackToPlayer( TrackPointer pTrack, const QString& group, bool play) { emit loadTrackToPlayer(pTrack, group, play); } +#endif void Library::slotRefreshLibraryModels() { m_pMixxxLibraryFeature->refreshLibraryModels(); diff --git a/src/library/library.h b/src/library/library.h index e0190084eeb..75b2e4e014a 100644 --- a/src/library/library.h +++ b/src/library/library.h @@ -114,7 +114,11 @@ class Library: public QObject { void slotShowTrackModel(QAbstractItemModel* model); void slotSwitchToView(const QString& view); void slotLoadTrack(TrackPointer pTrack); +#ifdef __STEM__ + void slotLoadTrackToPlayer(TrackPointer pTrack, const QString& group, uint stemMask, bool play); +#else void slotLoadTrackToPlayer(TrackPointer pTrack, const QString& group, bool play); +#endif void slotLoadLocationToPlayer(const QString& location, const QString& group, bool play); void slotRefreshLibraryModels(); void slotCreatePlaylist(); @@ -127,7 +131,16 @@ class Library: public QObject { void showTrackModel(QAbstractItemModel* model, bool restoreState = true); void switchToView(const QString& view); void loadTrack(TrackPointer pTrack); - void loadTrackToPlayer(TrackPointer pTrack, const QString& group, bool play = false); +#ifdef __STEM__ + void loadTrackToPlayer(TrackPointer pTrack, + const QString& group, + uint stemMask, + bool play = false); +#else + void loadTrackToPlayer(TrackPointer pTrack, + const QString& group, + bool play = false); +#endif void restoreSearch(const QString&); void search(const QString& text); void disableSearch(); diff --git a/src/library/librarycontrol.cpp b/src/library/librarycontrol.cpp index 0b69650eb51..74874e3e040 100644 --- a/src/library/librarycontrol.cpp +++ b/src/library/librarycontrol.cpp @@ -38,6 +38,19 @@ LoadToGroupController::LoadToGroupController(LibraryControl* pParent, const QStr this, &LoadToGroupController::slotLoadToGroupAndPlay); +#ifdef __STEM__ + m_loadSelectedTrackStems = + std::make_unique(ConfigKey(group, "load_selected_track_stems")); + connect(m_loadSelectedTrackStems.get(), + &ControlObject::valueChanged, + this, + [this](double value) { + if (value >= 0 && value <= 2 << mixxx::kMaxSupportedStems) { + emit loadToGroup(m_group, static_cast(value), false); + } + }); +#endif + connect(this, &LoadToGroupController::loadToGroup, pParent, @@ -48,13 +61,24 @@ LoadToGroupController::~LoadToGroupController() = default; void LoadToGroupController::slotLoadToGroup(double v) { if (v > 0) { - emit loadToGroup(m_group, false); + emit loadToGroup(m_group, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); } } void LoadToGroupController::slotLoadToGroupAndPlay(double v) { if (v > 0) { - emit loadToGroup(m_group, true); +#ifdef __STEM__ + emit loadToGroup(m_group, + mixxx::kNoStemSelected, + true); +#else + emit loadToGroup(m_group, + true); +#endif } } @@ -600,14 +624,22 @@ void LibraryControl::slotUpdateTrackMenuControl(bool visible) { m_pShowTrackMenu->setAndConfirm(visible ? 1.0 : 0.0); } +#ifdef __STEM__ +void LibraryControl::slotLoadSelectedTrackToGroup(const QString& group, uint stemMask, bool play) { +#else void LibraryControl::slotLoadSelectedTrackToGroup(const QString& group, bool play) { +#endif if (!m_pLibraryWidget) { return; } WTrackTableView* pTrackTableView = m_pLibraryWidget->getCurrentTrackTableView(); if (pTrackTableView) { +#ifdef __STEM__ + pTrackTableView->loadSelectedTrackToGroup(group, stemMask, play); +#else pTrackTableView->loadSelectedTrackToGroup(group, play); +#endif } } diff --git a/src/library/librarycontrol.h b/src/library/librarycontrol.h index 65e21013ea4..dbc3ea66c8b 100644 --- a/src/library/librarycontrol.h +++ b/src/library/librarycontrol.h @@ -23,7 +23,14 @@ class LoadToGroupController : public QObject { virtual ~LoadToGroupController(); signals: - void loadToGroup(const QString& group, bool); +#ifdef __STEM__ + void loadToGroup(const QString& group, + uint stemMask, + bool); +#else + void loadToGroup(const QString& group, + bool); +#endif public slots: void slotLoadToGroup(double v); @@ -33,6 +40,10 @@ class LoadToGroupController : public QObject { const QString m_group; std::unique_ptr m_pLoadControl; std::unique_ptr m_pLoadAndPlayControl; + +#ifdef __STEM__ + std::unique_ptr m_loadSelectedTrackStems; +#endif }; class LibraryControl : public QObject { @@ -54,7 +65,11 @@ class LibraryControl : public QObject { public slots: // Deprecated navigation slots +#ifdef __STEM__ + void slotLoadSelectedTrackToGroup(const QString& group, uint stemMask, bool play); +#else void slotLoadSelectedTrackToGroup(const QString& group, bool play); +#endif void slotUpdateTrackMenuControl(bool visible); private slots: diff --git a/src/library/libraryfeature.h b/src/library/libraryfeature.h index 61972192dcd..9840d4424b2 100644 --- a/src/library/libraryfeature.h +++ b/src/library/libraryfeature.h @@ -135,7 +135,16 @@ class LibraryFeature : public QObject { void showTrackModel(QAbstractItemModel* model, bool restoreState = true); void switchToView(const QString& view); void loadTrack(TrackPointer pTrack); - void loadTrackToPlayer(TrackPointer pTrack, const QString& group, bool play = false); +#ifdef __STEM__ + void loadTrackToPlayer(TrackPointer pTrack, + const QString& group, + uint stemMask, + bool play = false); +#else + void loadTrackToPlayer(TrackPointer pTrack, + const QString& group, + bool play = false); +#endif /// saves the scroll, selection and current state of the library model void saveModelState(); /// restores the scroll, selection and current state of the library model diff --git a/src/library/recording/dlgrecording.h b/src/library/recording/dlgrecording.h index 8d715056319..3ea574632cb 100644 --- a/src/library/recording/dlgrecording.h +++ b/src/library/recording/dlgrecording.h @@ -37,7 +37,11 @@ class DlgRecording : public QWidget, public Ui::DlgRecording, public virtual Lib signals: void loadTrack(TrackPointer tio); - void loadTrackToPlayer(TrackPointer tio, const QString& group, bool play); +#ifdef __STEM__ + void loadTrackToPlayer(TrackPointer tio, const QString& group, uint stemMask, bool); +#else + void loadTrackToPlayer(TrackPointer tio, const QString& group, bool); +#endif void restoreSearch(const QString& search); void restoreModelState(); diff --git a/src/library/tabledelegates/previewbuttondelegate.cpp b/src/library/tabledelegates/previewbuttondelegate.cpp index 87c897ec95f..d4b695a3636 100644 --- a/src/library/tabledelegates/previewbuttondelegate.cpp +++ b/src/library/tabledelegates/previewbuttondelegate.cpp @@ -220,7 +220,11 @@ void PreviewButtonDelegate::buttonClicked() { TrackPointer pTrack = pTrackModel->getTrack(m_currentEditedCellIndex); if (pTrack && pTrack != pOldTrack) { // Load to preview deck and start playing - emit loadTrackToPlayer(pTrack, kPreviewDeckGroup, true); + emit loadTrackToPlayer(pTrack, kPreviewDeckGroup, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + true); startedPlaying = true; } else if (pTrack == pOldTrack && !isPreviewDeckPlaying()) { // Since the Preview deck might be hidden, starting at the main cue diff --git a/src/library/tabledelegates/previewbuttondelegate.h b/src/library/tabledelegates/previewbuttondelegate.h index a42b206bc12..a7a9be95dbe 100644 --- a/src/library/tabledelegates/previewbuttondelegate.h +++ b/src/library/tabledelegates/previewbuttondelegate.h @@ -54,7 +54,11 @@ class PreviewButtonDelegate : public TableItemDelegate { const QModelIndex& index) const override; signals: - void loadTrackToPlayer(const TrackPointer& pTrack, const QString& group, bool play); +#ifdef __STEM__ + void loadTrackToPlayer(const TrackPointer& pTrack, const QString& group, uint stemMask, bool); +#else + void loadTrackToPlayer(const TrackPointer& pTrack, const QString& group, bool); +#endif void buttonSetChecked(bool); public slots: diff --git a/src/mixer/basetrackplayer.cpp b/src/mixer/basetrackplayer.cpp index 1b8162c7fe9..8a44f5a0a50 100644 --- a/src/mixer/basetrackplayer.cpp +++ b/src/mixer/basetrackplayer.cpp @@ -20,14 +20,15 @@ #include "vinylcontrol/defs_vinylcontrol.h" #include "waveform/renderers/waveformwidgetrenderer.h" +// EVE OSC +// #include "osc/oscfunctions.h" +// EVE OSC + namespace { constexpr double kNoTrackColor = -1; constexpr double kShiftCuesOffsetMillis = 10; constexpr double kShiftCuesOffsetSmallMillis = 1; -#ifdef __STEM__ -constexpr int kMaxSupportedStems = 4; -#endif const QString kEffectGroupFormat = QStringLiteral("[EqualizerRack1_%1_Effect1]"); inline double trackColorToDouble(mixxx::RgbColor::optional_t color) { @@ -35,6 +36,24 @@ inline double trackColorToDouble(mixxx::RgbColor::optional_t color) { } } // namespace +// EveOSC +enum DefOscBodyType { + STRINGBODY = 1, + INTBODY = 2, + DOUBLEBODY = 3, + FLOATBODY = 4 +}; + +void OscFunctionsSendPtrType(UserSettingsPointer m_pConfig, + QString OscGroup, + QString OscKey, + enum DefOscBodyType OscBodyType, + QString OscMessageBodyQString, + int OscMessageBodyInt, + double OscMessageBodyDouble, + float OscMessageBodyFloat); +// EveOSC + BaseTrackPlayer::BaseTrackPlayer(PlayerManager* pParent, const QString& group) : BasePlayer(pParent, group) { } @@ -101,7 +120,7 @@ BaseTrackPlayerImpl::BaseTrackPlayerImpl( // Duration of the current song, we create this one because nothing else does. m_pDuration = std::make_unique( - ConfigKey(getGroup(), "duration")); + ConfigKey(getGroup(), "duration")); // Track color of the current track m_pTrackColor = std::make_unique( @@ -289,9 +308,9 @@ BaseTrackPlayerImpl::BaseTrackPlayerImpl( } #ifdef __STEM__ - m_pStemColors.reserve(kMaxSupportedStems); + m_pStemColors.reserve(mixxx::kMaxSupportedStems); QString group = getGroup(); - for (int stemIdx = 1; stemIdx <= kMaxSupportedStems; stemIdx++) { + for (int stemIdx = 1; stemIdx <= mixxx::kMaxSupportedStems; stemIdx++) { QString stemGroup = QStringLiteral("%1Stem%2]") .arg(group.left(group.size() - 1), QString::number(stemIdx)); @@ -420,7 +439,11 @@ void BaseTrackPlayerImpl::slotEjectTrack(double v) { if (elapsed < mixxx::Duration::fromMillis(kUnreplaceDelay)) { TrackPointer lastEjected = m_pPlayerManager->getSecondLastEjectedTrack(); if (lastEjected) { - slotLoadTrack(lastEjected, false); + slotLoadTrack(lastEjected, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); } return; } @@ -429,11 +452,14 @@ void BaseTrackPlayerImpl::slotEjectTrack(double v) { if (!m_pLoadedTrack) { TrackPointer lastEjected = m_pPlayerManager->getLastEjectedTrack(); if (lastEjected) { - slotLoadTrack(lastEjected, false); + slotLoadTrack(lastEjected, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); } return; } - m_pChannel->getEngineBuffer()->ejectTrack(); } @@ -543,10 +569,17 @@ void BaseTrackPlayerImpl::disconnectLoadedTrack() { disconnect(m_pLoadedTrack.get(), nullptr, m_pKey.get(), nullptr); } -void BaseTrackPlayerImpl::slotLoadTrack(TrackPointer pNewTrack, bool bPlay) { - //qDebug() << "BaseTrackPlayerImpl::slotLoadTrack" << getGroup() << pNewTrack.get(); - // Before loading the track, ensure we have access. This uses lazy - // evaluation to make sure track isn't NULL before we dereference it. +#ifdef __STEM__ +void BaseTrackPlayerImpl::slotLoadTrack(TrackPointer pNewTrack, + uint stemMask, + bool bPlay) { +#else +void BaseTrackPlayerImpl::slotLoadTrack(TrackPointer pNewTrack, + bool bPlay) { +#endif + // qDebug() << "BaseTrackPlayerImpl::slotLoadTrack" << getGroup() << pNewTrack.get(); + // Before loading the track, ensure we have access. This uses lazy + // evaluation to make sure track isn't NULL before we dereference it. if (pNewTrack) { auto fileInfo = pNewTrack->getFileInfo(); if (!Sandbox::askForAccess(&fileInfo)) { @@ -566,7 +599,17 @@ void BaseTrackPlayerImpl::slotLoadTrack(TrackPointer pNewTrack, bool bPlay) { // Request a new track from EngineBuffer EngineBuffer* pEngineBuffer = m_pChannel->getEngineBuffer(); +#ifdef __STEM__ + pEngineBuffer->loadTrack(pNewTrack, + stemMask, + bPlay, + m_pChannelToCloneFrom); + + // Select a specific stem if requested + emit selectedStems(stemMask); +#else pEngineBuffer->loadTrack(pNewTrack, bPlay, m_pChannelToCloneFrom); +#endif } void BaseTrackPlayerImpl::slotLoadFailed(TrackPointer pTrack, const QString& reason) { @@ -600,8 +643,8 @@ void BaseTrackPlayerImpl::slotLoadFailed(TrackPointer pTrack, const QString& rea } void BaseTrackPlayerImpl::slotTrackLoaded(TrackPointer pNewTrack, - TrackPointer pOldTrack) { - //qDebug() << "BaseTrackPlayerImpl::slotTrackLoaded" << pNewTrack.get() << pOldTrack.get(); + TrackPointer pOldTrack) { + // qDebug() << "BaseTrackPlayerImpl::slotTrackLoaded" << pNewTrack.get() << pOldTrack.get(); if (!pNewTrack && pOldTrack && pOldTrack == m_pLoadedTrack) { @@ -635,8 +678,8 @@ void BaseTrackPlayerImpl::slotTrackLoaded(TrackPointer pNewTrack, m_pKey->set(m_pLoadedTrack->getKey()); slotSetTrackColor(m_pLoadedTrack->getColor()); - if(m_pConfig->getValue( - ConfigKey("[Mixer Profile]", "EqAutoReset"), false)) { + if (m_pConfig->getValue( + ConfigKey("[Mixer Profile]", "EqAutoReset"), false)) { if (m_pLowFilter) { m_pLowFilter->set(1.0); } @@ -657,7 +700,7 @@ void BaseTrackPlayerImpl::slotTrackLoaded(TrackPointer pNewTrack, } } if (m_pConfig->getValue( - ConfigKey("[Mixer Profile]", "GainAutoReset"), false)) { + ConfigKey("[Mixer Profile]", "GainAutoReset"), false)) { m_pPreGain->set(1.0); } @@ -698,7 +741,7 @@ void BaseTrackPlayerImpl::slotTrackLoaded(TrackPointer pNewTrack, #ifdef __STEM__ if (m_pStemColors.size()) { const auto& stemInfo = m_pLoadedTrack->getStemInfo(); - DEBUG_ASSERT(stemInfo.size() <= kMaxSupportedStems); + DEBUG_ASSERT(stemInfo.size() <= mixxx::kMaxSupportedStems); int stemIdx = 0; for (const auto& stemColorCo : m_pStemColors) { auto color = kNoTrackColor; @@ -726,6 +769,51 @@ void BaseTrackPlayerImpl::slotTrackLoaded(TrackPointer pNewTrack, // Update the PlayerInfo class that is used in EngineBroadcast to replace // the metadata of a stream PlayerInfo::instance().setTrackInfo(getGroup(), m_pLoadedTrack); + QString trackInfoArtist = " "; + QString trackInfoTitle = " "; + QString DeckStatusTxtLine2 = " "; + QString DeckStatusTxtLine3 = " "; + QString DeckStatusTxtLine4 = " "; + QTime tempStatusTime = QTime::currentTime(); + QString DeckStatusTime = tempStatusTime.toString("hh:mm:ss"); + + if (pNewTrack) { + // QString trackInfo = pNewTrack->getInfo(); + trackInfoArtist = pNewTrack->getArtist(); + trackInfoTitle = pNewTrack->getTitle(); + trackInfoArtist.replace("\"", "''"); + trackInfoTitle.replace("\"", "''"); + DeckStatusTxtLine2 = "Artist : \"" + trackInfoArtist + "\","; + DeckStatusTxtLine3 = "Title : \"" + trackInfoTitle + "\","; + DeckStatusTxtLine4 = "Time : \"" + DeckStatusTime + "\","; + + } else { + DeckStatusTxtLine2 = "Artist : \" \","; + DeckStatusTxtLine3 = "Title : \" \","; + DeckStatusTxtLine4 = "Time : \"" + DeckStatusTime + "\","; + } + QString trackInfoDeck = getGroup(); + trackInfoDeck.replace("[Channel", ""); + trackInfoDeck.replace("]", ""); + QString DeckStatusFilePath = m_pConfig->getSettingsPath(); + DeckStatusFilePath.replace("Roaming", "Local"); + DeckStatusFilePath.replace("\\", "/"); + QString DeckStatusFileLocation = + DeckStatusFilePath + "/controllers/Status" + getGroup() + ".js"; + // Different file for each Deck / Sampler + QString DeckStatusTxtLine1 = "var TrackDeck" + trackInfoDeck + " = { "; + QString DeckStatusTxtLine5 = "};"; + QFile DeckStatusFile(DeckStatusFileLocation); + DeckStatusFile.remove(); + DeckStatusFile.open(QIODevice::ReadWrite | QIODevice::Append); + // DeckStatusFile.open(QIODevice::ReadWrite | QIODevice::Append); + QTextStream DeckStatusTxt(&DeckStatusFile); + DeckStatusTxt << DeckStatusTxtLine1 << "\n"; + DeckStatusTxt << DeckStatusTxtLine2 << "\n"; + DeckStatusTxt << DeckStatusTxtLine3 << "\n"; + DeckStatusTxt << DeckStatusTxtLine4 << "\n"; + DeckStatusTxt << DeckStatusTxtLine5 << "\n"; + DeckStatusFile.close(); } TrackPointer BaseTrackPlayerImpl::getLoadedTrack() const { @@ -777,7 +865,11 @@ void BaseTrackPlayerImpl::slotCloneChannel(EngineChannel* pChannel) { m_pChannelToCloneFrom = pChannel; bool play = ControlObject::toBool(ConfigKey(m_pChannelToCloneFrom->getGroup(), "play")); - slotLoadTrack(pTrack, play); + slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + play); } void BaseTrackPlayerImpl::slotLoadTrackFromDeck(double d) { @@ -801,7 +893,11 @@ void BaseTrackPlayerImpl::loadTrackFromGroup(const QString& group) { return; } - slotLoadTrack(pTrack, false); + slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); } bool BaseTrackPlayerImpl::isTrackMenuControlAvailable() { @@ -923,6 +1019,12 @@ void BaseTrackPlayerImpl::slotTrackRatingChangeRequestRelative(int change) { void BaseTrackPlayerImpl::slotPlayToggled(double value) { if (value == 0 && m_replaygainPending) { setReplayGain(m_pLoadedTrack->getReplayGain().getRatio()); + + // EveOSC begin + // if (m_pConfig->getValue(ConfigKey("[OSC]", "OscEnabled"))) { + // OscFunctionsSendFloat(getGroup(), "play", value); + // } + // EveOSC end } } @@ -961,8 +1063,8 @@ void BaseTrackPlayerImpl::slotVinylControlEnabled(double v) { } void BaseTrackPlayerImpl::slotWaveformZoomValueChangeRequest(double v) { - if (v <= WaveformWidgetRenderer::s_waveformMaxZoom - && v >= WaveformWidgetRenderer::s_waveformMinZoom) { + if (v <= WaveformWidgetRenderer::s_waveformMaxZoom && + v >= WaveformWidgetRenderer::s_waveformMinZoom) { m_pWaveformZoom->setAndConfirm(v); } } diff --git a/src/mixer/basetrackplayer.h b/src/mixer/basetrackplayer.h index 537fc092079..28bcd88b17e 100644 --- a/src/mixer/basetrackplayer.h +++ b/src/mixer/basetrackplayer.h @@ -46,7 +46,14 @@ class BaseTrackPlayer : public BasePlayer { }; public slots: - virtual void slotLoadTrack(TrackPointer pTrack, bool bPlay = false) = 0; +#ifdef __STEM__ + virtual void slotLoadTrack(TrackPointer pTrack, + uint stemMask, + bool bPlay = false) = 0; +#else + virtual void slotLoadTrack(TrackPointer pTrack, + bool bPlay = false) = 0; +#endif virtual void slotCloneFromGroup(const QString& group) = 0; virtual void slotCloneDeck() = 0; virtual void slotEjectTrack(double) = 0; @@ -57,6 +64,9 @@ class BaseTrackPlayer : public BasePlayer { void newTrackLoaded(TrackPointer pLoadedTrack); void trackUnloaded(TrackPointer pUnloadedTrack); void loadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack); +#ifdef __STEM__ + void selectedStems(uint stemMask); +#endif void playerEmpty(); void noVinylControlInputConfigured(); void trackRatingChanged(int rating); @@ -94,7 +104,14 @@ class BaseTrackPlayerImpl : public BaseTrackPlayer { TrackPointer loadFakeTrack(bool bPlay, double filebpm); public slots: - void slotLoadTrack(TrackPointer track, bool bPlay) final; +#ifdef __STEM__ + void slotLoadTrack(TrackPointer track, + uint stemMask, + bool bPlay) final; +#else + void slotLoadTrack(TrackPointer track, + bool bPlay) final; +#endif void slotEjectTrack(double) final; void slotCloneFromGroup(const QString& group) final; void slotCloneDeck() final; diff --git a/src/mixer/playermanager.cpp b/src/mixer/playermanager.cpp index d54d474f73f..2a38360aacf 100644 --- a/src/mixer/playermanager.cpp +++ b/src/mixer/playermanager.cpp @@ -670,7 +670,13 @@ void PlayerManager::slotCloneDeck(const QString& source_group, const QString& ta pPlayer->slotCloneFromGroup(source_group); } -void PlayerManager::slotLoadTrackToPlayer(TrackPointer pTrack, const QString& group, bool play) { +#ifdef __STEM__ +void PlayerManager::slotLoadTrackToPlayer( + TrackPointer pTrack, const QString& group, uint stemMask, bool play) { +#else +void PlayerManager::slotLoadTrackToPlayer( + TrackPointer pTrack, const QString& group, bool play) { +#endif // Do not lock mutex in this method unless it is changed to access // PlayerManager state. BaseTrackPlayer* pPlayer = getPlayer(group); @@ -721,7 +727,11 @@ void PlayerManager::slotLoadTrackToPlayer(TrackPointer pTrack, const QString& gr if (clone) { pPlayer->slotCloneDeck(); } else { +#ifdef __STEM__ + pPlayer->slotLoadTrack(pTrack, stemMask, play); +#else pPlayer->slotLoadTrack(pTrack, play); +#endif } m_lastLoadedPlayer = group; @@ -773,7 +783,11 @@ void PlayerManager::slotLoadTrackIntoNextAvailableDeck(TrackPointer pTrack) { return; } - pDeck->slotLoadTrack(pTrack, false); + pDeck->slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); } void PlayerManager::slotLoadLocationIntoNextAvailableDeck(const QString& location, bool play) { diff --git a/src/mixer/playermanager.h b/src/mixer/playermanager.h index 96300baba16..b5298ae2493 100644 --- a/src/mixer/playermanager.h +++ b/src/mixer/playermanager.h @@ -194,7 +194,11 @@ class PlayerManager : public QObject, public PlayerManagerInterface { public slots: // Slots for loading tracks into a Player, which is either a Sampler or a Deck +#ifdef __STEM__ + void slotLoadTrackToPlayer(TrackPointer pTrack, const QString& group, uint stemMask, bool play); +#else void slotLoadTrackToPlayer(TrackPointer pTrack, const QString& group, bool play); +#endif void slotLoadLocationToPlayer(const QString& location, const QString& group, bool play); void slotLoadLocationToPlayerMaybePlay(const QString& location, const QString& group); diff --git a/src/mixer/samplerbank.cpp b/src/mixer/samplerbank.cpp index 4e03f6cdea7..dca2e1b4112 100644 --- a/src/mixer/samplerbank.cpp +++ b/src/mixer/samplerbank.cpp @@ -212,7 +212,12 @@ bool SamplerBank::loadSamplerBankFromPath(const QString& samplerBankPath) { } if (location.isEmpty()) { - m_pPlayerManager->slotLoadTrackToPlayer(TrackPointer(), group, false); + m_pPlayerManager->slotLoadTrackToPlayer( + TrackPointer(), group, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); } else { m_pPlayerManager->slotLoadLocationToPlayer(location, group, false); } diff --git a/src/mixxxmainwindow.cpp b/src/mixxxmainwindow.cpp index c5b44237bf9..3fc44a892bc 100644 --- a/src/mixxxmainwindow.cpp +++ b/src/mixxxmainwindow.cpp @@ -62,6 +62,11 @@ #include "vinylcontrol/vinylcontrolmanager.h" #endif +// EveOSC +#include "osc/oscfunctions.h" +#include "osc/oscreceiver.cpp" +// EveOSC + namespace { #ifdef __LINUX__ // Detect if the desktop supports a global menu to decide whether we need to rebuild @@ -132,6 +137,9 @@ MixxxMainWindow::MixxxMainWindow(std::shared_ptr pCoreServi m_pGuiTick = new GuiTick(); m_pVisualsManager = new VisualsManager(); + // EveOSC + oscEnable(); + // EveOSC } #ifdef MIXXX_USE_QOPENGL @@ -325,7 +333,7 @@ void MixxxMainWindow::initialize() { reportCriticalErrorAndQuit( "default skin cannot be loaded - see mixxx trace for more information"); m_pCentralWidget = oldWidget; - //TODO (XXX) add dialog to warn user and launch skin choice page + // TODO (XXX) add dialog to warn user and launch skin choice page } else { m_pMenuBar->setStyleSheet(m_pCentralWidget->styleSheet()); } @@ -600,7 +608,7 @@ void MixxxMainWindow::alwaysHideMenuBarDlg() { #endif QDialog::DialogCode MixxxMainWindow::soundDeviceErrorDlg( - const QString &title, const QString &text, bool* retryClicked) { + const QString& title, const QString& text, bool* retryClicked) { QMessageBox msgBox; msgBox.setIcon(QMessageBox::Warning); msgBox.setWindowTitle(title); @@ -615,8 +623,7 @@ QDialog::DialogCode MixxxMainWindow::soundDeviceErrorDlg( QPushButton* exitButton = msgBox.addButton(tr("Exit"), QMessageBox::ActionRole); - while (true) - { + while (true) { msgBox.exec(); if (msgBox.clickedButton() == retryButton) { @@ -705,19 +712,18 @@ QDialog::DialogCode MixxxMainWindow::noOutputDlg(bool* continueClicked) { msgBox.setWindowTitle(tr("No Output Devices")); msgBox.setText( "" + tr("Mixxx was configured without any output sound devices. " - "Audio processing will be disabled without a configured output device.") + + "Audio processing will be disabled without a configured output device.") + "
    " - "
  • " + - tr("Continue without any outputs.") + - "
  • " - "
  • " + - tr("Reconfigure Mixxx's sound device settings.") + - "
  • " - "
  • " + - tr("Exit Mixxx.") + - "
  • " - "
" - ); + "
  • " + + tr("Continue without any outputs.") + + "
  • " + "
  • " + + tr("Reconfigure Mixxx's sound device settings.") + + "
  • " + "
  • " + + tr("Exit Mixxx.") + + "
  • " + ""); QPushButton* continueButton = msgBox.addButton(tr("Continue"), QMessageBox::ActionRole); @@ -726,8 +732,7 @@ QDialog::DialogCode MixxxMainWindow::noOutputDlg(bool* continueClicked) { QPushButton* exitButton = msgBox.addButton(tr("Exit"), QMessageBox::ActionRole); - while (true) - { + while (true) { msgBox.exec(); if (msgBox.clickedButton() == continueButton) { @@ -987,7 +992,7 @@ void MixxxMainWindow::slotFileLoadSongPlayer(int deck) { QString loadTrackText = tr("Load track to Deck %1").arg(QString::number(deck)); QString deckWarningMessage = tr("Deck %1 is currently playing a track.") - .arg(QString::number(deck)); + .arg(QString::number(deck)); QString areYouSure = tr("Are you sure you want to load a new track?"); if (ControlObject::get(ConfigKey(group, "play")) > 0.0) { @@ -1196,8 +1201,8 @@ void MixxxMainWindow::rebootMixxxView() { if (!loadConfiguredSkin()) { QMessageBox::critical(this, - tr("Error in skin file"), - tr("The selected skin cannot be loaded.")); + tr("Error in skin file"), + tr("The selected skin cannot be loaded.")); // m_pWidgetParent is NULL, we can't continue. return; } @@ -1340,7 +1345,7 @@ bool MixxxMainWindow::eventFilter(QObject* obj, QEvent* event) { return QMainWindow::eventFilter(obj, event); } -void MixxxMainWindow::closeEvent(QCloseEvent *event) { +void MixxxMainWindow::closeEvent(QCloseEvent* event) { // WARNING: We can receive a CloseEvent while only partially // initialized. This is because we call QApplication::processEvents to // render LaunchImage progress in the constructor. @@ -1369,7 +1374,7 @@ void MixxxMainWindow::checkDirectRendering() { UserSettingsPointer pConfig = m_pCoreServices->getSettings(); if (!factory->isOpenGlAvailable() && !factory->isOpenGlesAvailable() && - pConfig->getValueString(ConfigKey("[Direct Rendering]", "Warned")) != QString("yes")) { + pConfig->getValueString(ConfigKey("[Direct Rendering]", "Warned")) != QString("yes")) { QMessageBox::warning(nullptr, tr("OpenGL Direct Rendering"), tr("Direct rendering is not enabled on your machine.

    " @@ -1407,31 +1412,33 @@ bool MixxxMainWindow::confirmExit() { } if (playing) { QMessageBox::StandardButton btn = QMessageBox::question(this, - tr("Confirm Exit"), - tr("A deck is currently playing. Exit Mixxx?"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + tr("Confirm Exit"), + tr("A deck is currently playing. Exit Mixxx?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); if (btn == QMessageBox::No) { return false; } } else if (playingSampler) { QMessageBox::StandardButton btn = QMessageBox::question(this, - tr("Confirm Exit"), - tr("A sampler is currently playing. Exit Mixxx?"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + tr("Confirm Exit"), + tr("A sampler is currently playing. Exit Mixxx?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); if (btn == QMessageBox::No) { return false; } } if (m_pPrefDlg && m_pPrefDlg->isVisible()) { - QMessageBox::StandardButton btn = QMessageBox::question( - this, tr("Confirm Exit"), - tr("The preferences window is still open.") + "
    " + - tr("Discard any changes and exit Mixxx?"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + QMessageBox::StandardButton btn = QMessageBox::question(this, + tr("Confirm Exit"), + tr("The preferences window is still open.") + "
    " + + tr("Discard any changes and exit Mixxx?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); if (btn == QMessageBox::No) { return false; - } - else { + } else { m_pPrefDlg->close(); } } @@ -1445,3 +1452,28 @@ void MixxxMainWindow::initializationProgressUpdate(int progress, const QString& } qApp->processEvents(); } + +void MixxxMainWindow::oscEnable() { + UserSettingsPointer pConfig; + QString MixxxOSCStatusFilePath = m_pCoreServices->getSettings()->getSettingsPath(); + QString MixxxOSCStatusFileLocation = MixxxOSCStatusFilePath + "/MixxxOSCStatus.txt"; + QFile MixxxOSCStatusFile(MixxxOSCStatusFileLocation); + MixxxOSCStatusFile.remove(); + MixxxOSCStatusFile.open(QIODevice::ReadWrite | QIODevice::Append); + QTextStream MixxxOSCStatusTxt(&MixxxOSCStatusFile); + if (m_pCoreServices->getSettings()->getValue(ConfigKey("[OSC]", "OscEnabled"))) { + MixxxOSCStatusTxt << QString("OSC enabled") << "\n"; + // OscFunctionsSendPtrType(m_pCoreServices->getSettings(), + // "[Osc]", + // "OscSync", + // FLOATBODY, + // "", + // 0, + // 0, + // 1); + OscReceiverMain(m_pCoreServices->getSettings()); + } else { + MixxxOSCStatusTxt << QString("OSC NOT enabled") << "\n"; + } + MixxxOSCStatusFile.close(); +} diff --git a/src/mixxxmainwindow.h b/src/mixxxmainwindow.h index 1c673578ce3..63e8c2cf2c1 100644 --- a/src/mixxxmainwindow.h +++ b/src/mixxxmainwindow.h @@ -104,6 +104,7 @@ class MixxxMainWindow : public QMainWindow { private: void initializeWindow(); void checkDirectRendering(); + void oscEnable(); /// Load skin to a QWidget that we set as the central widget. bool loadConfiguredSkin(); diff --git a/src/osc/ip/IpEndpointName.cpp b/src/osc/ip/IpEndpointName.cpp new file mode 100644 index 00000000000..60ece8a1a9e --- /dev/null +++ b/src/osc/ip/IpEndpointName.cpp @@ -0,0 +1,88 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + The text above constitutes the entire oscpack license; however, + the oscpack developer(s) also make the following non-binding requests: + + Any person wishing to distribute modifications to the Software is + requested to send the modifications to the original developer so that + they can be incorporated into the canonical version. It is also + requested that these non-binding requests be included whenever the + above license is reproduced. +*/ +#include "IpEndpointName.h" + +#include + +#include "NetworkingUtils.h" + + +unsigned long IpEndpointName::GetHostByName( const char *s ) +{ + return ::GetHostByName(s); +} + + +void IpEndpointName::AddressAsString( char *s ) const +{ + if( address == ANY_ADDRESS ){ + std::sprintf( s, "" ); + }else{ + std::sprintf( s, "%d.%d.%d.%d", + (int)((address >> 24) & 0xFF), + (int)((address >> 16) & 0xFF), + (int)((address >> 8) & 0xFF), + (int)(address & 0xFF) ); + } +} + + +void IpEndpointName::AddressAndPortAsString( char *s ) const +{ + if( port == ANY_PORT ){ + if( address == ANY_ADDRESS ){ + std::sprintf( s, ":" ); + }else{ + std::sprintf( s, "%d.%d.%d.%d:", + (int)((address >> 24) & 0xFF), + (int)((address >> 16) & 0xFF), + (int)((address >> 8) & 0xFF), + (int)(address & 0xFF) ); + } + }else{ + if( address == ANY_ADDRESS ){ + std::sprintf( s, ":%d", port ); + }else{ + std::sprintf( s, "%d.%d.%d.%d:%d", + (int)((address >> 24) & 0xFF), + (int)((address >> 16) & 0xFF), + (int)((address >> 8) & 0xFF), + (int)(address & 0xFF), + (int)port ); + } + } +} diff --git a/src/osc/ip/IpEndpointName.h b/src/osc/ip/IpEndpointName.h new file mode 100644 index 00000000000..a88c34248ee --- /dev/null +++ b/src/osc/ip/IpEndpointName.h @@ -0,0 +1,83 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + The text above constitutes the entire oscpack license; however, + the oscpack developer(s) also make the following non-binding requests: + + Any person wishing to distribute modifications to the Software is + requested to send the modifications to the original developer so that + they can be incorporated into the canonical version. It is also + requested that these non-binding requests be included whenever the + above license is reproduced. +*/ +#ifndef INCLUDED_OSCPACK_IPENDPOINTNAME_H +#define INCLUDED_OSCPACK_IPENDPOINTNAME_H + + +class IpEndpointName{ + static unsigned long GetHostByName( const char *s ); +public: + static const unsigned long ANY_ADDRESS = 0xFFFFFFFF; + static const int ANY_PORT = -1; + + IpEndpointName() + : address( ANY_ADDRESS ), port( ANY_PORT ) {} + IpEndpointName( int port_ ) + : address( ANY_ADDRESS ), port( port_ ) {} + IpEndpointName( unsigned long ipAddress_, int port_ ) + : address( ipAddress_ ), port( port_ ) {} + IpEndpointName( const char *addressName, int port_=ANY_PORT ) + : address( GetHostByName( addressName ) ) + , port( port_ ) {} + IpEndpointName( int addressA, int addressB, int addressC, int addressD, int port_=ANY_PORT ) + : address( ( (addressA << 24) | (addressB << 16) | (addressC << 8) | addressD ) ) + , port( port_ ) {} + + // address and port are maintained in host byte order here + unsigned long address; + int port; + + bool IsMulticastAddress() const { return ((address >> 24) & 0xFF) >= 224 && ((address >> 24) & 0xFF) <= 239; } + + enum { ADDRESS_STRING_LENGTH=17 }; + void AddressAsString( char *s ) const; + + enum { ADDRESS_AND_PORT_STRING_LENGTH=23}; + void AddressAndPortAsString( char *s ) const; +}; + +inline bool operator==( const IpEndpointName& lhs, const IpEndpointName& rhs ) +{ + return (lhs.address == rhs.address && lhs.port == rhs.port ); +} + +inline bool operator!=( const IpEndpointName& lhs, const IpEndpointName& rhs ) +{ + return !(lhs == rhs); +} + +#endif /* INCLUDED_OSCPACK_IPENDPOINTNAME_H */ diff --git a/src/osc/ip/NetworkingUtils.h b/src/osc/ip/NetworkingUtils.h new file mode 100644 index 00000000000..457cbd6cfb6 --- /dev/null +++ b/src/osc/ip/NetworkingUtils.h @@ -0,0 +1,56 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + The text above constitutes the entire oscpack license; however, + the oscpack developer(s) also make the following non-binding requests: + + Any person wishing to distribute modifications to the Software is + requested to send the modifications to the original developer so that + they can be incorporated into the canonical version. It is also + requested that these non-binding requests be included whenever the + above license is reproduced. +*/ +#ifndef INCLUDED_OSCPACK_NETWORKINGUTILS_H +#define INCLUDED_OSCPACK_NETWORKINGUTILS_H + + +// in general NetworkInitializer is only used internally, but if you're +// application creates multiple sockets from different threads at runtime you +// should instantiate one of these in main just to make sure the networking +// layer is initialized. +class NetworkInitializer{ +public: + NetworkInitializer(); + ~NetworkInitializer(); +}; + + +// return ip address of host name in host byte order +unsigned long GetHostByName( const char *name ); + + +#endif /* INCLUDED_OSCPACK_NETWORKINGUTILS_H */ diff --git a/src/osc/ip/PacketListener.h b/src/osc/ip/PacketListener.h new file mode 100644 index 00000000000..512e6399989 --- /dev/null +++ b/src/osc/ip/PacketListener.h @@ -0,0 +1,50 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + The text above constitutes the entire oscpack license; however, + the oscpack developer(s) also make the following non-binding requests: + + Any person wishing to distribute modifications to the Software is + requested to send the modifications to the original developer so that + they can be incorporated into the canonical version. It is also + requested that these non-binding requests be included whenever the + above license is reproduced. +*/ +#ifndef INCLUDED_OSCPACK_PACKETLISTENER_H +#define INCLUDED_OSCPACK_PACKETLISTENER_H + + +class IpEndpointName; + +class PacketListener{ +public: + virtual ~PacketListener() {} + virtual void ProcessPacket( const char *data, int size, + const IpEndpointName& remoteEndpoint ) = 0; +}; + +#endif /* INCLUDED_OSCPACK_PACKETLISTENER_H */ diff --git a/src/osc/ip/TimerListener.h b/src/osc/ip/TimerListener.h new file mode 100644 index 00000000000..f7fd3c7ece9 --- /dev/null +++ b/src/osc/ip/TimerListener.h @@ -0,0 +1,47 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + The text above constitutes the entire oscpack license; however, + the oscpack developer(s) also make the following non-binding requests: + + Any person wishing to distribute modifications to the Software is + requested to send the modifications to the original developer so that + they can be incorporated into the canonical version. It is also + requested that these non-binding requests be included whenever the + above license is reproduced. +*/ +#ifndef INCLUDED_OSCPACK_TIMERLISTENER_H +#define INCLUDED_OSCPACK_TIMERLISTENER_H + + +class TimerListener{ +public: + virtual ~TimerListener() {} + virtual void TimerExpired() = 0; +}; + +#endif /* INCLUDED_OSCPACK_TIMERLISTENER_H */ diff --git a/src/osc/ip/UdpSocket.h b/src/osc/ip/UdpSocket.h new file mode 100644 index 00000000000..bf8cf090bdb --- /dev/null +++ b/src/osc/ip/UdpSocket.h @@ -0,0 +1,178 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + The text above constitutes the entire oscpack license; however, + the oscpack developer(s) also make the following non-binding requests: + + Any person wishing to distribute modifications to the Software is + requested to send the modifications to the original developer so that + they can be incorporated into the canonical version. It is also + requested that these non-binding requests be included whenever the + above license is reproduced. +*/ +#ifndef INCLUDED_OSCPACK_UDPSOCKET_H +#define INCLUDED_OSCPACK_UDPSOCKET_H + +#include // size_t + +#include "NetworkingUtils.h" +#include "IpEndpointName.h" + + +class PacketListener; +class TimerListener; + +class UdpSocket; + +class SocketReceiveMultiplexer{ + class Implementation; + Implementation *impl_; + + friend class UdpSocket; + +public: + SocketReceiveMultiplexer(); + ~SocketReceiveMultiplexer(); + + // only call the attach/detach methods _before_ calling Run + + // only one listener per socket, each socket at most once + void AttachSocketListener( UdpSocket *socket, PacketListener *listener ); + void DetachSocketListener( UdpSocket *socket, PacketListener *listener ); + + void AttachPeriodicTimerListener( int periodMilliseconds, TimerListener *listener ); + void AttachPeriodicTimerListener( + int initialDelayMilliseconds, int periodMilliseconds, TimerListener *listener ); + void DetachPeriodicTimerListener( TimerListener *listener ); + + void Run(); // loop and block processing messages indefinitely + void RunUntilSigInt(); + void Break(); // call this from a listener to exit once the listener returns + void AsynchronousBreak(); // call this from another thread or signal handler to exit the Run() state +}; + + +class UdpSocket{ + class Implementation; + Implementation *impl_; + + friend class SocketReceiveMultiplexer::Implementation; + +public: + + // Ctor throws std::runtime_error if there's a problem + // initializing the socket. + UdpSocket(); + virtual ~UdpSocket(); + + // Enable broadcast addresses (e.g. x.x.x.255) + // Sets SO_BROADCAST socket option. + void SetEnableBroadcast( bool enableBroadcast ); + + // Enable multiple listeners for a single port on same + // network interface* + // Sets SO_REUSEADDR (also SO_REUSEPORT on OS X). + // [*] The exact behavior of SO_REUSEADDR and + // SO_REUSEPORT is undefined for some common cases + // and may have drastically different behavior on different + // operating systems. + void SetAllowReuse( bool allowReuse ); + + + // The socket is created in an unbound, unconnected state + // such a socket can only be used to send to an arbitrary + // address using SendTo(). To use Send() you need to first + // connect to a remote endpoint using Connect(). To use + // ReceiveFrom you need to first bind to a local endpoint + // using Bind(). + + // Retrieve the local endpoint name when sending to 'to' + IpEndpointName LocalEndpointFor( const IpEndpointName& remoteEndpoint ) const; + + // Connect to a remote endpoint which is used as the target + // for calls to Send() + void Connect( const IpEndpointName& remoteEndpoint ); + void Send( const char *data, std::size_t size ); + void SendTo( const IpEndpointName& remoteEndpoint, const char *data, std::size_t size ); + + + // Bind a local endpoint to receive incoming data. Endpoint + // can be 'any' for the system to choose an endpoint + void Bind( const IpEndpointName& localEndpoint ); + bool IsBound() const; + + std::size_t ReceiveFrom( IpEndpointName& remoteEndpoint, char *data, std::size_t size ); +}; + + +// convenience classes for transmitting and receiving +// they just call Connect and/or Bind in the ctor. +// note that you can still use a receive socket +// for transmitting etc + +class UdpTransmitSocket : public UdpSocket{ +public: + UdpTransmitSocket( const IpEndpointName& remoteEndpoint ) + { Connect( remoteEndpoint ); } +}; + + +class UdpReceiveSocket : public UdpSocket{ +public: + UdpReceiveSocket( const IpEndpointName& localEndpoint ) + { Bind( localEndpoint ); } +}; + + +// UdpListeningReceiveSocket provides a simple way to bind one listener +// to a single socket without having to manually set up a SocketReceiveMultiplexer + +class UdpListeningReceiveSocket : public UdpSocket{ + SocketReceiveMultiplexer mux_; + PacketListener *listener_; + + public: + UdpListeningReceiveSocket(const IpEndpointName& localEndpoint, PacketListener* listener) + : listener_(listener) + + { + Bind( localEndpoint ); + mux_.AttachSocketListener( this, listener_ ); + } + + ~UdpListeningReceiveSocket() + { mux_.DetachSocketListener( this, listener_ ); } + + // see SocketReceiveMultiplexer above for the behaviour of these methods... + void Run() { mux_.Run(); } + void RunUntilSigInt() { mux_.RunUntilSigInt(); } + void Break() { mux_.Break(); } + void AsynchronousBreak() { mux_.AsynchronousBreak(); } +}; + + +#endif /* INCLUDED_OSCPACK_UDPSOCKET_H */ diff --git a/src/osc/ip/posix/NetworkingUtils.cpp b/src/osc/ip/posix/NetworkingUtils.cpp new file mode 100644 index 00000000000..31b51f2cf21 --- /dev/null +++ b/src/osc/ip/posix/NetworkingUtils.cpp @@ -0,0 +1,64 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + The text above constitutes the entire oscpack license; however, + the oscpack developer(s) also make the following non-binding requests: + + Any person wishing to distribute modifications to the Software is + requested to send the modifications to the original developer so that + they can be incorporated into the canonical version. It is also + requested that these non-binding requests be included whenever the + above license is reproduced. +*/ +#include "ip/NetworkingUtils.h" + +#include +#include +#include + +#include + + + +NetworkInitializer::NetworkInitializer() {} + +NetworkInitializer::~NetworkInitializer() {} + + +unsigned long GetHostByName( const char *name ) +{ + unsigned long result = 0; + + struct hostent *h = gethostbyname( name ); + if( h ){ + struct in_addr a; + std::memcpy( &a, h->h_addr_list[0], h->h_length ); + result = ntohl(a.s_addr); + } + + return result; +} diff --git a/src/osc/ip/posix/UdpSocket.cpp b/src/osc/ip/posix/UdpSocket.cpp new file mode 100644 index 00000000000..f36e7879d22 --- /dev/null +++ b/src/osc/ip/posix/UdpSocket.cpp @@ -0,0 +1,601 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + The text above constitutes the entire oscpack license; however, + the oscpack developer(s) also make the following non-binding requests: + + Any person wishing to distribute modifications to the Software is + requested to send the modifications to the original developer so that + they can be incorporated into the canonical version. It is also + requested that these non-binding requests be included whenever the + above license is reproduced. +*/ +#include "ip/UdpSocket.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include // for sockaddr_in + +#include +#include +#include +#include + +#include +#include +#include // for memset +#include +#include + +#include "ip/PacketListener.h" +#include "ip/TimerListener.h" + + +#if defined(__APPLE__) && !defined(_SOCKLEN_T) +// pre system 10.3 didn't have socklen_t +typedef ssize_t socklen_t; +#endif + + +static void SockaddrFromIpEndpointName( struct sockaddr_in& sockAddr, const IpEndpointName& endpoint ) +{ + std::memset( (char *)&sockAddr, 0, sizeof(sockAddr ) ); + sockAddr.sin_family = AF_INET; + + sockAddr.sin_addr.s_addr = + (endpoint.address == IpEndpointName::ANY_ADDRESS) + ? INADDR_ANY + : htonl( endpoint.address ); + + sockAddr.sin_port = + (endpoint.port == IpEndpointName::ANY_PORT) + ? 0 + : htons( endpoint.port ); +} + + +static IpEndpointName IpEndpointNameFromSockaddr( const struct sockaddr_in& sockAddr ) +{ + return IpEndpointName( + (sockAddr.sin_addr.s_addr == INADDR_ANY) + ? IpEndpointName::ANY_ADDRESS + : ntohl( sockAddr.sin_addr.s_addr ), + (sockAddr.sin_port == 0) + ? IpEndpointName::ANY_PORT + : ntohs( sockAddr.sin_port ) + ); +} + + +class UdpSocket::Implementation{ + bool isBound_; + bool isConnected_; + + int socket_; + struct sockaddr_in connectedAddr_; + struct sockaddr_in sendToAddr_; + +public: + + Implementation() + : isBound_( false ) + , isConnected_( false ) + , socket_( -1 ) + { + if( (socket_ = socket( AF_INET, SOCK_DGRAM, 0 )) == -1 ){ + throw std::runtime_error("unable to create udp socket\n"); + } + + std::memset( &sendToAddr_, 0, sizeof(sendToAddr_) ); + sendToAddr_.sin_family = AF_INET; + } + + ~Implementation() + { + if (socket_ != -1) close(socket_); + } + + void SetEnableBroadcast( bool enableBroadcast ) + { + int broadcast = (enableBroadcast) ? 1 : 0; // int on posix + setsockopt(socket_, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast)); + } + + void SetAllowReuse( bool allowReuse ) + { + int reuseAddr = (allowReuse) ? 1 : 0; // int on posix + setsockopt(socket_, SOL_SOCKET, SO_REUSEADDR, &reuseAddr, sizeof(reuseAddr)); + +#ifdef __APPLE__ + // needed also for OS X - enable multiple listeners for a single port on same network interface + int reusePort = (allowReuse) ? 1 : 0; // int on posix + setsockopt(socket_, SOL_SOCKET, SO_REUSEPORT, &reusePort, sizeof(reusePort)); +#endif + } + + IpEndpointName LocalEndpointFor( const IpEndpointName& remoteEndpoint ) const + { + assert( isBound_ ); + + // first connect the socket to the remote server + + struct sockaddr_in connectSockAddr; + SockaddrFromIpEndpointName( connectSockAddr, remoteEndpoint ); + + if (connect(socket_, (struct sockaddr *)&connectSockAddr, sizeof(connectSockAddr)) < 0) { + throw std::runtime_error("unable to connect udp socket\n"); + } + + // get the address + + struct sockaddr_in sockAddr; + std::memset( (char *)&sockAddr, 0, sizeof(sockAddr ) ); + socklen_t length = sizeof(sockAddr); + if (getsockname(socket_, (struct sockaddr *)&sockAddr, &length) < 0) { + throw std::runtime_error("unable to getsockname\n"); + } + + if( isConnected_ ){ + // reconnect to the connected address + + if (connect(socket_, (struct sockaddr *)&connectedAddr_, sizeof(connectedAddr_)) < 0) { + throw std::runtime_error("unable to connect udp socket\n"); + } + + }else{ + // unconnect from the remote address + + struct sockaddr_in unconnectSockAddr; + std::memset( (char *)&unconnectSockAddr, 0, sizeof(unconnectSockAddr ) ); + unconnectSockAddr.sin_family = AF_UNSPEC; + // address fields are zero + int connectResult = connect(socket_, (struct sockaddr *)&unconnectSockAddr, sizeof(unconnectSockAddr)); + if ( connectResult < 0 && errno != EAFNOSUPPORT ) { + throw std::runtime_error("unable to un-connect udp socket\n"); + } + } + + return IpEndpointNameFromSockaddr( sockAddr ); + } + + void Connect( const IpEndpointName& remoteEndpoint ) + { + SockaddrFromIpEndpointName( connectedAddr_, remoteEndpoint ); + + if (connect(socket_, (struct sockaddr *)&connectedAddr_, sizeof(connectedAddr_)) < 0) { + throw std::runtime_error("unable to connect udp socket\n"); + } + + isConnected_ = true; + } + + void Send( const char *data, std::size_t size ) + { + assert( isConnected_ ); + + send( socket_, data, size, 0 ); + } + + void SendTo( const IpEndpointName& remoteEndpoint, const char *data, std::size_t size ) + { + sendToAddr_.sin_addr.s_addr = htonl( remoteEndpoint.address ); + sendToAddr_.sin_port = htons( remoteEndpoint.port ); + + sendto( socket_, data, size, 0, (sockaddr*)&sendToAddr_, sizeof(sendToAddr_) ); + } + + void Bind( const IpEndpointName& localEndpoint ) + { + struct sockaddr_in bindSockAddr; + SockaddrFromIpEndpointName( bindSockAddr, localEndpoint ); + + if (bind(socket_, (struct sockaddr *)&bindSockAddr, sizeof(bindSockAddr)) < 0) { + throw std::runtime_error("unable to bind udp socket\n"); + } + + isBound_ = true; + } + + bool IsBound() const { return isBound_; } + + std::size_t ReceiveFrom( IpEndpointName& remoteEndpoint, char *data, std::size_t size ) + { + assert( isBound_ ); + + struct sockaddr_in fromAddr; + socklen_t fromAddrLen = sizeof(fromAddr); + + ssize_t result = recvfrom(socket_, data, size, 0, + (struct sockaddr *) &fromAddr, (socklen_t*)&fromAddrLen); + if( result < 0 ) + return 0; + + remoteEndpoint.address = ntohl(fromAddr.sin_addr.s_addr); + remoteEndpoint.port = ntohs(fromAddr.sin_port); + + return (std::size_t)result; + } + + int Socket() { return socket_; } +}; + +UdpSocket::UdpSocket() +{ + impl_ = new Implementation(); +} + +UdpSocket::~UdpSocket() +{ + delete impl_; +} + +void UdpSocket::SetEnableBroadcast( bool enableBroadcast ) +{ + impl_->SetEnableBroadcast( enableBroadcast ); +} + +void UdpSocket::SetAllowReuse( bool allowReuse ) +{ + impl_->SetAllowReuse( allowReuse ); +} + +IpEndpointName UdpSocket::LocalEndpointFor( const IpEndpointName& remoteEndpoint ) const +{ + return impl_->LocalEndpointFor( remoteEndpoint ); +} + +void UdpSocket::Connect( const IpEndpointName& remoteEndpoint ) +{ + impl_->Connect( remoteEndpoint ); +} + +void UdpSocket::Send( const char *data, std::size_t size ) +{ + impl_->Send( data, size ); +} + +void UdpSocket::SendTo( const IpEndpointName& remoteEndpoint, const char *data, std::size_t size ) +{ + impl_->SendTo( remoteEndpoint, data, size ); +} + +void UdpSocket::Bind( const IpEndpointName& localEndpoint ) +{ + impl_->Bind( localEndpoint ); +} + +bool UdpSocket::IsBound() const +{ + return impl_->IsBound(); +} + +std::size_t UdpSocket::ReceiveFrom( IpEndpointName& remoteEndpoint, char *data, std::size_t size ) +{ + return impl_->ReceiveFrom( remoteEndpoint, data, size ); +} + + +struct AttachedTimerListener{ + AttachedTimerListener( int id, int p, TimerListener *tl ) + : initialDelayMs( id ) + , periodMs( p ) + , listener( tl ) {} + int initialDelayMs; + int periodMs; + TimerListener *listener; +}; + + +static bool CompareScheduledTimerCalls( + const std::pair< double, AttachedTimerListener > & lhs, const std::pair< double, AttachedTimerListener > & rhs ) +{ + return lhs.first < rhs.first; +} + + +SocketReceiveMultiplexer *multiplexerInstanceToAbortWithSigInt_ = 0; + +extern "C" /*static*/ void InterruptSignalHandler( int ); +/*static*/ void InterruptSignalHandler( int ) +{ + multiplexerInstanceToAbortWithSigInt_->AsynchronousBreak(); + signal( SIGINT, SIG_DFL ); +} + + +class SocketReceiveMultiplexer::Implementation{ + std::vector< std::pair< PacketListener*, UdpSocket* > > socketListeners_; + std::vector< AttachedTimerListener > timerListeners_; + + volatile bool break_; + int breakPipe_[2]; // [0] is the reader descriptor and [1] the writer + + double GetCurrentTimeMs() const + { + struct timeval t; + + gettimeofday( &t, 0 ); + + return ((double)t.tv_sec*1000.) + ((double)t.tv_usec / 1000.); + } + +public: + Implementation() + { + if( pipe(breakPipe_) != 0 ) + throw std::runtime_error( "creation of asynchronous break pipes failed\n" ); + } + + ~Implementation() + { + close( breakPipe_[0] ); + close( breakPipe_[1] ); + } + + void AttachSocketListener( UdpSocket *socket, PacketListener *listener ) + { + assert( std::find( socketListeners_.begin(), socketListeners_.end(), std::make_pair(listener, socket) ) == socketListeners_.end() ); + // we don't check that the same socket has been added multiple times, even though this is an error + socketListeners_.push_back( std::make_pair( listener, socket ) ); + } + + void DetachSocketListener( UdpSocket *socket, PacketListener *listener ) + { + std::vector< std::pair< PacketListener*, UdpSocket* > >::iterator i = + std::find( socketListeners_.begin(), socketListeners_.end(), std::make_pair(listener, socket) ); + assert( i != socketListeners_.end() ); + + socketListeners_.erase( i ); + } + + void AttachPeriodicTimerListener( int periodMilliseconds, TimerListener *listener ) + { + timerListeners_.push_back( AttachedTimerListener( periodMilliseconds, periodMilliseconds, listener ) ); + } + + void AttachPeriodicTimerListener( int initialDelayMilliseconds, int periodMilliseconds, TimerListener *listener ) + { + timerListeners_.push_back( AttachedTimerListener( initialDelayMilliseconds, periodMilliseconds, listener ) ); + } + + void DetachPeriodicTimerListener( TimerListener *listener ) + { + std::vector< AttachedTimerListener >::iterator i = timerListeners_.begin(); + while( i != timerListeners_.end() ){ + if( i->listener == listener ) + break; + ++i; + } + + assert( i != timerListeners_.end() ); + + timerListeners_.erase( i ); + } + + void Run() + { + break_ = false; + char *data = 0; + + try{ + + // configure the master fd_set for select() + + fd_set masterfds, tempfds; + FD_ZERO( &masterfds ); + FD_ZERO( &tempfds ); + + // in addition to listening to the inbound sockets we + // also listen to the asynchronous break pipe, so that AsynchronousBreak() + // can break us out of select() from another thread. + FD_SET( breakPipe_[0], &masterfds ); + int fdmax = breakPipe_[0]; + + for( std::vector< std::pair< PacketListener*, UdpSocket* > >::iterator i = socketListeners_.begin(); + i != socketListeners_.end(); ++i ){ + + if( fdmax < i->second->impl_->Socket() ) + fdmax = i->second->impl_->Socket(); + FD_SET( i->second->impl_->Socket(), &masterfds ); + } + + + // configure the timer queue + double currentTimeMs = GetCurrentTimeMs(); + + // expiry time ms, listener + std::vector< std::pair< double, AttachedTimerListener > > timerQueue_; + for( std::vector< AttachedTimerListener >::iterator i = timerListeners_.begin(); + i != timerListeners_.end(); ++i ) + timerQueue_.push_back( std::make_pair( currentTimeMs + i->initialDelayMs, *i ) ); + std::sort( timerQueue_.begin(), timerQueue_.end(), CompareScheduledTimerCalls ); + + const int MAX_BUFFER_SIZE = 4098; + data = new char[ MAX_BUFFER_SIZE ]; + IpEndpointName remoteEndpoint; + + struct timeval timeout; + + while( !break_ ){ + tempfds = masterfds; + + struct timeval *timeoutPtr = 0; + if( !timerQueue_.empty() ){ + double timeoutMs = timerQueue_.front().first - GetCurrentTimeMs(); + if( timeoutMs < 0 ) + timeoutMs = 0; + + long timoutSecondsPart = (long)(timeoutMs * .001); + timeout.tv_sec = (time_t)timoutSecondsPart; + // 1000000 microseconds in a second + timeout.tv_usec = (suseconds_t)((timeoutMs - (timoutSecondsPart * 1000)) * 1000); + timeoutPtr = &timeout; + } + + if( select( fdmax + 1, &tempfds, 0, 0, timeoutPtr ) < 0 ){ + if( break_ ){ + break; + }else if( errno == EINTR ){ + // on returning an error, select() doesn't clear tempfds. + // so tempfds would remain all set, which would cause read( breakPipe_[0]... + // below to block indefinitely. therefore if select returns EINTR we restart + // the while() loop instead of continuing on to below. + continue; + }else{ + throw std::runtime_error("select failed\n"); + } + } + + if( FD_ISSET( breakPipe_[0], &tempfds ) ){ + // clear pending data from the asynchronous break pipe + char c; + read( breakPipe_[0], &c, 1 ); + } + + if( break_ ) + break; + + for( std::vector< std::pair< PacketListener*, UdpSocket* > >::iterator i = socketListeners_.begin(); + i != socketListeners_.end(); ++i ){ + + if( FD_ISSET( i->second->impl_->Socket(), &tempfds ) ){ + + std::size_t size = i->second->ReceiveFrom( remoteEndpoint, data, MAX_BUFFER_SIZE ); + if( size > 0 ){ + i->first->ProcessPacket( data, (int)size, remoteEndpoint ); + if( break_ ) + break; + } + } + } + + // execute any expired timers + currentTimeMs = GetCurrentTimeMs(); + bool resort = false; + for( std::vector< std::pair< double, AttachedTimerListener > >::iterator i = timerQueue_.begin(); + i != timerQueue_.end() && i->first <= currentTimeMs; ++i ){ + + i->second.listener->TimerExpired(); + if( break_ ) + break; + + i->first += i->second.periodMs; + resort = true; + } + if( resort ) + std::sort( timerQueue_.begin(), timerQueue_.end(), CompareScheduledTimerCalls ); + } + + delete [] data; + }catch(...){ + if( data ) + delete [] data; + throw; + } + } + + void Break() + { + break_ = true; + } + + void AsynchronousBreak() + { + break_ = true; + + // Send a termination message to the asynchronous break pipe, so select() will return + write( breakPipe_[1], "!", 1 ); + } +}; + + + +SocketReceiveMultiplexer::SocketReceiveMultiplexer() +{ + impl_ = new Implementation(); +} + +SocketReceiveMultiplexer::~SocketReceiveMultiplexer() +{ + delete impl_; +} + +void SocketReceiveMultiplexer::AttachSocketListener( UdpSocket *socket, PacketListener *listener ) +{ + impl_->AttachSocketListener( socket, listener ); +} + +void SocketReceiveMultiplexer::DetachSocketListener( UdpSocket *socket, PacketListener *listener ) +{ + impl_->DetachSocketListener( socket, listener ); +} + +void SocketReceiveMultiplexer::AttachPeriodicTimerListener( int periodMilliseconds, TimerListener *listener ) +{ + impl_->AttachPeriodicTimerListener( periodMilliseconds, listener ); +} + +void SocketReceiveMultiplexer::AttachPeriodicTimerListener( int initialDelayMilliseconds, int periodMilliseconds, TimerListener *listener ) +{ + impl_->AttachPeriodicTimerListener( initialDelayMilliseconds, periodMilliseconds, listener ); +} + +void SocketReceiveMultiplexer::DetachPeriodicTimerListener( TimerListener *listener ) +{ + impl_->DetachPeriodicTimerListener( listener ); +} + +void SocketReceiveMultiplexer::Run() +{ + impl_->Run(); +} + +void SocketReceiveMultiplexer::RunUntilSigInt() +{ + assert( multiplexerInstanceToAbortWithSigInt_ == 0 ); /* at present we support only one multiplexer instance running until sig int */ + multiplexerInstanceToAbortWithSigInt_ = this; + signal( SIGINT, InterruptSignalHandler ); + impl_->Run(); + signal( SIGINT, SIG_DFL ); + multiplexerInstanceToAbortWithSigInt_ = 0; +} + +void SocketReceiveMultiplexer::Break() +{ + impl_->Break(); +} + +void SocketReceiveMultiplexer::AsynchronousBreak() +{ + impl_->AsynchronousBreak(); +} diff --git a/src/osc/ip/win32/NetworkingUtils.cpp b/src/osc/ip/win32/NetworkingUtils.cpp new file mode 100644 index 00000000000..12c97271d6c --- /dev/null +++ b/src/osc/ip/win32/NetworkingUtils.cpp @@ -0,0 +1,96 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + The text above constitutes the entire oscpack license; however, + the oscpack developer(s) also make the following non-binding requests: + + Any person wishing to distribute modifications to the Software is + requested to send the modifications to the original developer so that + they can be incorporated into the canonical version. It is also + requested that these non-binding requests be included whenever the + above license is reproduced. +*/ +// #include "ip/NetworkingUtils.h" +#include "../NetworkingUtils.h" + +#include // this must come first to prevent errors with MSVC7 +#include + +#include + + +static LONG initCount_ = 0; +static bool winsockInitialized_ = false; + +NetworkInitializer::NetworkInitializer() +{ + if( InterlockedIncrement( &initCount_ ) == 1 ){ + // there is a race condition here if one thread tries to access + // the library while another is still initializing it. + // i can't think of an easy way to fix it so i'm telling you here + // in case you need to init the library from two threads at once. + // this is why the header file advises to instantiate one of these + // in main() so that the initialization happens globally + + // initialize winsock + WSAData wsaData; + int nCode = WSAStartup(MAKEWORD(1, 1), &wsaData); + if( nCode != 0 ){ + //std::cout << "WSAStartup() failed with error code " << nCode << "\n"; + }else{ + winsockInitialized_ = true; + } + } +} + + +NetworkInitializer::~NetworkInitializer() +{ + if( InterlockedDecrement( &initCount_ ) == 0 ){ + if( winsockInitialized_ ){ + WSACleanup(); + winsockInitialized_ = false; + } + } +} + + +unsigned long GetHostByName( const char *name ) +{ + NetworkInitializer networkInitializer; + + unsigned long result = 0; + + struct hostent *h = gethostbyname( name ); + if( h ){ + struct in_addr a; + std::memcpy( &a, h->h_addr_list[0], h->h_length ); + result = ntohl(a.s_addr); + } + + return result; +} diff --git a/src/osc/ip/win32/UdpSocket.cpp b/src/osc/ip/win32/UdpSocket.cpp new file mode 100644 index 00000000000..27fd615d32a --- /dev/null +++ b/src/osc/ip/win32/UdpSocket.cpp @@ -0,0 +1,579 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + The text above constitutes the entire oscpack license; however, + the oscpack developer(s) also make the following non-binding requests: + + Any person wishing to distribute modifications to the Software is + requested to send the modifications to the original developer so that + they can be incorporated into the canonical version. It is also + requested that these non-binding requests be included whenever the + above license is reproduced. +*/ + +#include // this must come first to prevent errors with MSVC7 +#include +#include // for timeGetTime() + +#ifndef WINCE +#include +#endif + +#include +#include +#include // for memset +#include +#include + +#include "../UdpSocket.h" // usually I'd include the module header first + // but this is causing conflicts with BCB4 due to + // std::size_t usage. + +#include "../NetworkingUtils.h" +#include "../PacketListener.h" +#include "../TimerListener.h" + +//#include "ip/UdpSocket.h" // usually I'd include the module header first + // but this is causing conflicts with BCB4 due to + // std::size_t usage. + +//#include "ip/NetworkingUtils.h" +//#include "ip/PacketListener.h" +//#include "ip/TimerListener.h" + + + +typedef int socklen_t; + + +static void SockaddrFromIpEndpointName( struct sockaddr_in& sockAddr, const IpEndpointName& endpoint ) +{ + std::memset( (char *)&sockAddr, 0, sizeof(sockAddr ) ); + sockAddr.sin_family = AF_INET; + + sockAddr.sin_addr.s_addr = + (endpoint.address == IpEndpointName::ANY_ADDRESS) + ? INADDR_ANY + : htonl( endpoint.address ); + + sockAddr.sin_port = + (endpoint.port == IpEndpointName::ANY_PORT) + ? (short)0 + : htons( (short)endpoint.port ); +} + + +static IpEndpointName IpEndpointNameFromSockaddr( const struct sockaddr_in& sockAddr ) +{ + return IpEndpointName( + (sockAddr.sin_addr.s_addr == INADDR_ANY) + ? IpEndpointName::ANY_ADDRESS + : ntohl( sockAddr.sin_addr.s_addr ), + (sockAddr.sin_port == 0) + ? IpEndpointName::ANY_PORT + : ntohs( sockAddr.sin_port ) + ); +} + + +class UdpSocket::Implementation{ + NetworkInitializer networkInitializer_; + + bool isBound_; + bool isConnected_; + + SOCKET socket_; + struct sockaddr_in connectedAddr_; + struct sockaddr_in sendToAddr_; + +public: + + Implementation() + : isBound_( false ) + , isConnected_( false ) + , socket_( INVALID_SOCKET ) + { + if( (socket_ = socket( AF_INET, SOCK_DGRAM, 0 )) == INVALID_SOCKET ){ + throw std::runtime_error("unable to create udp socket\n"); + } + + std::memset( &sendToAddr_, 0, sizeof(sendToAddr_) ); + sendToAddr_.sin_family = AF_INET; + } + + ~Implementation() + { + if (socket_ != INVALID_SOCKET) closesocket(socket_); + } + + void SetEnableBroadcast( bool enableBroadcast ) + { + char broadcast = (char)((enableBroadcast) ? 1 : 0); // char on win32 + setsockopt(socket_, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast)); + } + + void SetAllowReuse( bool allowReuse ) + { + // Note: SO_REUSEADDR is non-deterministic for listening sockets on Win32. See MSDN article: + // "Using SO_REUSEADDR and SO_EXCLUSIVEADDRUSE" + // http://msdn.microsoft.com/en-us/library/ms740621%28VS.85%29.aspx + + char reuseAddr = (char)((allowReuse) ? 1 : 0); // char on win32 + setsockopt(socket_, SOL_SOCKET, SO_REUSEADDR, &reuseAddr, sizeof(reuseAddr)); + } + + IpEndpointName LocalEndpointFor( const IpEndpointName& remoteEndpoint ) const + { + assert( isBound_ ); + + // first connect the socket to the remote server + + struct sockaddr_in connectSockAddr; + SockaddrFromIpEndpointName( connectSockAddr, remoteEndpoint ); + + if (connect(socket_, (struct sockaddr *)&connectSockAddr, sizeof(connectSockAddr)) < 0) { + throw std::runtime_error("unable to connect udp socket\n"); + } + + // get the address + + struct sockaddr_in sockAddr; + std::memset( (char *)&sockAddr, 0, sizeof(sockAddr ) ); + socklen_t length = sizeof(sockAddr); + if (getsockname(socket_, (struct sockaddr *)&sockAddr, &length) < 0) { + throw std::runtime_error("unable to getsockname\n"); + } + + if( isConnected_ ){ + // reconnect to the connected address + + if (connect(socket_, (struct sockaddr *)&connectedAddr_, sizeof(connectedAddr_)) < 0) { + throw std::runtime_error("unable to connect udp socket\n"); + } + + }else{ + // unconnect from the remote address + + struct sockaddr_in unconnectSockAddr; + SockaddrFromIpEndpointName( unconnectSockAddr, IpEndpointName() ); + + if( connect(socket_, (struct sockaddr *)&unconnectSockAddr, sizeof(unconnectSockAddr)) < 0 + && WSAGetLastError() != WSAEADDRNOTAVAIL ){ + throw std::runtime_error("unable to un-connect udp socket\n"); + } + } + + return IpEndpointNameFromSockaddr( sockAddr ); + } + + void Connect( const IpEndpointName& remoteEndpoint ) + { + SockaddrFromIpEndpointName( connectedAddr_, remoteEndpoint ); + + if (connect(socket_, (struct sockaddr *)&connectedAddr_, sizeof(connectedAddr_)) < 0) { + throw std::runtime_error("unable to connect udp socket\n"); + } + + isConnected_ = true; + } + + void Send( const char *data, std::size_t size ) + { + assert( isConnected_ ); + + send( socket_, data, (int)size, 0 ); + } + + void SendTo( const IpEndpointName& remoteEndpoint, const char *data, std::size_t size ) + { + sendToAddr_.sin_addr.s_addr = htonl( remoteEndpoint.address ); + sendToAddr_.sin_port = htons( (short)remoteEndpoint.port ); + + sendto( socket_, data, (int)size, 0, (sockaddr*)&sendToAddr_, sizeof(sendToAddr_) ); + } + + void Bind( const IpEndpointName& localEndpoint ) + { + struct sockaddr_in bindSockAddr; + SockaddrFromIpEndpointName( bindSockAddr, localEndpoint ); + + if (bind(socket_, (struct sockaddr *)&bindSockAddr, sizeof(bindSockAddr)) < 0) { + throw std::runtime_error("unable to bind udp socket\n"); + } + + isBound_ = true; + } + + bool IsBound() const { return isBound_; } + + std::size_t ReceiveFrom( IpEndpointName& remoteEndpoint, char *data, std::size_t size ) + { + assert( isBound_ ); + + struct sockaddr_in fromAddr; + socklen_t fromAddrLen = sizeof(fromAddr); + + int result = recvfrom(socket_, data, (int)size, 0, + (struct sockaddr *) &fromAddr, (socklen_t*)&fromAddrLen); + if( result < 0 ) + return 0; + + remoteEndpoint.address = ntohl(fromAddr.sin_addr.s_addr); + remoteEndpoint.port = ntohs(fromAddr.sin_port); + + return result; + } + + SOCKET& Socket() { return socket_; } +}; + +UdpSocket::UdpSocket() +{ + impl_ = new Implementation(); +} + +UdpSocket::~UdpSocket() +{ + delete impl_; +} + +void UdpSocket::SetEnableBroadcast( bool enableBroadcast ) +{ + impl_->SetEnableBroadcast( enableBroadcast ); +} + +void UdpSocket::SetAllowReuse( bool allowReuse ) +{ + impl_->SetAllowReuse( allowReuse ); +} + +IpEndpointName UdpSocket::LocalEndpointFor( const IpEndpointName& remoteEndpoint ) const +{ + return impl_->LocalEndpointFor( remoteEndpoint ); +} + +void UdpSocket::Connect( const IpEndpointName& remoteEndpoint ) +{ + impl_->Connect( remoteEndpoint ); +} + +void UdpSocket::Send( const char *data, std::size_t size ) +{ + impl_->Send( data, size ); +} + +void UdpSocket::SendTo( const IpEndpointName& remoteEndpoint, const char *data, std::size_t size ) +{ + impl_->SendTo( remoteEndpoint, data, size ); +} + +void UdpSocket::Bind( const IpEndpointName& localEndpoint ) +{ + impl_->Bind( localEndpoint ); +} + +bool UdpSocket::IsBound() const +{ + return impl_->IsBound(); +} + +std::size_t UdpSocket::ReceiveFrom( IpEndpointName& remoteEndpoint, char *data, std::size_t size ) +{ + return impl_->ReceiveFrom( remoteEndpoint, data, size ); +} + + +struct AttachedTimerListener{ + AttachedTimerListener( int id, int p, TimerListener *tl ) + : initialDelayMs( id ) + , periodMs( p ) + , listener( tl ) {} + int initialDelayMs; + int periodMs; + TimerListener *listener; +}; + + +static bool CompareScheduledTimerCalls( + const std::pair< double, AttachedTimerListener > & lhs, const std::pair< double, AttachedTimerListener > & rhs ) +{ + return lhs.first < rhs.first; +} + + +SocketReceiveMultiplexer *multiplexerInstanceToAbortWithSigInt_ = 0; + +extern "C" /*static*/ void InterruptSignalHandler( int ); +/*static*/ void InterruptSignalHandler( int ) +{ + multiplexerInstanceToAbortWithSigInt_->AsynchronousBreak(); +#ifndef WINCE + signal( SIGINT, SIG_DFL ); +#endif +} + + +class SocketReceiveMultiplexer::Implementation{ + NetworkInitializer networkInitializer_; + + std::vector< std::pair< PacketListener*, UdpSocket* > > socketListeners_; + std::vector< AttachedTimerListener > timerListeners_; + + volatile bool break_; + HANDLE breakEvent_; + + double GetCurrentTimeMs() const + { +#ifndef WINCE + return timeGetTime(); // FIXME: bad choice if you want to run for more than 40 days +#else + return 0; +#endif + } + +public: + Implementation() + { + breakEvent_ = CreateEvent( NULL, FALSE, FALSE, NULL ); + } + + ~Implementation() + { + CloseHandle( breakEvent_ ); + } + + void AttachSocketListener( UdpSocket *socket, PacketListener *listener ) + { + assert( std::find( socketListeners_.begin(), socketListeners_.end(), std::make_pair(listener, socket) ) == socketListeners_.end() ); + // we don't check that the same socket has been added multiple times, even though this is an error + socketListeners_.push_back( std::make_pair( listener, socket ) ); + } + + void DetachSocketListener( UdpSocket *socket, PacketListener *listener ) + { + std::vector< std::pair< PacketListener*, UdpSocket* > >::iterator i = + std::find( socketListeners_.begin(), socketListeners_.end(), std::make_pair(listener, socket) ); + assert( i != socketListeners_.end() ); + + socketListeners_.erase( i ); + } + + void AttachPeriodicTimerListener( int periodMilliseconds, TimerListener *listener ) + { + timerListeners_.push_back( AttachedTimerListener( periodMilliseconds, periodMilliseconds, listener ) ); + } + + void AttachPeriodicTimerListener( int initialDelayMilliseconds, int periodMilliseconds, TimerListener *listener ) + { + timerListeners_.push_back( AttachedTimerListener( initialDelayMilliseconds, periodMilliseconds, listener ) ); + } + + void DetachPeriodicTimerListener( TimerListener *listener ) + { + std::vector< AttachedTimerListener >::iterator i = timerListeners_.begin(); + while( i != timerListeners_.end() ){ + if( i->listener == listener ) + break; + ++i; + } + + assert( i != timerListeners_.end() ); + + timerListeners_.erase( i ); + } + + void Run() + { + break_ = false; + + // prepare the window events which we use to wake up on incoming data + // we use this instead of select() primarily to support the AsyncBreak() + // mechanism. + + std::vector events( socketListeners_.size() + 1, 0 ); + int j=0; + for( std::vector< std::pair< PacketListener*, UdpSocket* > >::iterator i = socketListeners_.begin(); + i != socketListeners_.end(); ++i, ++j ){ + + HANDLE event = CreateEvent( NULL, FALSE, FALSE, NULL ); + WSAEventSelect( i->second->impl_->Socket(), event, FD_READ ); // note that this makes the socket non-blocking which is why we can safely call RecieveFrom() on all sockets below + events[j] = event; + } + + + events[ socketListeners_.size() ] = breakEvent_; // last event in the collection is the break event + + + // configure the timer queue + double currentTimeMs = GetCurrentTimeMs(); + + // expiry time ms, listener + std::vector< std::pair< double, AttachedTimerListener > > timerQueue_; + for( std::vector< AttachedTimerListener >::iterator i = timerListeners_.begin(); + i != timerListeners_.end(); ++i ) + timerQueue_.push_back( std::make_pair( currentTimeMs + i->initialDelayMs, *i ) ); + std::sort( timerQueue_.begin(), timerQueue_.end(), CompareScheduledTimerCalls ); + + const int MAX_BUFFER_SIZE = 4098; + char *data = new char[ MAX_BUFFER_SIZE ]; + IpEndpointName remoteEndpoint; + + while( !break_ ){ + + double currentTimeMs = GetCurrentTimeMs(); + + DWORD waitTime = INFINITE; + if( !timerQueue_.empty() ){ + + waitTime = (DWORD)( timerQueue_.front().first >= currentTimeMs + ? timerQueue_.front().first - currentTimeMs + : 0 ); + } + + DWORD waitResult = WaitForMultipleObjects( (DWORD)socketListeners_.size() + 1, &events[0], FALSE, waitTime ); + if( break_ ) + break; + + if( waitResult != WAIT_TIMEOUT ){ + for( int i = waitResult - WAIT_OBJECT_0; i < (int)socketListeners_.size(); ++i ){ + std::size_t size = socketListeners_[i].second->ReceiveFrom( remoteEndpoint, data, MAX_BUFFER_SIZE ); + if( size > 0 ){ + socketListeners_[i].first->ProcessPacket( data, (int)size, remoteEndpoint ); + if( break_ ) + break; + } + } + } + + // execute any expired timers + currentTimeMs = GetCurrentTimeMs(); + bool resort = false; + for( std::vector< std::pair< double, AttachedTimerListener > >::iterator i = timerQueue_.begin(); + i != timerQueue_.end() && i->first <= currentTimeMs; ++i ){ + + i->second.listener->TimerExpired(); + if( break_ ) + break; + + i->first += i->second.periodMs; + resort = true; + } + if( resort ) + std::sort( timerQueue_.begin(), timerQueue_.end(), CompareScheduledTimerCalls ); + } + + delete [] data; + + // free events + j = 0; + for( std::vector< std::pair< PacketListener*, UdpSocket* > >::iterator i = socketListeners_.begin(); + i != socketListeners_.end(); ++i, ++j ){ + + WSAEventSelect( i->second->impl_->Socket(), events[j], 0 ); // remove association between socket and event + CloseHandle( events[j] ); + unsigned long enableNonblocking = 0; + ioctlsocket( i->second->impl_->Socket(), FIONBIO, &enableNonblocking ); // make the socket blocking again + } + } + + void Break() + { + break_ = true; + } + + void AsynchronousBreak() + { + break_ = true; + SetEvent( breakEvent_ ); + } +}; + + + +SocketReceiveMultiplexer::SocketReceiveMultiplexer() +{ + impl_ = new Implementation(); +} + +SocketReceiveMultiplexer::~SocketReceiveMultiplexer() +{ + delete impl_; +} + +void SocketReceiveMultiplexer::AttachSocketListener( UdpSocket *socket, PacketListener *listener ) +{ + impl_->AttachSocketListener( socket, listener ); +} + +void SocketReceiveMultiplexer::DetachSocketListener( UdpSocket *socket, PacketListener *listener ) +{ + impl_->DetachSocketListener( socket, listener ); +} + +void SocketReceiveMultiplexer::AttachPeriodicTimerListener( int periodMilliseconds, TimerListener *listener ) +{ + impl_->AttachPeriodicTimerListener( periodMilliseconds, listener ); +} + +void SocketReceiveMultiplexer::AttachPeriodicTimerListener( int initialDelayMilliseconds, int periodMilliseconds, TimerListener *listener ) +{ + impl_->AttachPeriodicTimerListener( initialDelayMilliseconds, periodMilliseconds, listener ); +} + +void SocketReceiveMultiplexer::DetachPeriodicTimerListener( TimerListener *listener ) +{ + impl_->DetachPeriodicTimerListener( listener ); +} + +void SocketReceiveMultiplexer::Run() +{ + impl_->Run(); +} + +void SocketReceiveMultiplexer::RunUntilSigInt() +{ + assert( multiplexerInstanceToAbortWithSigInt_ == 0 ); /* at present we support only one multiplexer instance running until sig int */ + multiplexerInstanceToAbortWithSigInt_ = this; +#ifndef WINCE + signal( SIGINT, InterruptSignalHandler ); +#endif + impl_->Run(); +#ifndef WINCE + signal( SIGINT, SIG_DFL ); +#endif + multiplexerInstanceToAbortWithSigInt_ = 0; +} + +void SocketReceiveMultiplexer::Break() +{ + impl_->Break(); +} + +void SocketReceiveMultiplexer::AsynchronousBreak() +{ + impl_->AsynchronousBreak(); +} diff --git a/src/osc/osc/MessageMappingOscPacketListener.h b/src/osc/osc/MessageMappingOscPacketListener.h new file mode 100644 index 00000000000..c16af1684ff --- /dev/null +++ b/src/osc/osc/MessageMappingOscPacketListener.h @@ -0,0 +1,80 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + The text above constitutes the entire oscpack license; however, + the oscpack developer(s) also make the following non-binding requests: + + Any person wishing to distribute modifications to the Software is + requested to send the modifications to the original developer so that + they can be incorporated into the canonical version. It is also + requested that these non-binding requests be included whenever the + above license is reproduced. +*/ +#ifndef INCLUDED_OSCPACK_MESSAGEMAPPINGOSCPACKETLISTENER_H +#define INCLUDED_OSCPACK_MESSAGEMAPPINGOSCPACKETLISTENER_H + +#include +#include + +#include "OscPacketListener.h" + + + +namespace osc{ + +template< class T > +class MessageMappingOscPacketListener : public OscPacketListener{ +public: + typedef void (T::*function_type)(const osc::ReceivedMessage&, const IpEndpointName&); + +protected: + void RegisterMessageFunction( const char *addressPattern, function_type f ) + { + functions_.insert( std::make_pair( addressPattern, f ) ); + } + + virtual void ProcessMessage( const osc::ReceivedMessage& m, + const IpEndpointName& remoteEndpoint ) + { + typename function_map_type::iterator i = functions_.find( m.AddressPattern() ); + if( i != functions_.end() ) + (dynamic_cast(this)->*(i->second))( m, remoteEndpoint ); + } + +private: + struct cstr_compare{ + bool operator()( const char *lhs, const char *rhs ) const + { return std::strcmp( lhs, rhs ) < 0; } + }; + + typedef std::map function_map_type; + function_map_type functions_; +}; + +} // namespace osc + +#endif /* INCLUDED_OSCPACK_MESSAGEMAPPINGOSCPACKETLISTENER_H */ diff --git a/src/osc/osc/OscException.h b/src/osc/osc/OscException.h new file mode 100644 index 00000000000..0e291f23c50 --- /dev/null +++ b/src/osc/osc/OscException.h @@ -0,0 +1,62 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + The text above constitutes the entire oscpack license; however, + the oscpack developer(s) also make the following non-binding requests: + + Any person wishing to distribute modifications to the Software is + requested to send the modifications to the original developer so that + they can be incorporated into the canonical version. It is also + requested that these non-binding requests be included whenever the + above license is reproduced. +*/ +#ifndef INCLUDED_OSCPACK_OSCEXCEPTION_H +#define INCLUDED_OSCPACK_OSCEXCEPTION_H + +#include + +namespace osc{ + +class Exception : public std::exception { + const char *what_; + +public: + Exception() throw() {} + Exception( const Exception& src ) throw() + : std::exception( src ) + , what_( src.what_ ) {} + Exception( const char *w ) throw() + : what_( w ) {} + Exception& operator=( const Exception& src ) throw() + { what_ = src.what_; return *this; } + virtual ~Exception() throw() {} + virtual const char* what() const throw() { return what_; } +}; + +} // namespace osc + +#endif /* INCLUDED_OSCPACK_OSCEXCEPTION_H */ diff --git a/src/osc/osc/OscHostEndianness.h b/src/osc/osc/OscHostEndianness.h new file mode 100644 index 00000000000..32454bc47f1 --- /dev/null +++ b/src/osc/osc/OscHostEndianness.h @@ -0,0 +1,126 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + The text above constitutes the entire oscpack license; however, + the oscpack developer(s) also make the following non-binding requests: + + Any person wishing to distribute modifications to the Software is + requested to send the modifications to the original developer so that + they can be incorporated into the canonical version. It is also + requested that these non-binding requests be included whenever the + above license is reproduced. +*/ +#ifndef INCLUDED_OSCPACK_OSCHOSTENDIANNESS_H +#define INCLUDED_OSCPACK_OSCHOSTENDIANNESS_H + +/* + Make sure either OSC_HOST_LITTLE_ENDIAN or OSC_HOST_BIG_ENDIAN is defined + + We try to use preprocessor symbols to deduce the host endianness. + + Alternatively you can define one of the above symbols from the command line. + Usually you do this with the -D flag to the compiler. e.g.: + + $ g++ -DOSC_HOST_LITTLE_ENDIAN ... +*/ + +#if defined(OSC_HOST_LITTLE_ENDIAN) || defined(OSC_HOST_BIG_ENDIAN) + +// endianness defined on the command line. nothing to do here. + +#elif defined(__WIN32__) || defined(WIN32) || defined(WINCE) + +// assume that __WIN32__ is only defined on little endian systems + +#define OSC_HOST_LITTLE_ENDIAN 1 +#undef OSC_HOST_BIG_ENDIAN + +#elif defined(__APPLE__) + +#if defined(__LITTLE_ENDIAN__) + +#define OSC_HOST_LITTLE_ENDIAN 1 +#undef OSC_HOST_BIG_ENDIAN + +#elif defined(__BIG_ENDIAN__) + +#define OSC_HOST_BIG_ENDIAN 1 +#undef OSC_HOST_LITTLE_ENDIAN + +#endif + +#elif defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && defined(__ORDER_BIG_ENDIAN__) + +// should cover gcc and clang + +#if (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) + +#define OSC_HOST_LITTLE_ENDIAN 1 +#undef OSC_HOST_BIG_ENDIAN + +#elif (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) + +#define OSC_HOST_BIG_ENDIAN 1 +#undef OSC_HOST_LITTLE_ENDIAN + +#endif + +#else + +// gcc defines __LITTLE_ENDIAN__ and __BIG_ENDIAN__ +// for others used here see http://sourceforge.net/p/predef/wiki/Endianness/ +#if (defined(__LITTLE_ENDIAN__) && !defined(__BIG_ENDIAN__)) \ + || (defined(__ARMEL__) && !defined(__ARMEB__)) \ + || (defined(__AARCH64EL__) && !defined(__AARCH64EB__)) \ + || (defined(_MIPSEL) && !defined(_MIPSEB)) \ + || (defined(__MIPSEL) && !defined(__MIPSEB)) \ + || (defined(__MIPSEL__) && !defined(__MIPSEB__)) + +#define OSC_HOST_LITTLE_ENDIAN 1 +#undef OSC_HOST_BIG_ENDIAN + +#elif (defined(__BIG_ENDIAN__) && !defined(__LITTLE_ENDIAN__)) \ + || (defined(__ARMEB__) && !defined(__ARMEL__)) \ + || (defined(__AARCH64EB__) && !defined(__AARCH64EL__)) \ + || (defined(_MIPSEB) && !defined(_MIPSEL)) \ + || (defined(__MIPSEB) && !defined(__MIPSEL)) \ + || (defined(__MIPSEB__) && !defined(__MIPSEL__)) + +#define OSC_HOST_BIG_ENDIAN 1 +#undef OSC_HOST_LITTLE_ENDIAN + +#endif + +#endif + +#if !defined(OSC_HOST_LITTLE_ENDIAN) && !defined(OSC_HOST_BIG_ENDIAN) + +#error please edit OSCHostEndianness.h or define one of {OSC_HOST_LITTLE_ENDIAN, OSC_HOST_BIG_ENDIAN} to configure endianness + +#endif + +#endif /* INCLUDED_OSCPACK_OSCHOSTENDIANNESS_H */ diff --git a/src/osc/osc/OscOutboundPacketStream.cpp b/src/osc/osc/OscOutboundPacketStream.cpp new file mode 100644 index 00000000000..229d590fadd --- /dev/null +++ b/src/osc/osc/OscOutboundPacketStream.cpp @@ -0,0 +1,681 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + The text above constitutes the entire oscpack license; however, + the oscpack developer(s) also make the following non-binding requests: + + Any person wishing to distribute modifications to the Software is + requested to send the modifications to the original developer so that + they can be incorporated into the canonical version. It is also + requested that these non-binding requests be included whenever the + above license is reproduced. +*/ +#include "OscOutboundPacketStream.h" + +#if defined(__WIN32__) || defined(WIN32) || defined(_WIN32) +#include // for alloca +#else +//#include // alloca on Linux (also OSX) +#include // alloca on OSX and FreeBSD (and Linux?) +#endif + +#include +#include // memcpy, memmove, strcpy, strlen +#include // ptrdiff_t + +#include "OscHostEndianness.h" + +#if defined(__BORLANDC__) // workaround for BCB4 release build intrinsics bug +namespace std { +using ::__strcpy__; // avoid error: E2316 '__strcpy__' is not a member of 'std'. +} +#endif + +namespace osc{ + +static void FromInt32( char *p, int32 x ) +{ +#ifdef OSC_HOST_LITTLE_ENDIAN + union{ + osc::int32 i; + char c[4]; + } u; + + u.i = x; + + p[3] = u.c[0]; + p[2] = u.c[1]; + p[1] = u.c[2]; + p[0] = u.c[3]; +#else + *reinterpret_cast(p) = x; +#endif +} + + +static void FromUInt32( char *p, uint32 x ) +{ +#ifdef OSC_HOST_LITTLE_ENDIAN + union{ + osc::uint32 i; + char c[4]; + } u; + + u.i = x; + + p[3] = u.c[0]; + p[2] = u.c[1]; + p[1] = u.c[2]; + p[0] = u.c[3]; +#else + *reinterpret_cast(p) = x; +#endif +} + + +static void FromInt64( char *p, int64 x ) +{ +#ifdef OSC_HOST_LITTLE_ENDIAN + union{ + osc::int64 i; + char c[8]; + } u; + + u.i = x; + + p[7] = u.c[0]; + p[6] = u.c[1]; + p[5] = u.c[2]; + p[4] = u.c[3]; + p[3] = u.c[4]; + p[2] = u.c[5]; + p[1] = u.c[6]; + p[0] = u.c[7]; +#else + *reinterpret_cast(p) = x; +#endif +} + + +static void FromUInt64( char *p, uint64 x ) +{ +#ifdef OSC_HOST_LITTLE_ENDIAN + union{ + osc::uint64 i; + char c[8]; + } u; + + u.i = x; + + p[7] = u.c[0]; + p[6] = u.c[1]; + p[5] = u.c[2]; + p[4] = u.c[3]; + p[3] = u.c[4]; + p[2] = u.c[5]; + p[1] = u.c[6]; + p[0] = u.c[7]; +#else + *reinterpret_cast(p) = x; +#endif +} + + +// round up to the next highest multiple of 4. unless x is already a multiple of 4 +static inline std::size_t RoundUp4( std::size_t x ) +{ + return (x + 3) & ~((std::size_t)0x03); +} + + +OutboundPacketStream::OutboundPacketStream( char *buffer, std::size_t capacity ) + : data_( buffer ) + , end_( data_ + capacity ) + , typeTagsCurrent_( end_ ) + , messageCursor_( data_ ) + , argumentCurrent_( data_ ) + , elementSizePtr_( 0 ) + , messageIsInProgress_( false ) +{ + // sanity check integer types declared in OscTypes.h + // you'll need to fix OscTypes.h if any of these asserts fail + assert( sizeof(osc::int32) == 4 ); + assert( sizeof(osc::uint32) == 4 ); + assert( sizeof(osc::int64) == 8 ); + assert( sizeof(osc::uint64) == 8 ); +} + + +OutboundPacketStream::~OutboundPacketStream() +{ + +} + + +char *OutboundPacketStream::BeginElement( char *beginPtr ) +{ + if( elementSizePtr_ == 0 ){ + + elementSizePtr_ = reinterpret_cast(data_); + + return beginPtr; + + }else{ + // store an offset to the old element size ptr in the element size slot + // we store an offset rather than the actual pointer to be 64 bit clean. + *reinterpret_cast(beginPtr) = + (uint32)(reinterpret_cast(elementSizePtr_) - data_); + + elementSizePtr_ = reinterpret_cast(beginPtr); + + return beginPtr + 4; + } +} + + +void OutboundPacketStream::EndElement( char *endPtr ) +{ + assert( elementSizePtr_ != 0 ); + + if( elementSizePtr_ == reinterpret_cast(data_) ){ + + elementSizePtr_ = 0; + + }else{ + // while building an element, an offset to the containing element's + // size slot is stored in the elements size slot (or a ptr to data_ + // if there is no containing element). We retrieve that here + uint32 *previousElementSizePtr = + reinterpret_cast(data_ + *elementSizePtr_); + + // then we store the element size in the slot. note that the element + // size does not include the size slot, hence the - 4 below. + + std::ptrdiff_t d = endPtr - reinterpret_cast(elementSizePtr_); + // assert( d >= 4 && d <= 0x7FFFFFFF ); // assume packets smaller than 2Gb + + uint32 elementSize = static_cast(d - 4); + FromUInt32( reinterpret_cast(elementSizePtr_), elementSize ); + + // finally, we reset the element size ptr to the containing element + elementSizePtr_ = previousElementSizePtr; + } +} + + +bool OutboundPacketStream::ElementSizeSlotRequired() const +{ + return (elementSizePtr_ != 0); +} + + +void OutboundPacketStream::CheckForAvailableBundleSpace() +{ + std::size_t required = Size() + ((ElementSizeSlotRequired())?4:0) + 16; + + if( required > Capacity() ) + throw OutOfBufferMemoryException(); +} + + +void OutboundPacketStream::CheckForAvailableMessageSpace( const char *addressPattern ) +{ + // plus 4 for at least four bytes of type tag + std::size_t required = Size() + ((ElementSizeSlotRequired())?4:0) + + RoundUp4(std::strlen(addressPattern) + 1) + 4; + + if( required > Capacity() ) + throw OutOfBufferMemoryException(); +} + + +void OutboundPacketStream::CheckForAvailableArgumentSpace( std::size_t argumentLength ) +{ + // plus three for extra type tag, comma and null terminator + std::size_t required = (argumentCurrent_ - data_) + argumentLength + + RoundUp4( (end_ - typeTagsCurrent_) + 3 ); + + if( required > Capacity() ) + throw OutOfBufferMemoryException(); +} + + +void OutboundPacketStream::Clear() +{ + typeTagsCurrent_ = end_; + messageCursor_ = data_; + argumentCurrent_ = data_; + elementSizePtr_ = 0; + messageIsInProgress_ = false; +} + + +std::size_t OutboundPacketStream::Capacity() const +{ + return end_ - data_; +} + + +std::size_t OutboundPacketStream::Size() const +{ + std::size_t result = argumentCurrent_ - data_; + if( IsMessageInProgress() ){ + // account for the length of the type tag string. the total type tag + // includes an initial comma, plus at least one terminating \0 + result += RoundUp4( (end_ - typeTagsCurrent_) + 2 ); + } + + return result; +} + + +const char *OutboundPacketStream::Data() const +{ + return data_; +} + + +bool OutboundPacketStream::IsReady() const +{ + return (!IsMessageInProgress() && !IsBundleInProgress()); +} + + +bool OutboundPacketStream::IsMessageInProgress() const +{ + return messageIsInProgress_; +} + + +bool OutboundPacketStream::IsBundleInProgress() const +{ + return (elementSizePtr_ != 0); +} + + +OutboundPacketStream& OutboundPacketStream::operator<<( const BundleInitiator& rhs ) +{ + if( IsMessageInProgress() ) + throw MessageInProgressException(); + + CheckForAvailableBundleSpace(); + + messageCursor_ = BeginElement( messageCursor_ ); + + std::memcpy( messageCursor_, "#bundle\0", 8 ); + FromUInt64( messageCursor_ + 8, rhs.timeTag ); + + messageCursor_ += 16; + argumentCurrent_ = messageCursor_; + + return *this; +} + + +OutboundPacketStream& OutboundPacketStream::operator<<( const BundleTerminator& rhs ) +{ + (void) rhs; + + if( !IsBundleInProgress() ) + throw BundleNotInProgressException(); + if( IsMessageInProgress() ) + throw MessageInProgressException(); + + EndElement( messageCursor_ ); + + return *this; +} + + +OutboundPacketStream& OutboundPacketStream::operator<<( const BeginMessage& rhs ) +{ + if( IsMessageInProgress() ) + throw MessageInProgressException(); + + CheckForAvailableMessageSpace( rhs.addressPattern ); + + messageCursor_ = BeginElement( messageCursor_ ); + + std::strcpy( messageCursor_, rhs.addressPattern ); + std::size_t rhsLength = std::strlen(rhs.addressPattern); + messageCursor_ += rhsLength + 1; + + // zero pad to 4-byte boundary + std::size_t i = rhsLength + 1; + while( i & 0x3 ){ + *messageCursor_++ = '\0'; + ++i; + } + + argumentCurrent_ = messageCursor_; + typeTagsCurrent_ = end_; + + messageIsInProgress_ = true; + + return *this; +} + + +OutboundPacketStream& OutboundPacketStream::operator<<( const MessageTerminator& rhs ) +{ + (void) rhs; + + if( !IsMessageInProgress() ) + throw MessageNotInProgressException(); + + std::size_t typeTagsCount = end_ - typeTagsCurrent_; + + if( typeTagsCount ){ + + char *tempTypeTags = (char*)alloca(typeTagsCount); + std::memcpy( tempTypeTags, typeTagsCurrent_, typeTagsCount ); + + // slot size includes comma and null terminator + std::size_t typeTagSlotSize = RoundUp4( typeTagsCount + 2 ); + + std::size_t argumentsSize = argumentCurrent_ - messageCursor_; + + std::memmove( messageCursor_ + typeTagSlotSize, messageCursor_, argumentsSize ); + + messageCursor_[0] = ','; + // copy type tags in reverse (really forward) order + for( std::size_t i=0; i < typeTagsCount; ++i ) + messageCursor_[i+1] = tempTypeTags[ (typeTagsCount-1) - i ]; + + char *p = messageCursor_ + 1 + typeTagsCount; + for( std::size_t i=0; i < (typeTagSlotSize - (typeTagsCount + 1)); ++i ) + *p++ = '\0'; + + typeTagsCurrent_ = end_; + + // advance messageCursor_ for next message + messageCursor_ += typeTagSlotSize + argumentsSize; + + }else{ + // send an empty type tags string + std::memcpy( messageCursor_, ",\0\0\0", 4 ); + + // advance messageCursor_ for next message + messageCursor_ += 4; + } + + argumentCurrent_ = messageCursor_; + + EndElement( messageCursor_ ); + + messageIsInProgress_ = false; + + return *this; +} + + +OutboundPacketStream& OutboundPacketStream::operator<<( bool rhs ) +{ + CheckForAvailableArgumentSpace(0); + + *(--typeTagsCurrent_) = (char)((rhs) ? TRUE_TYPE_TAG : FALSE_TYPE_TAG); + + return *this; +} + + +OutboundPacketStream& OutboundPacketStream::operator<<( const NilType& rhs ) +{ + (void) rhs; + CheckForAvailableArgumentSpace(0); + + *(--typeTagsCurrent_) = NIL_TYPE_TAG; + + return *this; +} + + +OutboundPacketStream& OutboundPacketStream::operator<<( const InfinitumType& rhs ) +{ + (void) rhs; + CheckForAvailableArgumentSpace(0); + + *(--typeTagsCurrent_) = INFINITUM_TYPE_TAG; + + return *this; +} + + +OutboundPacketStream& OutboundPacketStream::operator<<( int32 rhs ) +{ + CheckForAvailableArgumentSpace(4); + + *(--typeTagsCurrent_) = INT32_TYPE_TAG; + FromInt32( argumentCurrent_, rhs ); + argumentCurrent_ += 4; + + return *this; +} + + +OutboundPacketStream& OutboundPacketStream::operator<<( float rhs ) +{ + CheckForAvailableArgumentSpace(4); + + *(--typeTagsCurrent_) = FLOAT_TYPE_TAG; + +#ifdef OSC_HOST_LITTLE_ENDIAN + union{ + float f; + char c[4]; + } u; + + u.f = rhs; + + argumentCurrent_[3] = u.c[0]; + argumentCurrent_[2] = u.c[1]; + argumentCurrent_[1] = u.c[2]; + argumentCurrent_[0] = u.c[3]; +#else + *reinterpret_cast(argumentCurrent_) = rhs; +#endif + + argumentCurrent_ += 4; + + return *this; +} + + +OutboundPacketStream& OutboundPacketStream::operator<<( char rhs ) +{ + CheckForAvailableArgumentSpace(4); + + *(--typeTagsCurrent_) = CHAR_TYPE_TAG; + FromInt32( argumentCurrent_, rhs ); + argumentCurrent_ += 4; + + return *this; +} + + +OutboundPacketStream& OutboundPacketStream::operator<<( const RgbaColor& rhs ) +{ + CheckForAvailableArgumentSpace(4); + + *(--typeTagsCurrent_) = RGBA_COLOR_TYPE_TAG; + FromUInt32( argumentCurrent_, rhs ); + argumentCurrent_ += 4; + + return *this; +} + + +OutboundPacketStream& OutboundPacketStream::operator<<( const MidiMessage& rhs ) +{ + CheckForAvailableArgumentSpace(4); + + *(--typeTagsCurrent_) = MIDI_MESSAGE_TYPE_TAG; + FromUInt32( argumentCurrent_, rhs ); + argumentCurrent_ += 4; + + return *this; +} + + +OutboundPacketStream& OutboundPacketStream::operator<<( int64 rhs ) +{ + CheckForAvailableArgumentSpace(8); + + *(--typeTagsCurrent_) = INT64_TYPE_TAG; + FromInt64( argumentCurrent_, rhs ); + argumentCurrent_ += 8; + + return *this; +} + + +OutboundPacketStream& OutboundPacketStream::operator<<( const TimeTag& rhs ) +{ + CheckForAvailableArgumentSpace(8); + + *(--typeTagsCurrent_) = TIME_TAG_TYPE_TAG; + FromUInt64( argumentCurrent_, rhs ); + argumentCurrent_ += 8; + + return *this; +} + + +OutboundPacketStream& OutboundPacketStream::operator<<( double rhs ) +{ + CheckForAvailableArgumentSpace(8); + + *(--typeTagsCurrent_) = DOUBLE_TYPE_TAG; + +#ifdef OSC_HOST_LITTLE_ENDIAN + union{ + double f; + char c[8]; + } u; + + u.f = rhs; + + argumentCurrent_[7] = u.c[0]; + argumentCurrent_[6] = u.c[1]; + argumentCurrent_[5] = u.c[2]; + argumentCurrent_[4] = u.c[3]; + argumentCurrent_[3] = u.c[4]; + argumentCurrent_[2] = u.c[5]; + argumentCurrent_[1] = u.c[6]; + argumentCurrent_[0] = u.c[7]; +#else + *reinterpret_cast(argumentCurrent_) = rhs; +#endif + + argumentCurrent_ += 8; + + return *this; +} + + +OutboundPacketStream& OutboundPacketStream::operator<<( const char *rhs ) +{ + CheckForAvailableArgumentSpace( RoundUp4(std::strlen(rhs) + 1) ); + + *(--typeTagsCurrent_) = STRING_TYPE_TAG; + std::strcpy( argumentCurrent_, rhs ); + std::size_t rhsLength = std::strlen(rhs); + argumentCurrent_ += rhsLength + 1; + + // zero pad to 4-byte boundary + std::size_t i = rhsLength + 1; + while( i & 0x3 ){ + *argumentCurrent_++ = '\0'; + ++i; + } + + return *this; +} + + +OutboundPacketStream& OutboundPacketStream::operator<<( const Symbol& rhs ) +{ + CheckForAvailableArgumentSpace( RoundUp4(std::strlen(rhs) + 1) ); + + *(--typeTagsCurrent_) = SYMBOL_TYPE_TAG; + std::strcpy( argumentCurrent_, rhs ); + std::size_t rhsLength = std::strlen(rhs); + argumentCurrent_ += rhsLength + 1; + + // zero pad to 4-byte boundary + std::size_t i = rhsLength + 1; + while( i & 0x3 ){ + *argumentCurrent_++ = '\0'; + ++i; + } + + return *this; +} + + +OutboundPacketStream& OutboundPacketStream::operator<<( const Blob& rhs ) +{ + CheckForAvailableArgumentSpace( 4 + RoundUp4(rhs.size) ); + + *(--typeTagsCurrent_) = BLOB_TYPE_TAG; + FromUInt32( argumentCurrent_, rhs.size ); + argumentCurrent_ += 4; + + std::memcpy( argumentCurrent_, rhs.data, rhs.size ); + argumentCurrent_ += rhs.size; + + // zero pad to 4-byte boundary + unsigned long i = rhs.size; + while( i & 0x3 ){ + *argumentCurrent_++ = '\0'; + ++i; + } + + return *this; +} + +OutboundPacketStream& OutboundPacketStream::operator<<( const ArrayInitiator& rhs ) +{ + (void) rhs; + CheckForAvailableArgumentSpace(0); + + *(--typeTagsCurrent_) = ARRAY_BEGIN_TYPE_TAG; + + return *this; +} + +OutboundPacketStream& OutboundPacketStream::operator<<( const ArrayTerminator& rhs ) +{ + (void) rhs; + CheckForAvailableArgumentSpace(0); + + *(--typeTagsCurrent_) = ARRAY_END_TYPE_TAG; + + return *this; +} + +} // namespace osc diff --git a/src/osc/osc/OscOutboundPacketStream.h b/src/osc/osc/OscOutboundPacketStream.h new file mode 100644 index 00000000000..14fced987a6 --- /dev/null +++ b/src/osc/osc/OscOutboundPacketStream.h @@ -0,0 +1,154 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + The text above constitutes the entire oscpack license; however, + the oscpack developer(s) also make the following non-binding requests: + + Any person wishing to distribute modifications to the Software is + requested to send the modifications to the original developer so that + they can be incorporated into the canonical version. It is also + requested that these non-binding requests be included whenever the + above license is reproduced. +*/ +#ifndef INCLUDED_OSCPACK_OSCOUTBOUNDPACKETSTREAM_H +#define INCLUDED_OSCPACK_OSCOUTBOUNDPACKETSTREAM_H + +#include // size_t + +#include "OscTypes.h" +#include "OscException.h" + + +namespace osc{ + +class OutOfBufferMemoryException : public Exception{ +public: + OutOfBufferMemoryException( const char *w="out of buffer memory" ) + : Exception( w ) {} +}; + +class BundleNotInProgressException : public Exception{ +public: + BundleNotInProgressException( + const char *w="call to EndBundle when bundle is not in progress" ) + : Exception( w ) {} +}; + +class MessageInProgressException : public Exception{ +public: + MessageInProgressException( + const char *w="opening or closing bundle or message while message is in progress" ) + : Exception( w ) {} +}; + +class MessageNotInProgressException : public Exception{ +public: + MessageNotInProgressException( + const char *w="call to EndMessage when message is not in progress" ) + : Exception( w ) {} +}; + + +class OutboundPacketStream{ +public: + OutboundPacketStream( char *buffer, std::size_t capacity ); + ~OutboundPacketStream(); + + void Clear(); + + std::size_t Capacity() const; + + // invariant: size() is valid even while building a message. + std::size_t Size() const; + + const char *Data() const; + + // indicates that all messages have been closed with a matching EndMessage + // and all bundles have been closed with a matching EndBundle + bool IsReady() const; + + bool IsMessageInProgress() const; + bool IsBundleInProgress() const; + + OutboundPacketStream& operator<<( const BundleInitiator& rhs ); + OutboundPacketStream& operator<<( const BundleTerminator& rhs ); + + OutboundPacketStream& operator<<( const BeginMessage& rhs ); + OutboundPacketStream& operator<<( const MessageTerminator& rhs ); + + OutboundPacketStream& operator<<( bool rhs ); + OutboundPacketStream& operator<<( const NilType& rhs ); + OutboundPacketStream& operator<<( const InfinitumType& rhs ); + OutboundPacketStream& operator<<( int32 rhs ); + +#if !(defined(__x86_64__) || defined(_M_X64)) + OutboundPacketStream& operator<<( int rhs ) + { *this << (int32)rhs; return *this; } +#endif + + OutboundPacketStream& operator<<( float rhs ); + OutboundPacketStream& operator<<( char rhs ); + OutboundPacketStream& operator<<( const RgbaColor& rhs ); + OutboundPacketStream& operator<<( const MidiMessage& rhs ); + OutboundPacketStream& operator<<( int64 rhs ); + OutboundPacketStream& operator<<( const TimeTag& rhs ); + OutboundPacketStream& operator<<( double rhs ); + OutboundPacketStream& operator<<( const char* rhs ); + OutboundPacketStream& operator<<( const Symbol& rhs ); + OutboundPacketStream& operator<<( const Blob& rhs ); + + OutboundPacketStream& operator<<( const ArrayInitiator& rhs ); + OutboundPacketStream& operator<<( const ArrayTerminator& rhs ); + +private: + + char *BeginElement( char *beginPtr ); + void EndElement( char *endPtr ); + + bool ElementSizeSlotRequired() const; + void CheckForAvailableBundleSpace(); + void CheckForAvailableMessageSpace( const char *addressPattern ); + void CheckForAvailableArgumentSpace( std::size_t argumentLength ); + + char *data_; + char *end_; + + char *typeTagsCurrent_; // stored in reverse order + char *messageCursor_; + char *argumentCurrent_; + + // elementSizePtr_ has two special values: 0 indicates that a bundle + // isn't open, and elementSizePtr_==data_ indicates that a bundle is + // open but that it doesn't have a size slot (ie the outermost bundle) + uint32 *elementSizePtr_; + + bool messageIsInProgress_; +}; + +} // namespace osc + +#endif /* INCLUDED_OSCPACK_OSCOUTBOUNDPACKETSTREAM_H */ diff --git a/src/osc/osc/OscPacketListener.h b/src/osc/osc/OscPacketListener.h new file mode 100644 index 00000000000..f0385111e98 --- /dev/null +++ b/src/osc/osc/OscPacketListener.h @@ -0,0 +1,81 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + The text above constitutes the entire oscpack license; however, + the oscpack developer(s) also make the following non-binding requests: + + Any person wishing to distribute modifications to the Software is + requested to send the modifications to the original developer so that + they can be incorporated into the canonical version. It is also + requested that these non-binding requests be included whenever the + above license is reproduced. +*/ + +#ifndef INCLUDED_OSCPACK_OSCPACKETLISTENER_H +#define INCLUDED_OSCPACK_OSCPACKETLISTENER_H + +#include "OscReceivedElements.h" +#include "../ip/PacketListener.h" + + + +namespace osc{ + +class OscPacketListener : public PacketListener { + protected: + virtual void ProcessBundle( const osc::ReceivedBundle& b, + const IpEndpointName& remoteEndpoint ) + { + // ignore bundle time tag for now + + for( ReceivedBundle::const_iterator i = b.ElementsBegin(); + i != b.ElementsEnd(); ++i ){ + if( i->IsBundle() ) + ProcessBundle( ReceivedBundle(*i), remoteEndpoint ); + else + ProcessMessage( ReceivedMessage(*i), remoteEndpoint ); + } + } + + virtual void ProcessMessage( const osc::ReceivedMessage& m, + const IpEndpointName& remoteEndpoint ) = 0; + +public: + virtual void ProcessPacket( const char *data, int size, + const IpEndpointName& remoteEndpoint ) + { + osc::ReceivedPacket p( data, size ); + if( p.IsBundle() ) + ProcessBundle( ReceivedBundle(p), remoteEndpoint ); + else + ProcessMessage( ReceivedMessage(p), remoteEndpoint ); + } +}; + +} // namespace osc + +#endif /* INCLUDED_OSCPACK_OSCPACKETLISTENER_H */ diff --git a/src/osc/osc/OscPrintReceivedElements.cpp b/src/osc/osc/OscPrintReceivedElements.cpp new file mode 100644 index 00000000000..747e86c254a --- /dev/null +++ b/src/osc/osc/OscPrintReceivedElements.cpp @@ -0,0 +1,261 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + The text above constitutes the entire oscpack license; however, + the oscpack developer(s) also make the following non-binding requests: + + Any person wishing to distribute modifications to the Software is + requested to send the modifications to the original developer so that + they can be incorporated into the canonical version. It is also + requested that these non-binding requests be included whenever the + above license is reproduced. +*/ +#include "OscPrintReceivedElements.h" + +#include +#include +#include +#include + +#if defined(__BORLANDC__) // workaround for BCB4 release build intrinsics bug +namespace std { +using ::__strcpy__; // avoid error: E2316 '__strcpy__' is not a member of 'std'. +} +#endif + +namespace osc{ + + +std::ostream& operator<<( std::ostream & os, + const ReceivedMessageArgument& arg ) +{ + switch( arg.TypeTag() ){ + case TRUE_TYPE_TAG: + os << "bool:true"; + break; + + case FALSE_TYPE_TAG: + os << "bool:false"; + break; + + case NIL_TYPE_TAG: + os << "(Nil)"; + break; + + case INFINITUM_TYPE_TAG: + os << "(Infinitum)"; + break; + + case INT32_TYPE_TAG: + os << "int32:" << arg.AsInt32Unchecked(); + break; + + case FLOAT_TYPE_TAG: + os << "float32:" << arg.AsFloatUnchecked(); + break; + + case CHAR_TYPE_TAG: + { + char s[2] = {0}; + s[0] = arg.AsCharUnchecked(); + os << "char:'" << s << "'"; + } + break; + + case RGBA_COLOR_TYPE_TAG: + { + uint32 color = arg.AsRgbaColorUnchecked(); + + os << "RGBA:0x" + << std::hex << std::setfill('0') + << std::setw(2) << (int)((color>>24) & 0xFF) + << std::setw(2) << (int)((color>>16) & 0xFF) + << std::setw(2) << (int)((color>>8) & 0xFF) + << std::setw(2) << (int)(color & 0xFF) + << std::setfill(' '); + os.unsetf(std::ios::basefield); + } + break; + + case MIDI_MESSAGE_TYPE_TAG: + { + uint32 m = arg.AsMidiMessageUnchecked(); + os << "midi (port, status, data1, data2):<<" + << std::hex << std::setfill('0') + << "0x" << std::setw(2) << (int)((m>>24) & 0xFF) + << " 0x" << std::setw(2) << (int)((m>>16) & 0xFF) + << " 0x" << std::setw(2) << (int)((m>>8) & 0xFF) + << " 0x" << std::setw(2) << (int)(m & 0xFF) + << std::setfill(' ') << ">>"; + os.unsetf(std::ios::basefield); + } + break; + + case INT64_TYPE_TAG: + os << "int64:" << arg.AsInt64Unchecked(); + break; + + case TIME_TAG_TYPE_TAG: + { + os << "OSC-timetag:" << arg.AsTimeTagUnchecked() << " "; + + std::time_t t = + (unsigned long)( arg.AsTimeTagUnchecked() >> 32 ); + + const char *timeString = std::ctime( &t ); + size_t len = std::strlen( timeString ); + + // -1 to omit trailing newline from string returned by ctime() + if( len > 1 ) + os.write( timeString, len - 1 ); + } + break; + + case DOUBLE_TYPE_TAG: + os << "double:" << arg.AsDoubleUnchecked(); + break; + + case STRING_TYPE_TAG: + os << "OSC-string:`" << arg.AsStringUnchecked() << "'"; + break; + + case SYMBOL_TYPE_TAG: + os << "OSC-string (symbol):`" << arg.AsSymbolUnchecked() << "'"; + break; + + case BLOB_TYPE_TAG: + { + const void *data; + osc_bundle_element_size_t size; + arg.AsBlobUnchecked( data, size ); + os << "OSC-blob:<<" << std::hex << std::setfill('0'); + unsigned char *p = (unsigned char*)data; + for( osc_bundle_element_size_t i = 0; i < size; ++i ){ + os << "0x" << std::setw(2) << int(p[i]); + if( i != size-1 ) + os << ' '; + } + os.unsetf(std::ios::basefield); + os << ">>" << std::setfill(' '); + } + break; + + case ARRAY_BEGIN_TYPE_TAG: + os << "["; + break; + + case ARRAY_END_TYPE_TAG: + os << "]"; + break; + + default: + os << "unknown"; + } + + return os; +} + + +std::ostream& operator<<( std::ostream & os, const ReceivedMessage& m ) +{ + os << "["; + if( m.AddressPatternIsUInt32() ) + os << m.AddressPatternAsUInt32(); + else + os << m.AddressPattern(); + + bool first = true; + for( ReceivedMessage::const_iterator i = m.ArgumentsBegin(); + i != m.ArgumentsEnd(); ++i ){ + if( first ){ + os << " "; + first = false; + }else{ + os << ", "; + } + + os << *i; + } + + os << "]"; + + return os; +} + + +std::ostream& operator<<( std::ostream & os, const ReceivedBundle& b ) +{ + static int indent = 0; + + for( int j=0; j < indent; ++j ) + os << " "; + os << "{ ( "; + if( b.TimeTag() == 1 ) + os << "immediate"; + else + os << b.TimeTag(); + os << " )\n"; + + ++indent; + + for( ReceivedBundle::const_iterator i = b.ElementsBegin(); + i != b.ElementsEnd(); ++i ){ + if( i->IsBundle() ){ + ReceivedBundle b(*i); + os << b << "\n"; + }else{ + ReceivedMessage m(*i); + for( int j=0; j < indent; ++j ) + os << " "; + os << m << "\n"; + } + } + + --indent; + + for( int j=0; j < indent; ++j ) + os << " "; + os << "}"; + + return os; +} + + +std::ostream& operator<<( std::ostream & os, const ReceivedPacket& p ) +{ + if( p.IsBundle() ){ + ReceivedBundle b(p); + os << b << "\n"; + }else{ + ReceivedMessage m(p); + os << m << "\n"; + } + + return os; +} + +} // namespace osc diff --git a/src/osc/osc/OscPrintReceivedElements.h b/src/osc/osc/OscPrintReceivedElements.h new file mode 100644 index 00000000000..bd975a24c23 --- /dev/null +++ b/src/osc/osc/OscPrintReceivedElements.h @@ -0,0 +1,54 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + The text above constitutes the entire oscpack license; however, + the oscpack developer(s) also make the following non-binding requests: + + Any person wishing to distribute modifications to the Software is + requested to send the modifications to the original developer so that + they can be incorporated into the canonical version. It is also + requested that these non-binding requests be included whenever the + above license is reproduced. +*/ +#ifndef INCLUDED_OSCPACK_OSCPRINTRECEIVEDELEMENTS_H +#define INCLUDED_OSCPACK_OSCPRINTRECEIVEDELEMENTS_H + +#include + +#include "OscReceivedElements.h" + + +namespace osc{ + +std::ostream& operator<<( std::ostream & os, const ReceivedPacket& p ); +std::ostream& operator<<( std::ostream & os, const ReceivedMessageArgument& arg ); +std::ostream& operator<<( std::ostream & os, const ReceivedMessage& m ); +std::ostream& operator<<( std::ostream & os, const ReceivedBundle& b ); + +} // namespace osc + +#endif /* INCLUDED_OSCPACK_OSCPRINTRECEIVEDELEMENTS_H */ diff --git a/src/osc/osc/OscReceivedElements.cpp b/src/osc/osc/OscReceivedElements.cpp new file mode 100644 index 00000000000..3d037aefada --- /dev/null +++ b/src/osc/osc/OscReceivedElements.cpp @@ -0,0 +1,795 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + The text above constitutes the entire oscpack license; however, + the oscpack developer(s) also make the following non-binding requests: + + Any person wishing to distribute modifications to the Software is + requested to send the modifications to the original developer so that + they can be incorporated into the canonical version. It is also + requested that these non-binding requests be included whenever the + above license is reproduced. +*/ +#include "OscReceivedElements.h" + +#include "OscHostEndianness.h" + +#include // ptrdiff_t + +namespace osc{ + + +// return the first 4 byte boundary after the end of a str4 +// be careful about calling this version if you don't know whether +// the string is terminated correctly. +static inline const char* FindStr4End( const char *p ) +{ + if( p[0] == '\0' ) // special case for SuperCollider integer address pattern + return p + 4; + + p += 3; + + while( *p ) + p += 4; + + return p + 1; +} + + +// return the first 4 byte boundary after the end of a str4 +// returns 0 if p == end or if the string is unterminated +static inline const char* FindStr4End( const char *p, const char *end ) +{ + if( p >= end ) + return 0; + + if( p[0] == '\0' ) // special case for SuperCollider integer address pattern + return p + 4; + + p += 3; + end -= 1; + + while( p < end && *p ) + p += 4; + + if( *p ) + return 0; + else + return p + 1; +} + + +// round up to the next highest multiple of 4. unless x is already a multiple of 4 +static inline uint32 RoundUp4( uint32 x ) +{ + return (x + 3) & ~((uint32)0x03); +} + + +static inline int32 ToInt32( const char *p ) +{ +#ifdef OSC_HOST_LITTLE_ENDIAN + union{ + osc::int32 i; + char c[4]; + } u; + + u.c[0] = p[3]; + u.c[1] = p[2]; + u.c[2] = p[1]; + u.c[3] = p[0]; + + return u.i; +#else + return *(int32*)p; +#endif +} + + +static inline uint32 ToUInt32( const char *p ) +{ +#ifdef OSC_HOST_LITTLE_ENDIAN + union{ + osc::uint32 i; + char c[4]; + } u; + + u.c[0] = p[3]; + u.c[1] = p[2]; + u.c[2] = p[1]; + u.c[3] = p[0]; + + return u.i; +#else + return *(uint32*)p; +#endif +} + + +static inline int64 ToInt64( const char *p ) +{ +#ifdef OSC_HOST_LITTLE_ENDIAN + union{ + osc::int64 i; + char c[8]; + } u; + + u.c[0] = p[7]; + u.c[1] = p[6]; + u.c[2] = p[5]; + u.c[3] = p[4]; + u.c[4] = p[3]; + u.c[5] = p[2]; + u.c[6] = p[1]; + u.c[7] = p[0]; + + return u.i; +#else + return *(int64*)p; +#endif +} + + +static inline uint64 ToUInt64( const char *p ) +{ +#ifdef OSC_HOST_LITTLE_ENDIAN + union{ + osc::uint64 i; + char c[8]; + } u; + + u.c[0] = p[7]; + u.c[1] = p[6]; + u.c[2] = p[5]; + u.c[3] = p[4]; + u.c[4] = p[3]; + u.c[5] = p[2]; + u.c[6] = p[1]; + u.c[7] = p[0]; + + return u.i; +#else + return *(uint64*)p; +#endif +} + +//------------------------------------------------------------------------------ + +bool ReceivedPacket::IsBundle() const +{ + return (Size() > 0 && Contents()[0] == '#'); +} + +//------------------------------------------------------------------------------ + +bool ReceivedBundleElement::IsBundle() const +{ + return (Size() > 0 && Contents()[0] == '#'); +} + + +osc_bundle_element_size_t ReceivedBundleElement::Size() const +{ + return ToInt32( sizePtr_ ); +} + +//------------------------------------------------------------------------------ + +bool ReceivedMessageArgument::AsBool() const +{ + if( !typeTagPtr_ ) + throw MissingArgumentException(); + else if( *typeTagPtr_ == TRUE_TYPE_TAG ) + return true; + else if( *typeTagPtr_ == FALSE_TYPE_TAG ) + return false; + else + throw WrongArgumentTypeException(); +} + + +bool ReceivedMessageArgument::AsBoolUnchecked() const +{ + if( !typeTagPtr_ ) + throw MissingArgumentException(); + else if( *typeTagPtr_ == TRUE_TYPE_TAG ) + return true; + else + return false; +} + + +int32 ReceivedMessageArgument::AsInt32() const +{ + if( !typeTagPtr_ ) + throw MissingArgumentException(); + else if( *typeTagPtr_ == INT32_TYPE_TAG ) + return AsInt32Unchecked(); + else + throw WrongArgumentTypeException(); +} + + +int32 ReceivedMessageArgument::AsInt32Unchecked() const +{ +#ifdef OSC_HOST_LITTLE_ENDIAN + union{ + osc::int32 i; + char c[4]; + } u; + + u.c[0] = argumentPtr_[3]; + u.c[1] = argumentPtr_[2]; + u.c[2] = argumentPtr_[1]; + u.c[3] = argumentPtr_[0]; + + return u.i; +#else + return *(int32*)argument_; +#endif +} + + +float ReceivedMessageArgument::AsFloat() const +{ + if( !typeTagPtr_ ) + throw MissingArgumentException(); + else if( *typeTagPtr_ == FLOAT_TYPE_TAG ) + return AsFloatUnchecked(); + else + throw WrongArgumentTypeException(); +} + + +float ReceivedMessageArgument::AsFloatUnchecked() const +{ +#ifdef OSC_HOST_LITTLE_ENDIAN + union{ + float f; + char c[4]; + } u; + + u.c[0] = argumentPtr_[3]; + u.c[1] = argumentPtr_[2]; + u.c[2] = argumentPtr_[1]; + u.c[3] = argumentPtr_[0]; + + return u.f; +#else + return *(float*)argument_; +#endif +} + + +char ReceivedMessageArgument::AsChar() const +{ + if( !typeTagPtr_ ) + throw MissingArgumentException(); + else if( *typeTagPtr_ == CHAR_TYPE_TAG ) + return AsCharUnchecked(); + else + throw WrongArgumentTypeException(); +} + + +char ReceivedMessageArgument::AsCharUnchecked() const +{ + return (char)ToInt32( argumentPtr_ ); +} + + +uint32 ReceivedMessageArgument::AsRgbaColor() const +{ + if( !typeTagPtr_ ) + throw MissingArgumentException(); + else if( *typeTagPtr_ == RGBA_COLOR_TYPE_TAG ) + return AsRgbaColorUnchecked(); + else + throw WrongArgumentTypeException(); +} + + +uint32 ReceivedMessageArgument::AsRgbaColorUnchecked() const +{ + return ToUInt32( argumentPtr_ ); +} + + +uint32 ReceivedMessageArgument::AsMidiMessage() const +{ + if( !typeTagPtr_ ) + throw MissingArgumentException(); + else if( *typeTagPtr_ == MIDI_MESSAGE_TYPE_TAG ) + return AsMidiMessageUnchecked(); + else + throw WrongArgumentTypeException(); +} + + +uint32 ReceivedMessageArgument::AsMidiMessageUnchecked() const +{ + return ToUInt32( argumentPtr_ ); +} + + +int64 ReceivedMessageArgument::AsInt64() const +{ + if( !typeTagPtr_ ) + throw MissingArgumentException(); + else if( *typeTagPtr_ == INT64_TYPE_TAG ) + return AsInt64Unchecked(); + else + throw WrongArgumentTypeException(); +} + + +int64 ReceivedMessageArgument::AsInt64Unchecked() const +{ + return ToInt64( argumentPtr_ ); +} + + +uint64 ReceivedMessageArgument::AsTimeTag() const +{ + if( !typeTagPtr_ ) + throw MissingArgumentException(); + else if( *typeTagPtr_ == TIME_TAG_TYPE_TAG ) + return AsTimeTagUnchecked(); + else + throw WrongArgumentTypeException(); +} + + +uint64 ReceivedMessageArgument::AsTimeTagUnchecked() const +{ + return ToUInt64( argumentPtr_ ); +} + + +double ReceivedMessageArgument::AsDouble() const +{ + if( !typeTagPtr_ ) + throw MissingArgumentException(); + else if( *typeTagPtr_ == DOUBLE_TYPE_TAG ) + return AsDoubleUnchecked(); + else + throw WrongArgumentTypeException(); +} + + +double ReceivedMessageArgument::AsDoubleUnchecked() const +{ +#ifdef OSC_HOST_LITTLE_ENDIAN + union{ + double d; + char c[8]; + } u; + + u.c[0] = argumentPtr_[7]; + u.c[1] = argumentPtr_[6]; + u.c[2] = argumentPtr_[5]; + u.c[3] = argumentPtr_[4]; + u.c[4] = argumentPtr_[3]; + u.c[5] = argumentPtr_[2]; + u.c[6] = argumentPtr_[1]; + u.c[7] = argumentPtr_[0]; + + return u.d; +#else + return *(double*)argument_; +#endif +} + + +const char* ReceivedMessageArgument::AsString() const +{ + if( !typeTagPtr_ ) + throw MissingArgumentException(); + else if( *typeTagPtr_ == STRING_TYPE_TAG ) + return argumentPtr_; + else + throw WrongArgumentTypeException(); +} + + +const char* ReceivedMessageArgument::AsSymbol() const +{ + if( !typeTagPtr_ ) + throw MissingArgumentException(); + else if( *typeTagPtr_ == SYMBOL_TYPE_TAG ) + return argumentPtr_; + else + throw WrongArgumentTypeException(); +} + + +void ReceivedMessageArgument::AsBlob( const void*& data, osc_bundle_element_size_t& size ) const +{ + if( !typeTagPtr_ ) + throw MissingArgumentException(); + else if( *typeTagPtr_ == BLOB_TYPE_TAG ) + AsBlobUnchecked( data, size ); + else + throw WrongArgumentTypeException(); +} + + +void ReceivedMessageArgument::AsBlobUnchecked( const void*& data, osc_bundle_element_size_t& size ) const +{ + // read blob size as an unsigned int then validate + osc_bundle_element_size_t sizeResult = (osc_bundle_element_size_t)ToUInt32( argumentPtr_ ); + if( !IsValidElementSizeValue(sizeResult) ) + throw MalformedMessageException("invalid blob size"); + + size = sizeResult; + data = (void*)(argumentPtr_+ osc::OSC_SIZEOF_INT32); +} + +std::size_t ReceivedMessageArgument::ComputeArrayItemCount() const +{ + // it is only valid to call ComputeArrayItemCount when the argument is the array start marker + if( !IsArrayBegin() ) + throw WrongArgumentTypeException(); + + std::size_t result = 0; + unsigned int level = 0; + const char *typeTag = typeTagPtr_ + 1; + + // iterate through all type tags. note that ReceivedMessage::Init + // has already checked that the message is well formed. + while( *typeTag ) { + switch( *typeTag++ ) { + case ARRAY_BEGIN_TYPE_TAG: + level += 1; + break; + + case ARRAY_END_TYPE_TAG: + if(level == 0) + return result; + level -= 1; + break; + + default: + if( level == 0 ) // only count items at level 0 + ++result; + } + } + + return result; +} + +//------------------------------------------------------------------------------ + +void ReceivedMessageArgumentIterator::Advance() +{ + if( !value_.typeTagPtr_ ) + return; + + switch( *value_.typeTagPtr_++ ){ + case '\0': + // don't advance past end + --value_.typeTagPtr_; + break; + + case TRUE_TYPE_TAG: + case FALSE_TYPE_TAG: + case NIL_TYPE_TAG: + case INFINITUM_TYPE_TAG: + + // zero length + break; + + case INT32_TYPE_TAG: + case FLOAT_TYPE_TAG: + case CHAR_TYPE_TAG: + case RGBA_COLOR_TYPE_TAG: + case MIDI_MESSAGE_TYPE_TAG: + + value_.argumentPtr_ += 4; + break; + + case INT64_TYPE_TAG: + case TIME_TAG_TYPE_TAG: + case DOUBLE_TYPE_TAG: + + value_.argumentPtr_ += 8; + break; + + case STRING_TYPE_TAG: + case SYMBOL_TYPE_TAG: + + // we use the unsafe function FindStr4End(char*) here because all of + // the arguments have already been validated in + // ReceivedMessage::Init() below. + + value_.argumentPtr_ = FindStr4End( value_.argumentPtr_ ); + break; + + case BLOB_TYPE_TAG: + { + // treat blob size as an unsigned int for the purposes of this calculation + uint32 blobSize = ToUInt32( value_.argumentPtr_ ); + value_.argumentPtr_ = value_.argumentPtr_ + osc::OSC_SIZEOF_INT32 + RoundUp4( blobSize ); + } + break; + + case ARRAY_BEGIN_TYPE_TAG: + case ARRAY_END_TYPE_TAG: + + // [ Indicates the beginning of an array. The tags following are for + // data in the Array until a close brace tag is reached. + // ] Indicates the end of an array. + + // zero length, don't advance argument ptr + break; + + default: // unknown type tag + // don't advance + --value_.typeTagPtr_; + break; + } +} + +//------------------------------------------------------------------------------ + +ReceivedMessage::ReceivedMessage( const ReceivedPacket& packet ) + : addressPattern_( packet.Contents() ) +{ + Init( packet.Contents(), packet.Size() ); +} + + +ReceivedMessage::ReceivedMessage( const ReceivedBundleElement& bundleElement ) + : addressPattern_( bundleElement.Contents() ) +{ + Init( bundleElement.Contents(), bundleElement.Size() ); +} + + +bool ReceivedMessage::AddressPatternIsUInt32() const +{ + return (addressPattern_[0] == '\0'); +} + + +uint32 ReceivedMessage::AddressPatternAsUInt32() const +{ + return ToUInt32( addressPattern_ ); +} + + +void ReceivedMessage::Init( const char *message, osc_bundle_element_size_t size ) +{ + if( !IsValidElementSizeValue(size) ) + throw MalformedMessageException( "invalid message size" ); + + if( size == 0 ) + throw MalformedMessageException( "zero length messages not permitted" ); + + if( !IsMultipleOf4(size) ) + throw MalformedMessageException( "message size must be multiple of four" ); + + const char *end = message + size; + + typeTagsBegin_ = FindStr4End( addressPattern_, end ); + if( typeTagsBegin_ == 0 ){ + // address pattern was not terminated before end + throw MalformedMessageException( "unterminated address pattern" ); + } + + if( typeTagsBegin_ == end ){ + // message consists of only the address pattern - no arguments or type tags. + typeTagsBegin_ = 0; + typeTagsEnd_ = 0; + arguments_ = 0; + + }else{ + if( *typeTagsBegin_ != ',' ) + throw MalformedMessageException( "type tags not present" ); + + if( *(typeTagsBegin_ + 1) == '\0' ){ + // zero length type tags + typeTagsBegin_ = 0; + typeTagsEnd_ = 0; + arguments_ = 0; + + }else{ + // check that all arguments are present and well formed + + arguments_ = FindStr4End( typeTagsBegin_, end ); + if( arguments_ == 0 ){ + throw MalformedMessageException( "type tags were not terminated before end of message" ); + } + + ++typeTagsBegin_; // advance past initial ',' + + const char *typeTag = typeTagsBegin_; + const char *argument = arguments_; + unsigned int arrayLevel = 0; + + do{ + switch( *typeTag ){ + case TRUE_TYPE_TAG: + case FALSE_TYPE_TAG: + case NIL_TYPE_TAG: + case INFINITUM_TYPE_TAG: + // zero length + break; + + // [ Indicates the beginning of an array. The tags following are for + // data in the Array until a close brace tag is reached. + // ] Indicates the end of an array. + case ARRAY_BEGIN_TYPE_TAG: + ++arrayLevel; + // (zero length argument data) + break; + + case ARRAY_END_TYPE_TAG: + --arrayLevel; + // (zero length argument data) + break; + + case INT32_TYPE_TAG: + case FLOAT_TYPE_TAG: + case CHAR_TYPE_TAG: + case RGBA_COLOR_TYPE_TAG: + case MIDI_MESSAGE_TYPE_TAG: + + if( argument == end ) + throw MalformedMessageException( "arguments exceed message size" ); + argument += 4; + if( argument > end ) + throw MalformedMessageException( "arguments exceed message size" ); + break; + + case INT64_TYPE_TAG: + case TIME_TAG_TYPE_TAG: + case DOUBLE_TYPE_TAG: + + if( argument == end ) + throw MalformedMessageException( "arguments exceed message size" ); + argument += 8; + if( argument > end ) + throw MalformedMessageException( "arguments exceed message size" ); + break; + + case STRING_TYPE_TAG: + case SYMBOL_TYPE_TAG: + + if( argument == end ) + throw MalformedMessageException( "arguments exceed message size" ); + argument = FindStr4End( argument, end ); + if( argument == 0 ) + throw MalformedMessageException( "unterminated string argument" ); + break; + + case BLOB_TYPE_TAG: + { + if( argument + osc::OSC_SIZEOF_INT32 > end ) + MalformedMessageException( "arguments exceed message size" ); + + // treat blob size as an unsigned int for the purposes of this calculation + uint32 blobSize = ToUInt32( argument ); + argument = argument + osc::OSC_SIZEOF_INT32 + RoundUp4( blobSize ); + if( argument > end ) + MalformedMessageException( "arguments exceed message size" ); + } + break; + + default: + throw MalformedMessageException( "unknown type tag" ); + } + + }while( *++typeTag != '\0' ); + typeTagsEnd_ = typeTag; + + if( arrayLevel != 0 ) + throw MalformedMessageException( "array was not terminated before end of message (expected ']' end of array tag)" ); + } + + // These invariants should be guaranteed by the above code. + // we depend on them in the implementation of ArgumentCount() +#ifndef NDEBUG + std::ptrdiff_t argumentCount = typeTagsEnd_ - typeTagsBegin_; + assert( argumentCount >= 0 ); + assert( argumentCount <= OSC_INT32_MAX ); +#endif + } +} + +//------------------------------------------------------------------------------ + +ReceivedBundle::ReceivedBundle( const ReceivedPacket& packet ) + : elementCount_( 0 ) +{ + Init( packet.Contents(), packet.Size() ); +} + + +ReceivedBundle::ReceivedBundle( const ReceivedBundleElement& bundleElement ) + : elementCount_( 0 ) +{ + Init( bundleElement.Contents(), bundleElement.Size() ); +} + + +void ReceivedBundle::Init( const char *bundle, osc_bundle_element_size_t size ) +{ + + if( !IsValidElementSizeValue(size) ) + throw MalformedBundleException( "invalid bundle size" ); + + if( size < 16 ) + throw MalformedBundleException( "packet too short for bundle" ); + + if( !IsMultipleOf4(size) ) + throw MalformedBundleException( "bundle size must be multiple of four" ); + + if( bundle[0] != '#' + || bundle[1] != 'b' + || bundle[2] != 'u' + || bundle[3] != 'n' + || bundle[4] != 'd' + || bundle[5] != 'l' + || bundle[6] != 'e' + || bundle[7] != '\0' ) + throw MalformedBundleException( "bad bundle address pattern" ); + + end_ = bundle + size; + + timeTag_ = bundle + 8; + + const char *p = timeTag_ + 8; + + while( p < end_ ){ + if( p + osc::OSC_SIZEOF_INT32 > end_ ) + throw MalformedBundleException( "packet too short for elementSize" ); + + // treat element size as an unsigned int for the purposes of this calculation + uint32 elementSize = ToUInt32( p ); + if( (elementSize & ((uint32)0x03)) != 0 ) + throw MalformedBundleException( "bundle element size must be multiple of four" ); + + p += osc::OSC_SIZEOF_INT32 + elementSize; + if( p > end_ ) + throw MalformedBundleException( "packet too short for bundle element" ); + + ++elementCount_; + } + + if( p != end_ ) + throw MalformedBundleException( "bundle contents " ); +} + + +uint64 ReceivedBundle::TimeTag() const +{ + return ToUInt64( timeTag_ ); +} + + +} // namespace osc diff --git a/src/osc/osc/OscReceivedElements.h b/src/osc/osc/OscReceivedElements.h new file mode 100644 index 00000000000..0a8170348c1 --- /dev/null +++ b/src/osc/osc/OscReceivedElements.h @@ -0,0 +1,548 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + The text above constitutes the entire oscpack license; however, + the oscpack developer(s) also make the following non-binding requests: + + Any person wishing to distribute modifications to the Software is + requested to send the modifications to the original developer so that + they can be incorporated into the canonical version. It is also + requested that these non-binding requests be included whenever the + above license is reproduced. +*/ +#ifndef INCLUDED_OSCPACK_OSCRECEIVEDELEMENTS_H +#define INCLUDED_OSCPACK_OSCRECEIVEDELEMENTS_H + +#include +#include +#include // size_t + +#include "OscTypes.h" +#include "OscException.h" + + +namespace osc{ + + +class MalformedPacketException : public Exception{ +public: + MalformedPacketException( const char *w="malformed packet" ) + : Exception( w ) {} +}; + +class MalformedMessageException : public Exception{ +public: + MalformedMessageException( const char *w="malformed message" ) + : Exception( w ) {} +}; + +class MalformedBundleException : public Exception{ +public: + MalformedBundleException( const char *w="malformed bundle" ) + : Exception( w ) {} +}; + +class WrongArgumentTypeException : public Exception{ +public: + WrongArgumentTypeException( const char *w="wrong argument type" ) + : Exception( w ) {} +}; + +class MissingArgumentException : public Exception{ +public: + MissingArgumentException( const char *w="missing argument" ) + : Exception( w ) {} +}; + +class ExcessArgumentException : public Exception{ +public: + ExcessArgumentException( const char *w="too many arguments" ) + : Exception( w ) {} +}; + + +class ReceivedPacket{ +public: + // Although the OSC spec is not entirely clear on this, we only support + // packets up to 0x7FFFFFFC bytes long (the maximum 4-byte aligned value + // representable by an int32). An exception will be raised if you pass a + // larger value to the ReceivedPacket() constructor. + + ReceivedPacket( const char *contents, osc_bundle_element_size_t size ) + : contents_( contents ) + , size_( ValidateSize(size) ) {} + + ReceivedPacket( const char *contents, std::size_t size ) + : contents_( contents ) + , size_( ValidateSize( (osc_bundle_element_size_t)size ) ) {} + +#if !(defined(__x86_64__) || defined(_M_X64)) + ReceivedPacket( const char *contents, int size ) + : contents_( contents ) + , size_( ValidateSize( (osc_bundle_element_size_t)size ) ) {} +#endif + + bool IsMessage() const { return !IsBundle(); } + bool IsBundle() const; + + osc_bundle_element_size_t Size() const { return size_; } + const char *Contents() const { return contents_; } + +private: + const char *contents_; + osc_bundle_element_size_t size_; + + static osc_bundle_element_size_t ValidateSize( osc_bundle_element_size_t size ) + { + // sanity check integer types declared in OscTypes.h + // you'll need to fix OscTypes.h if any of these asserts fail + assert( sizeof(osc::int32) == 4 ); + assert( sizeof(osc::uint32) == 4 ); + assert( sizeof(osc::int64) == 8 ); + assert( sizeof(osc::uint64) == 8 ); + + if( !IsValidElementSizeValue(size) ) + throw MalformedPacketException( "invalid packet size" ); + + if( size == 0 ) + throw MalformedPacketException( "zero length elements not permitted" ); + + if( !IsMultipleOf4(size) ) + throw MalformedPacketException( "element size must be multiple of four" ); + + return size; + } +}; + + +class ReceivedBundleElement{ +public: + ReceivedBundleElement( const char *sizePtr ) + : sizePtr_( sizePtr ) {} + + friend class ReceivedBundleElementIterator; + + bool IsMessage() const { return !IsBundle(); } + bool IsBundle() const; + + osc_bundle_element_size_t Size() const; + const char *Contents() const { return sizePtr_ + osc::OSC_SIZEOF_INT32; } + +private: + const char *sizePtr_; +}; + + +class ReceivedBundleElementIterator{ +public: + ReceivedBundleElementIterator( const char *sizePtr ) + : value_( sizePtr ) {} + + ReceivedBundleElementIterator operator++() + { + Advance(); + return *this; + } + + ReceivedBundleElementIterator operator++(int) + { + ReceivedBundleElementIterator old( *this ); + Advance(); + return old; + } + + const ReceivedBundleElement& operator*() const { return value_; } + + const ReceivedBundleElement* operator->() const { return &value_; } + + friend bool operator==(const ReceivedBundleElementIterator& lhs, + const ReceivedBundleElementIterator& rhs ); + +private: + ReceivedBundleElement value_; + + void Advance() { value_.sizePtr_ = value_.Contents() + value_.Size(); } + + bool IsEqualTo( const ReceivedBundleElementIterator& rhs ) const + { + return value_.sizePtr_ == rhs.value_.sizePtr_; + } +}; + +inline bool operator==(const ReceivedBundleElementIterator& lhs, + const ReceivedBundleElementIterator& rhs ) +{ + return lhs.IsEqualTo( rhs ); +} + +inline bool operator!=(const ReceivedBundleElementIterator& lhs, + const ReceivedBundleElementIterator& rhs ) +{ + return !( lhs == rhs ); +} + + +class ReceivedMessageArgument{ +public: + ReceivedMessageArgument( const char *typeTagPtr, const char *argumentPtr ) + : typeTagPtr_( typeTagPtr ) + , argumentPtr_( argumentPtr ) {} + + friend class ReceivedMessageArgumentIterator; + + char TypeTag() const { return *typeTagPtr_; } + + // the unchecked methods below don't check whether the argument actually + // is of the specified type. they should only be used if you've already + // checked the type tag or the associated IsType() method. + + bool IsBool() const + { return *typeTagPtr_ == TRUE_TYPE_TAG || *typeTagPtr_ == FALSE_TYPE_TAG; } + bool AsBool() const; + bool AsBoolUnchecked() const; + + bool IsNil() const { return *typeTagPtr_ == NIL_TYPE_TAG; } + bool IsInfinitum() const { return *typeTagPtr_ == INFINITUM_TYPE_TAG; } + + bool IsInt32() const { return *typeTagPtr_ == INT32_TYPE_TAG; } + int32 AsInt32() const; + int32 AsInt32Unchecked() const; + + bool IsFloat() const { return *typeTagPtr_ == FLOAT_TYPE_TAG; } + float AsFloat() const; + float AsFloatUnchecked() const; + + bool IsChar() const { return *typeTagPtr_ == CHAR_TYPE_TAG; } + char AsChar() const; + char AsCharUnchecked() const; + + bool IsRgbaColor() const { return *typeTagPtr_ == RGBA_COLOR_TYPE_TAG; } + uint32 AsRgbaColor() const; + uint32 AsRgbaColorUnchecked() const; + + bool IsMidiMessage() const { return *typeTagPtr_ == MIDI_MESSAGE_TYPE_TAG; } + uint32 AsMidiMessage() const; + uint32 AsMidiMessageUnchecked() const; + + bool IsInt64() const { return *typeTagPtr_ == INT64_TYPE_TAG; } + int64 AsInt64() const; + int64 AsInt64Unchecked() const; + + bool IsTimeTag() const { return *typeTagPtr_ == TIME_TAG_TYPE_TAG; } + uint64 AsTimeTag() const; + uint64 AsTimeTagUnchecked() const; + + bool IsDouble() const { return *typeTagPtr_ == DOUBLE_TYPE_TAG; } + double AsDouble() const; + double AsDoubleUnchecked() const; + + bool IsString() const { return *typeTagPtr_ == STRING_TYPE_TAG; } + const char* AsString() const; + const char* AsStringUnchecked() const { return argumentPtr_; } + + bool IsSymbol() const { return *typeTagPtr_ == SYMBOL_TYPE_TAG; } + const char* AsSymbol() const; + const char* AsSymbolUnchecked() const { return argumentPtr_; } + + bool IsBlob() const { return *typeTagPtr_ == BLOB_TYPE_TAG; } + void AsBlob( const void*& data, osc_bundle_element_size_t& size ) const; + void AsBlobUnchecked( const void*& data, osc_bundle_element_size_t& size ) const; + + bool IsArrayBegin() const { return *typeTagPtr_ == ARRAY_BEGIN_TYPE_TAG; } + bool IsArrayEnd() const { return *typeTagPtr_ == ARRAY_END_TYPE_TAG; } + // Calculate the number of top-level items in the array. Nested arrays count as one item. + // Only valid at array start. Will throw an exception if IsArrayStart() == false. + std::size_t ComputeArrayItemCount() const; + +private: + const char *typeTagPtr_; + const char *argumentPtr_; +}; + + +class ReceivedMessageArgumentIterator{ +public: + ReceivedMessageArgumentIterator( const char *typeTags, const char *arguments ) + : value_( typeTags, arguments ) {} + + ReceivedMessageArgumentIterator operator++() + { + Advance(); + return *this; + } + + ReceivedMessageArgumentIterator operator++(int) + { + ReceivedMessageArgumentIterator old( *this ); + Advance(); + return old; + } + + const ReceivedMessageArgument& operator*() const { return value_; } + + const ReceivedMessageArgument* operator->() const { return &value_; } + + friend bool operator==(const ReceivedMessageArgumentIterator& lhs, + const ReceivedMessageArgumentIterator& rhs ); + +private: + ReceivedMessageArgument value_; + + void Advance(); + + bool IsEqualTo( const ReceivedMessageArgumentIterator& rhs ) const + { + return value_.typeTagPtr_ == rhs.value_.typeTagPtr_; + } +}; + +inline bool operator==(const ReceivedMessageArgumentIterator& lhs, + const ReceivedMessageArgumentIterator& rhs ) +{ + return lhs.IsEqualTo( rhs ); +} + +inline bool operator!=(const ReceivedMessageArgumentIterator& lhs, + const ReceivedMessageArgumentIterator& rhs ) +{ + return !( lhs == rhs ); +} + + +class ReceivedMessageArgumentStream{ + friend class ReceivedMessage; + ReceivedMessageArgumentStream( const ReceivedMessageArgumentIterator& begin, + const ReceivedMessageArgumentIterator& end ) + : p_( begin ) + , end_( end ) {} + + ReceivedMessageArgumentIterator p_, end_; + +public: + + // end of stream + bool Eos() const { return p_ == end_; } + + ReceivedMessageArgumentStream& operator>>( bool& rhs ) + { + if( Eos() ) + throw MissingArgumentException(); + + rhs = (*p_++).AsBool(); + return *this; + } + + // not sure if it would be useful to stream Nil and Infinitum + // for now it's not possible + // same goes for array boundaries + + ReceivedMessageArgumentStream& operator>>( int32& rhs ) + { + if( Eos() ) + throw MissingArgumentException(); + + rhs = (*p_++).AsInt32(); + return *this; + } + + ReceivedMessageArgumentStream& operator>>( float& rhs ) + { + if( Eos() ) + throw MissingArgumentException(); + + rhs = (*p_++).AsFloat(); + return *this; + } + + ReceivedMessageArgumentStream& operator>>( char& rhs ) + { + if( Eos() ) + throw MissingArgumentException(); + + rhs = (*p_++).AsChar(); + return *this; + } + + ReceivedMessageArgumentStream& operator>>( RgbaColor& rhs ) + { + if( Eos() ) + throw MissingArgumentException(); + + rhs.value = (*p_++).AsRgbaColor(); + return *this; + } + + ReceivedMessageArgumentStream& operator>>( MidiMessage& rhs ) + { + if( Eos() ) + throw MissingArgumentException(); + + rhs.value = (*p_++).AsMidiMessage(); + return *this; + } + + ReceivedMessageArgumentStream& operator>>( int64& rhs ) + { + if( Eos() ) + throw MissingArgumentException(); + + rhs = (*p_++).AsInt64(); + return *this; + } + + ReceivedMessageArgumentStream& operator>>( TimeTag& rhs ) + { + if( Eos() ) + throw MissingArgumentException(); + + rhs.value = (*p_++).AsTimeTag(); + return *this; + } + + ReceivedMessageArgumentStream& operator>>( double& rhs ) + { + if( Eos() ) + throw MissingArgumentException(); + + rhs = (*p_++).AsDouble(); + return *this; + } + + ReceivedMessageArgumentStream& operator>>( Blob& rhs ) + { + if( Eos() ) + throw MissingArgumentException(); + + (*p_++).AsBlob( rhs.data, rhs.size ); + return *this; + } + + ReceivedMessageArgumentStream& operator>>( const char*& rhs ) + { + if( Eos() ) + throw MissingArgumentException(); + + rhs = (*p_++).AsString(); + return *this; + } + + ReceivedMessageArgumentStream& operator>>( Symbol& rhs ) + { + if( Eos() ) + throw MissingArgumentException(); + + rhs.value = (*p_++).AsSymbol(); + return *this; + } + + ReceivedMessageArgumentStream& operator>>( MessageTerminator& rhs ) + { + (void) rhs; // suppress unused parameter warning + + if( !Eos() ) + throw ExcessArgumentException(); + + return *this; + } +}; + + +class ReceivedMessage{ + void Init( const char *bundle, osc_bundle_element_size_t size ); +public: + explicit ReceivedMessage( const ReceivedPacket& packet ); + explicit ReceivedMessage( const ReceivedBundleElement& bundleElement ); + + const char *AddressPattern() const { return addressPattern_; } + + // Support for non-standard SuperCollider integer address patterns: + bool AddressPatternIsUInt32() const; + uint32 AddressPatternAsUInt32() const; + + uint32 ArgumentCount() const { return static_cast(typeTagsEnd_ - typeTagsBegin_); } + + const char *TypeTags() const { return typeTagsBegin_; } + + + typedef ReceivedMessageArgumentIterator const_iterator; + + ReceivedMessageArgumentIterator ArgumentsBegin() const + { + return ReceivedMessageArgumentIterator( typeTagsBegin_, arguments_ ); + } + + ReceivedMessageArgumentIterator ArgumentsEnd() const + { + return ReceivedMessageArgumentIterator( typeTagsEnd_, 0 ); + } + + ReceivedMessageArgumentStream ArgumentStream() const + { + return ReceivedMessageArgumentStream( ArgumentsBegin(), ArgumentsEnd() ); + } + +private: + const char *addressPattern_; + const char *typeTagsBegin_; + const char *typeTagsEnd_; + const char *arguments_; +}; + + +class ReceivedBundle{ + void Init( const char *message, osc_bundle_element_size_t size ); +public: + explicit ReceivedBundle( const ReceivedPacket& packet ); + explicit ReceivedBundle( const ReceivedBundleElement& bundleElement ); + + uint64 TimeTag() const; + + uint32 ElementCount() const { return elementCount_; } + + typedef ReceivedBundleElementIterator const_iterator; + + ReceivedBundleElementIterator ElementsBegin() const + { + return ReceivedBundleElementIterator( timeTag_ + 8 ); + } + + ReceivedBundleElementIterator ElementsEnd() const + { + return ReceivedBundleElementIterator( end_ ); + } + +private: + const char *timeTag_; + const char *end_; + uint32 elementCount_; +}; + + +} // namespace osc + + +#endif /* INCLUDED_OSCPACK_OSCRECEIVEDELEMENTS_H */ diff --git a/src/osc/osc/OscTypes.cpp b/src/osc/osc/OscTypes.cpp new file mode 100644 index 00000000000..dd8bdbaeb30 --- /dev/null +++ b/src/osc/osc/OscTypes.cpp @@ -0,0 +1,52 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + The text above constitutes the entire oscpack license; however, + the oscpack developer(s) also make the following non-binding requests: + + Any person wishing to distribute modifications to the Software is + requested to send the modifications to the original developer so that + they can be incorporated into the canonical version. It is also + requested that these non-binding requests be included whenever the + above license is reproduced. +*/ +#include "OscTypes.h" + +namespace osc{ + +BundleInitiator BeginBundleImmediate(1); +BundleTerminator EndBundle; +MessageTerminator EndMessage; +NilType OscNil; +#ifndef _OBJC_OBJC_H_ +NilType Nil; // Objective-C defines Nil. so our Nil is deprecated. use OscNil instead +#endif +InfinitumType Infinitum; +ArrayInitiator BeginArray; +ArrayTerminator EndArray; + +} // namespace osc diff --git a/src/osc/osc/OscTypes.h b/src/osc/osc/OscTypes.h new file mode 100644 index 00000000000..ee2232e064e --- /dev/null +++ b/src/osc/osc/OscTypes.h @@ -0,0 +1,240 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* + The text above constitutes the entire oscpack license; however, + the oscpack developer(s) also make the following non-binding requests: + + Any person wishing to distribute modifications to the Software is + requested to send the modifications to the original developer so that + they can be incorporated into the canonical version. It is also + requested that these non-binding requests be included whenever the + above license is reproduced. +*/ +#ifndef INCLUDED_OSCPACK_OSCTYPES_H +#define INCLUDED_OSCPACK_OSCTYPES_H + + +namespace osc{ + +// basic types + +#if defined(__BORLANDC__) || defined(_MSC_VER) + +typedef __int64 int64; +typedef unsigned __int64 uint64; + +#elif defined(__x86_64__) || defined(_M_X64) + +typedef long int64; +typedef unsigned long uint64; + +#else + +typedef long long int64; +typedef unsigned long long uint64; + +#endif + + + +#if defined(__x86_64__) || defined(_M_X64) + +typedef signed int int32; +typedef unsigned int uint32; + +#else + +typedef signed long int32; +typedef unsigned long uint32; + +#endif + + +enum ValueTypeSizes{ + OSC_SIZEOF_INT32 = 4, + OSC_SIZEOF_UINT32 = 4, + OSC_SIZEOF_INT64 = 8, + OSC_SIZEOF_UINT64 = 8, +}; + + +// osc_bundle_element_size_t is used for the size of bundle elements and blobs +// the OSC spec specifies these as int32 (signed) but we ensure that they +// are always positive since negative field sizes make no sense. + +typedef int32 osc_bundle_element_size_t; + +enum { + OSC_INT32_MAX = 0x7FFFFFFF, + + // Element sizes are specified to be int32, and are always rounded up to nearest + // multiple of 4. Therefore their values can't be greater than 0x7FFFFFFC. + OSC_BUNDLE_ELEMENT_SIZE_MAX = 0x7FFFFFFC +}; + + +inline bool IsValidElementSizeValue( osc_bundle_element_size_t x ) +{ + // sizes may not be negative or exceed OSC_BUNDLE_ELEMENT_SIZE_MAX + return x >= 0 && x <= OSC_BUNDLE_ELEMENT_SIZE_MAX; +} + + +inline bool IsMultipleOf4( osc_bundle_element_size_t x ) +{ + return (x & ((osc_bundle_element_size_t)0x03)) == 0; +} + + +enum TypeTagValues { + TRUE_TYPE_TAG = 'T', + FALSE_TYPE_TAG = 'F', + NIL_TYPE_TAG = 'N', + INFINITUM_TYPE_TAG = 'I', + INT32_TYPE_TAG = 'i', + FLOAT_TYPE_TAG = 'f', + CHAR_TYPE_TAG = 'c', + RGBA_COLOR_TYPE_TAG = 'r', + MIDI_MESSAGE_TYPE_TAG = 'm', + INT64_TYPE_TAG = 'h', + TIME_TAG_TYPE_TAG = 't', + DOUBLE_TYPE_TAG = 'd', + STRING_TYPE_TAG = 's', + SYMBOL_TYPE_TAG = 'S', + BLOB_TYPE_TAG = 'b', + ARRAY_BEGIN_TYPE_TAG = '[', + ARRAY_END_TYPE_TAG = ']' +}; + + + +// i/o manipulators used for streaming interfaces + +struct BundleInitiator{ + explicit BundleInitiator( uint64 timeTag_ ) : timeTag( timeTag_ ) {} + uint64 timeTag; +}; + +extern BundleInitiator BeginBundleImmediate; + +inline BundleInitiator BeginBundle( uint64 timeTag=1 ) +{ + return BundleInitiator(timeTag); +} + + +struct BundleTerminator{ +}; + +extern BundleTerminator EndBundle; + +struct BeginMessage{ + explicit BeginMessage( const char *addressPattern_ ) : addressPattern( addressPattern_ ) {} + const char *addressPattern; +}; + +struct MessageTerminator{ +}; + +extern MessageTerminator EndMessage; + + +// osc specific types. they are defined as structs so they can be used +// as separately identifiable types with the streaming operators. + +struct NilType{ +}; + +extern NilType OscNil; + +#ifndef _OBJC_OBJC_H_ +extern NilType Nil; // Objective-C defines Nil. so our Nil is deprecated. use OscNil instead +#endif + +struct InfinitumType{ +}; + +extern InfinitumType Infinitum; + +struct RgbaColor{ + RgbaColor() {} + explicit RgbaColor( uint32 value_ ) : value( value_ ) {} + uint32 value; + + operator uint32() const { return value; } +}; + + +struct MidiMessage{ + MidiMessage() {} + explicit MidiMessage( uint32 value_ ) : value( value_ ) {} + uint32 value; + + operator uint32() const { return value; } +}; + + +struct TimeTag{ + TimeTag() {} + explicit TimeTag( uint64 value_ ) : value( value_ ) {} + uint64 value; + + operator uint64() const { return value; } +}; + + +struct Symbol{ + Symbol() {} + explicit Symbol( const char* value_ ) : value( value_ ) {} + const char* value; + + operator const char *() const { return value; } +}; + + +struct Blob{ + Blob() {} + explicit Blob( const void* data_, osc_bundle_element_size_t size_ ) + : data( data_ ), size( size_ ) {} + const void* data; + osc_bundle_element_size_t size; +}; + +struct ArrayInitiator{ +}; + +extern ArrayInitiator BeginArray; + +struct ArrayTerminator{ +}; + +extern ArrayTerminator EndArray; + +} // namespace osc + + +#endif /* INCLUDED_OSCPACK_OSCTYPES_H */ diff --git a/src/osc/oscfunctions.h b/src/osc/oscfunctions.h new file mode 100644 index 00000000000..81d2d7c1013 --- /dev/null +++ b/src/osc/oscfunctions.h @@ -0,0 +1,175 @@ +#ifndef OSCFUNCTIONS_H +#define OSCFUNCTIONS_H + +// #define oscClientAddress "192.168.0.125" +// constexpr const char* oscClientAddress = "192.168.0.125"; +// #define OscPortOut 9000 +// #define OscPortIn 9001 +// #define OUTPUT_BUFFER_SIZE 1024 +constexpr int OUTPUT_BUFFER_SIZE = 1024; +// #define IP_MTU_SIZE 1536 +constexpr int IP_MTU_SIZE = 1536; + +#include +#include +#include +#include +#include + +#include "osc/ip/UdpSocket.h" +#include "osc/osc/OscOutboundPacketStream.h" +#include "osc/osc/OscPacketListener.h" +#include "osc/osc/OscReceivedElements.h" + +enum DefOscBodyType { + STRINGBODY = 1, + INTBODY = 2, + DOUBLEBODY = 3, + FLOATBODY = 4 +}; + +void OscFunctionsSendPtrType(UserSettingsPointer m_pConfig, + QString OscGroup, + QString OscKey, + enum DefOscBodyType OscBodyType, + QString OscMessageBodyQString, + int OscMessageBodyInt, + double OscMessageBodyDouble, + float OscMessageBodyFloat) { + QString MixxxOSCStatusFileLocation = m_pConfig->getSettingsPath() + "/MixxxOSCStatus.txt"; + QFile MixxxOSCStatusFile(MixxxOSCStatusFileLocation); + MixxxOSCStatusFile.open(QIODevice::ReadWrite | QIODevice::Append); + QTextStream MixxxOSCStatusTxt(&MixxxOSCStatusFile); + + // OscGroup.replace("[", ""); + // OscGroup.replace("]", ""); + // OscGroup = OscGroup.mid(1, OscGroup.length() - 2); + + QString OscMessageHeader = "/" + OscGroup + "@" + OscKey; + QByteArray OscMessageHeaderBa = OscMessageHeader.toLocal8Bit(); + const char* OscMessageHeaderChar = OscMessageHeaderBa.data(); + QByteArray OscMessageBodyBa; + const char* OscMessageBodyChar; + + if (m_pConfig->getValue(ConfigKey("[OSC]", "OscEnabled"))) { + char buffer[IP_MTU_SIZE]; + osc::OutboundPacketStream p(buffer, IP_MTU_SIZE); + QString MixxxOSCStatusTxtBody; + switch (OscBodyType) { + case 1: // QSTRINGBODY = 1 + OscMessageBodyBa = OscMessageBodyQString.toLocal8Bit(); + OscMessageBodyChar = OscMessageBodyBa.data(); + p.Clear(); + p << osc::BeginBundle(); + p << osc::BeginMessage(OscMessageHeaderChar) << OscMessageBodyChar << osc::EndMessage; + p << osc::EndBundle; + MixxxOSCStatusTxtBody = OscMessageBodyChar; + break; + case 2: // INTBODY = 2 + p.Clear(); + p << osc::BeginBundle(); + p << osc::BeginMessage(OscMessageHeaderChar) << OscMessageBodyInt << osc::EndMessage; + p << osc::EndBundle; + MixxxOSCStatusTxtBody = QString::number(OscMessageBodyInt); + break; + case 3: // DOUBLEBODY = 3 + p.Clear(); + p << osc::BeginBundle(); + p << osc::BeginMessage(OscMessageHeaderChar) << OscMessageBodyDouble << osc::EndMessage; + p << osc::EndBundle; + MixxxOSCStatusTxtBody = QString::number(OscMessageBodyDouble); + break; + case 4: // FLOATBODY = 4 + p.Clear(); + p << osc::BeginBundle(); + p << osc::BeginMessage(OscMessageHeaderChar) << OscMessageBodyFloat << osc::EndMessage; + p << osc::EndBundle; + MixxxOSCStatusTxtBody = QString::number(OscMessageBodyFloat); + break; + } + + QString CKOscPortOut = m_pConfig->getValue(ConfigKey("[OSC]", "OscPortOut")); + int CKOscPortOutInt = CKOscPortOut.toInt(); + if (m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver1Active"))) { + QString CKOscRec1Active = m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver1Active")); + QString CKOscRec1Ip = m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver1Ip")); + QByteArray CKOscRec1Ipba = CKOscRec1Ip.toLocal8Bit(); + const char* CKOscRec1IpChar = CKOscRec1Ipba.data(); + + UdpTransmitSocket transmitSocket(IpEndpointName(CKOscRec1IpChar, CKOscPortOutInt)); + transmitSocket.Send(p.Data(), p.Size()); + MixxxOSCStatusTxt << "OSC Msg Send to Receiver 1 (" + << CKOscRec1IpChar << ":" << CKOscPortOutInt + << QString(") : <%1 : %2") + .arg(OscMessageHeader) + .arg(MixxxOSCStatusTxtBody) + << ">\n"; + } + if (m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver2Active"))) { + QString CKOscRec2Active = m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver2Active")); + QString CKOscRec2Ip = m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver2Ip")); + QByteArray CKOscRec2Ipba = CKOscRec2Ip.toLocal8Bit(); + const char* CKOscRec2IpChar = CKOscRec2Ipba.data(); + + UdpTransmitSocket transmitSocket(IpEndpointName(CKOscRec2IpChar, CKOscPortOutInt)); + transmitSocket.Send(p.Data(), p.Size()); + MixxxOSCStatusTxt << "OSC Msg Send to Receiver 2 (" + << CKOscRec2IpChar << ":" << CKOscPortOutInt + << QString(") : <%1 : %2") + .arg(OscMessageHeader) + .arg(MixxxOSCStatusTxtBody) + << ">\n"; + } + if (m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver3Active"))) { + QString CKOscRec3Active = m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver3Active")); + QString CKOscRec3Ip = m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver3Ip")); + QByteArray CKOscRec3Ipba = CKOscRec3Ip.toLocal8Bit(); + const char* CKOscRec3IpChar = CKOscRec3Ipba.data(); + + UdpTransmitSocket transmitSocket(IpEndpointName(CKOscRec3IpChar, CKOscPortOutInt)); + transmitSocket.Send(p.Data(), p.Size()); + MixxxOSCStatusTxt << "OSC Msg Send to Receiver 3 (" + << CKOscRec3IpChar << ":" << CKOscPortOutInt + << QString(") : <%1 : %2") + .arg(OscMessageHeader) + .arg(MixxxOSCStatusTxtBody) + << ">\n"; + } + if (m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver4Active"))) { + QString CKOscRec4Active = m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver4Active")); + QString CKOscRec4Ip = m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver4Ip")); + QByteArray CKOscRec4Ipba = CKOscRec4Ip.toLocal8Bit(); + const char* CKOscRec4IpChar = CKOscRec4Ipba.data(); + + UdpTransmitSocket transmitSocket(IpEndpointName(CKOscRec4IpChar, CKOscPortOutInt)); + transmitSocket.Send(p.Data(), p.Size()); + MixxxOSCStatusTxt << "OSC Msg Send to Receiver 4 (" + << CKOscRec4IpChar << ":" << CKOscPortOutInt + << QString(") : <%1 : %2") + .arg(OscMessageHeader) + .arg(MixxxOSCStatusTxtBody) + << ">\n"; + } + if (m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver5Active"))) { + QString CKOscRec5Active = m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver5Active")); + QString CKOscRec5Ip = m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver5Ip")); + QByteArray CKOscRec5Ipba = CKOscRec5Ip.toLocal8Bit(); + const char* CKOscRec5IpChar = CKOscRec5Ipba.data(); + + UdpTransmitSocket transmitSocket(IpEndpointName(CKOscRec5IpChar, CKOscPortOutInt)); + transmitSocket.Send(p.Data(), p.Size()); + MixxxOSCStatusTxt << "OSC Msg Send to Receiver 5 (" + << CKOscRec5IpChar << ":" << CKOscPortOutInt + << QString(") : <%1 : %2") + .arg(OscMessageHeader) + .arg(MixxxOSCStatusTxtBody) + << ">\n"; + } + } else { + MixxxOSCStatusTxt << QString("OSC NOT Enabled") << "\n"; + } + + MixxxOSCStatusFile.close(); +} + +#endif /* OSCFUNCTIONS_H */ diff --git a/src/osc/oscreceiver.cpp b/src/osc/oscreceiver.cpp new file mode 100644 index 00000000000..9170ad0a2e2 --- /dev/null +++ b/src/osc/oscreceiver.cpp @@ -0,0 +1,253 @@ +#include +#include +#include +#include + +#pragma comment(lib, "winmm.lib") + +#include "control/controlobject.h" +#include "osc/ip/UdpSocket.h" +#include "osc/osc/OscPacketListener.h" +#include "osc/osc/OscReceivedElements.h" +#include "oscfunctions.h" +#include "oscreceiver.h" + +void OscFunctionsSendPtrType(UserSettingsPointer m_pConfig, + QString OscGroup, + QString OscKey, + enum DefOscBodyType OscBodyType, + QString OscMessageBodyQString, + int OscMessageBodyInt, + double OscMessageBodyDouble, + float OscMessageBodyFloat); + +class OscReceivePacketListener : public osc::OscPacketListener { + public: + UserSettingsPointer m_pConfig; + OscReceivePacketListener(UserSettingsPointer aPointerHerePlease) { + m_pConfig = aPointerHerePlease; + }; + + private: + void ProcessMessage(const osc::ReceivedMessage& m, + const IpEndpointName& remoteEndpoint) { + (void)remoteEndpoint; + + try { + osc::ReceivedMessageArgumentStream args = m.ArgumentStream(); + osc::ReceivedMessage::const_iterator arg = m.ArgumentsBegin(); + + float oscInVal; + args >> oscInVal >> osc::EndMessage; + + oscResult oscIn; + oscIn.oscAddress = m.AddressPattern(); + oscIn.oscGroup, oscIn.oscKey; + oscIn.oscAddress.replace("/", ""); + oscIn.oscValue = oscInVal; + bool oscGet = false; + bool oscSet = false; + + if (oscIn.oscAddress.contains("Get#", Qt::CaseSensitive)) { + int posDel1 = oscIn.oscAddress.indexOf("Get#", 0, Qt::CaseInsensitive); + if (posDel1 == 0) { + oscGet = true; + } + } else { + oscSet = true; + } + if (oscGet) { + int posDel2 = oscIn.oscAddress.indexOf("@", 0, Qt::CaseInsensitive); + if (posDel2 > 0) { + oscIn.oscGroup = oscIn.oscAddress.mid(4, posDel2 - 4); + // oscIn.oscGroup = oscIn.oscAddress.mid(0, posDel2); + oscIn.oscGroupSB = "[" + oscIn.oscGroup + "]"; + oscIn.oscKey = oscIn.oscAddress.mid(posDel2 + 1, oscIn.oscAddress.length() - 4); + QString MixxxOSCStatusFileLocation = + m_pConfig->getSettingsPath() + "/MixxxOSCStatus.txt"; + QFile MixxxOSCStatusFile(MixxxOSCStatusFileLocation); + MixxxOSCStatusFile.open(QIODevice::ReadWrite | QIODevice::Append); + QTextStream MixxxOSCStatusTxt(&MixxxOSCStatusFile); + MixxxOSCStatusTxt << QString("OSC Msg Rcvd: Get Group, Key: Value: " + "<%1,%2 : %3>") + .arg(oscIn.oscGroupSB) + .arg(oscIn.oscKey) + .arg(oscIn.oscValue) + << "\n"; + // ControlObject::getControl(oscIn.oscGroupSB, oscIn.oscKey)->getParameter(); + + OscFunctionsSendPtrType(m_pConfig, + oscIn.oscGroup, + oscIn.oscKey, + FLOATBODY, + "", + 0, + 0, + ControlObject::getControl( + oscIn.oscGroupSB, oscIn.oscKey) + ->getParameter()); + + MixxxOSCStatusFile.close(); + qDebug() << "OSC Msg Rcvd: Get Group, Key: Value: " + << oscIn.oscGroup << "," << oscIn.oscKey << ":" + << oscIn.oscValue; + } + } + + if (!oscGet && oscSet) { + int posDel2 = oscIn.oscAddress.indexOf("@", 0, Qt::CaseInsensitive); + if (posDel2 > 0) { + oscIn.oscGroup = oscIn.oscAddress.mid(0, posDel2); + oscIn.oscGroupSB = "[" + oscIn.oscGroup + "]"; + oscIn.oscKey = oscIn.oscAddress.mid(posDel2 + 1, oscIn.oscAddress.length()); + + QString MixxxOSCStatusFileLocation = + m_pConfig->getSettingsPath() + "/MixxxOSCStatus.txt"; + QFile MixxxOSCStatusFile(MixxxOSCStatusFileLocation); + MixxxOSCStatusFile.open(QIODevice::ReadWrite | QIODevice::Append); + QTextStream MixxxOSCStatusTxt(&MixxxOSCStatusFile); + MixxxOSCStatusTxt << QString("OSC Msg Rcvd: Group, Key: Value: " + "<%1,%2 : %3>") + .arg(oscIn.oscGroupSB) + .arg(oscIn.oscKey) + .arg(oscIn.oscValue) + << "\n"; + ControlObject::getControl(oscIn.oscGroupSB, oscIn.oscKey)->set(oscIn.oscValue); + MixxxOSCStatusFile.close(); + // qDebug() << "OSC Msg Rcvd: Group, Key: Value: " + // << oscIn.oscGroupSB << "," << oscIn.oscKey << ":" + // << oscIn.oscValue; + // OscFunctionsSendPtrType(m_pConfig, + // "[Osc]", + // "OscSync", + // FLOATBODY, + // "", + // 0, + // 0, + // 1); + } + } + + } catch (osc::Exception& e) { + // std::cout << "error while parsing message: " << + // m.AddressPattern() << ": " << e.what() << "\n"; + QString MixxxOSCStatusFileLocation = + m_pConfig->getSettingsPath() + "/MixxxOSCStatus.txt"; + QFile MixxxOSCStatusFile(MixxxOSCStatusFileLocation); + MixxxOSCStatusFile.open(QIODevice::ReadWrite | QIODevice::Append); + QTextStream MixxxOSCStatusTxt(&MixxxOSCStatusFile); + MixxxOSCStatusTxt << QString("OSC Error parsing Msg from %1 : %2").arg(m.AddressPattern()).arg(e.what()) << "\n"; + MixxxOSCStatusFile.close(); + qDebug() << "OSC Error parsing Msg from " << m.AddressPattern() << "error: " << e.what(); + } + } +}; + +void RunOscReceiver(int OscPortIn, UserSettingsPointer m_pConfig) { + OscReceivePacketListener listener(m_pConfig); + UdpListeningReceiveSocket s(IpEndpointName(IpEndpointName::ANY_ADDRESS, OscPortIn), + &listener); + s.Run(); +} + +// #ifndef NO_OSC_TEST_MAIN + +void OscReceiverMain(UserSettingsPointer m_pConfig) { + QString MixxxOSCStatusFileLocation = m_pConfig->getSettingsPath() + "/MixxxOSCStatus.txt"; + QFile MixxxOSCStatusFile(MixxxOSCStatusFileLocation); + MixxxOSCStatusFile.remove(); + MixxxOSCStatusFile.open(QIODevice::ReadWrite | QIODevice::Append); + QTextStream MixxxOSCStatusTxt(&MixxxOSCStatusFile); + if (m_pConfig->getValue(ConfigKey("[OSC]", "OscEnabled"))) { + QString CKOscPortOut = m_pConfig->getValue(ConfigKey("[OSC]", "OscPortOut")); + QString CKOscPortIn = m_pConfig->getValue(ConfigKey("[OSC]", "OscPortIn")); + int CKOscPortOutInt = CKOscPortOut.toInt(); + int CKOscPortInInt = CKOscPortIn.toInt(); + MixxxOSCStatusTxt << QString("OSC Enabled -> Started") << "\n"; + qDebug() << "OSC Enabled -> Started"; + MixxxOSCStatusTxt << QString("OSC Settings: PortIn : %1") + .arg(CKOscPortInInt) + << "\n"; + MixxxOSCStatusTxt << QString("OSC Settings: PortOut : %1") + .arg(CKOscPortOutInt) + << "\n"; + + if (m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver1Active"))) { + QString CKOscRec1Active = m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver1Active")); + QString CKOscRec1Ip = m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver1Ip")); + QByteArray CKOscRec1Ipba = CKOscRec1Ip.toLocal8Bit(); + const char* CKOscRec1IpChar = CKOscRec1Ipba.data(); + MixxxOSCStatusTxt << QString("OSC Settings: Receiver 1 active : %1") + .arg(CKOscRec1Active) + << "\n"; + MixxxOSCStatusTxt << QString("OSC Settings: Receiver 1 ip : %1") + .arg(CKOscRec1IpChar) + << "\n"; + } else { + MixxxOSCStatusTxt << QString("OSC Settings: Receiver 1 NOT active") << "\n"; + } + if (m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver2Active"))) { + QString CKOscRec2Active = m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver2Active")); + QString CKOscRec2Ip = m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver2Ip")); + QByteArray CKOscRec2Ipba = CKOscRec2Ip.toLocal8Bit(); + const char* CKOscRec2IpChar = CKOscRec2Ipba.data(); + MixxxOSCStatusTxt << QString("OSC Settings: Receiver 2 active : %1") + .arg(CKOscRec2Active) + << "\n"; + MixxxOSCStatusTxt << QString("OSC Settings: Receiver 2 ip : %1") + .arg(CKOscRec2IpChar) + << "\n"; + } else { + MixxxOSCStatusTxt << QString("OSC Settings: Receiver 2 NOT active") << "\n"; + } + if (m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver3Active"))) { + QString CKOscRec3Active = m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver3Active")); + QString CKOscRec3Ip = m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver3Ip")); + QByteArray CKOscRec3Ipba = CKOscRec3Ip.toLocal8Bit(); + const char* CKOscRec3IpChar = CKOscRec3Ipba.data(); + MixxxOSCStatusTxt << QString("OSC Settings: Receiver 3 active : %1") + .arg(CKOscRec3Active) + << "\n"; + MixxxOSCStatusTxt << QString("OSC Settings: Receiver 3 ip : %1") + .arg(CKOscRec3IpChar) + << "\n"; + } else { + MixxxOSCStatusTxt << QString("OSC Settings: Receiver 3 NOT active") << "\n"; + } + if (m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver4Active"))) { + QString CKOscRec4Active = m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver4Active")); + QString CKOscRec4Ip = m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver4Ip")); + QByteArray CKOscRec4Ipba = CKOscRec4Ip.toLocal8Bit(); + const char* CKOscRec4IpChar = CKOscRec4Ipba.data(); + MixxxOSCStatusTxt << QString("OSC Settings: Receiver 4 active : %1") + .arg(CKOscRec4Active) + << "\n"; + MixxxOSCStatusTxt << QString("OSC Settings: Receiver 4 ip : %1") + .arg(CKOscRec4IpChar) + << "\n"; + } else { + MixxxOSCStatusTxt << QString("OSC Settings: Receiver 4 NOT active") << "\n"; + } + if (m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver5Active"))) { + QString CKOscRec5Active = m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver5Active")); + QString CKOscRec5Ip = m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver5Ip")); + QByteArray CKOscRec5Ipba = CKOscRec5Ip.toLocal8Bit(); + const char* CKOscRec5IpChar = CKOscRec5Ipba.data(); + MixxxOSCStatusTxt << QString("OSC Settings: Receiver 5 active : %1") + .arg(CKOscRec5Active) + << "\n"; + MixxxOSCStatusTxt << QString("OSC Settings: Receiver 5 ip : %1") + .arg(CKOscRec5IpChar) + << "\n"; + } else { + MixxxOSCStatusTxt << QString("OSC Settings: Receiver 5 NOT active") << "\n"; + } + std::thread tosc(RunOscReceiver, CKOscPortInInt, m_pConfig); + tosc.detach(); + } else { + MixxxOSCStatusTxt << QString("OSC NOT Enabled") << "\n"; + } + MixxxOSCStatusFile.close(); +} + +// #endif /* NO_OSC_TEST_MAIN */ diff --git a/src/osc/oscreceiver.h b/src/osc/oscreceiver.h new file mode 100644 index 00000000000..9d530381ef8 --- /dev/null +++ b/src/osc/oscreceiver.h @@ -0,0 +1,37 @@ +#ifndef INCLUDED_OSCRECEIVER_H +#define INCLUDED_OSCRECEIVER_H + +// #include "preferences/colorpalettesettings.h" + +#include +#include +#include +#include + +#include "control/controlproxy.h" +#include "control/pollingcontrolproxy.h" +#include "preferences/settingsmanager.h" +#include "preferences/usersettings.h" +#include "util/class.h" + +class ControlProxy; + +class oscResult { + public: + QString oscAddress; + QString oscGroup; + QString oscGroupSB; + QString oscKey; + float oscValue; +}; + +class oscReceiver { + public: + UserSettingsPointer m_pConfig; + +}; + +void OscReceiverMain(UserSettingsPointer m_pConfig); + + +#endif /* INCLUDED_OSCRECEIVER_H */ diff --git a/src/preferences/dialog/dlgpreferences.cpp b/src/preferences/dialog/dlgpreferences.cpp index 47b96b9fa06..2e61a59a977 100644 --- a/src/preferences/dialog/dlgpreferences.cpp +++ b/src/preferences/dialog/dlgpreferences.cpp @@ -36,6 +36,7 @@ #include "preferences/dialog/dlgprefbeats.h" #include "preferences/dialog/dlgprefkey.h" +#include "preferences/dialog/dlgprefosc.h" #include "preferences/dialog/dlgprefrecord.h" #include "preferences/dialog/dlgprefreplaygain.h" @@ -211,6 +212,12 @@ DlgPreferences::DlgPreferences( tr("Recording"), "ic_preferences_recording.svg"); + addPageWidget(PreferencesPage( + new DlgPrefOsc(this, m_pConfig), + new QTreeWidgetItem(contentsTreeWidget, QTreeWidgetItem::Type)), + tr("OSC"), + "ic_preferences_broadcast.svg"); + addPageWidget(PreferencesPage( new DlgPrefBeats(this, m_pConfig), new QTreeWidgetItem(contentsTreeWidget, QTreeWidgetItem::Type)), diff --git a/src/preferences/dialog/dlgprefosc.cpp b/src/preferences/dialog/dlgprefosc.cpp new file mode 100644 index 00000000000..2272248e953 --- /dev/null +++ b/src/preferences/dialog/dlgprefosc.cpp @@ -0,0 +1,237 @@ +#include "preferences/dialog/dlgprefosc.h" +#include "moc_dlgprefosc.cpp" + +DlgPrefOsc::DlgPrefOsc(QWidget* pParent, + UserSettingsPointer pConfig) + : DlgPreferencePage(pParent), + m_pConfig(pConfig) { + setupUi(this); + + // If OSC Receiver X is active -> OSC messages from Mixxx will be send to this receiver + connect(OscReceiver1ActiveCheckBox, + &QCheckBox::stateChanged, + this, + &DlgPrefOsc::slotToggleOscReceiver1Active); + + connect(OscReceiver2ActiveCheckBox, + &QCheckBox::stateChanged, + this, + &DlgPrefOsc::slotToggleOscReceiver1Active); + + connect(OscReceiver3ActiveCheckBox, + &QCheckBox::stateChanged, + this, + &DlgPrefOsc::slotToggleOscReceiver1Active); + + connect(OscReceiver4ActiveCheckBox, + &QCheckBox::stateChanged, + this, + &DlgPrefOsc::slotToggleOscReceiver1Active); + + connect(OscReceiver5ActiveCheckBox, + &QCheckBox::stateChanged, + this, + &DlgPrefOsc::slotToggleOscReceiver1Active); + + setScrollSafeGuardForAllInputWidgets(this); +} + +void DlgPrefOsc::slotUpdate() { + // Enable OSC-functions in Mixxx + OscEnabledCheckBox->setChecked(m_pConfig->getValue(ConfigKey("[OSC]", "OscEnabled"), false)); + + OscPortIn->setValue(m_pConfig->getValue(ConfigKey("[OSC]", "OscPortIn"), 9000)); + OscPortOut->setValue( + m_pConfig->getValue(ConfigKey("[OSC]", "OscPortOut"), 9001)); + // OscOutputBufferSize->setValue(m_pConfig->getValue(ConfigKey("[OSC]", "OscOutputBufferSize"), 1024)); + // OscIpMtuSize->setValue(m_pConfig->getValue(ConfigKey("[OSC]", "OscIpMtuSize"), 1536)); + + OscReceiver1ActiveCheckBox->setChecked(m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver1Active"), false)); + QString OscReceiver1IpTest = m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver1Ip")); + + OscReceiver1IpByte1->setValue( + m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver1IpByte1"), 1)); + OscReceiver1IpByte2->setValue( + m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver1IpByte2"), 1)); + OscReceiver1IpByte3->setValue( + m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver1IpByte3"), 1)); + OscReceiver1IpByte4->setValue( + m_pConfig->getValue(ConfigKey("[OSC]", "OscReceiver1IpByte4"), 1)); + + OscReceiver2ActiveCheckBox->setChecked(m_pConfig->getValue( + ConfigKey("[OSC]", "OscReceiver2Active"), false)); + OscReceiver2IpByte1->setValue(m_pConfig->getValue( + ConfigKey("[OSC]", "OscReceiver2IpByte1"), 1)); + OscReceiver2IpByte2->setValue(m_pConfig->getValue( + ConfigKey("[OSC]", "OscReceiver2IpByte2"), 1)); + OscReceiver2IpByte3->setValue(m_pConfig->getValue( + ConfigKey("[OSC]", "OscReceiver2IpByte3"), 1)); + OscReceiver2IpByte4->setValue(m_pConfig->getValue( + ConfigKey("[OSC]", "OscReceiver2IpByte4"), 1)); + + OscReceiver3ActiveCheckBox->setChecked(m_pConfig->getValue( + ConfigKey("[OSC]", "OscReceiver3Active"), false)); + OscReceiver3IpByte1->setValue(m_pConfig->getValue( + ConfigKey("[OSC]", "OscReceiver3IpByte1"), 1)); + OscReceiver3IpByte2->setValue(m_pConfig->getValue( + ConfigKey("[OSC]", "OscReceiver3IpByte2"), 1)); + OscReceiver3IpByte3->setValue(m_pConfig->getValue( + ConfigKey("[OSC]", "OscReceiver3IpByte3"), 1)); + OscReceiver3IpByte4->setValue(m_pConfig->getValue( + ConfigKey("[OSC]", "OscReceiver3IpByte4"), 1)); + + OscReceiver4ActiveCheckBox->setChecked(m_pConfig->getValue( + ConfigKey("[OSC]", "OscReceiver4Active"), false)); + OscReceiver4IpByte1->setValue(m_pConfig->getValue( + ConfigKey("[OSC]", "OscReceiver4IpByte1"), 1)); + OscReceiver4IpByte2->setValue(m_pConfig->getValue( + ConfigKey("[OSC]", "OscReceiver4IpByte2"), 1)); + OscReceiver4IpByte3->setValue(m_pConfig->getValue( + ConfigKey("[OSC]", "OscReceiver4IpByte3"), 1)); + OscReceiver4IpByte4->setValue(m_pConfig->getValue( + ConfigKey("[OSC]", "OscReceiver4IpByte4"), 1)); + + OscReceiver5ActiveCheckBox->setChecked(m_pConfig->getValue( + ConfigKey("[OSC]", "OscReceiver5Active"), false)); + OscReceiver5IpByte1->setValue(m_pConfig->getValue( + ConfigKey("[OSC]", "OscReceiver5IpByte1"), 1)); + OscReceiver5IpByte2->setValue(m_pConfig->getValue( + ConfigKey("[OSC]", "OscReceiver5IpByte2"), 1)); + OscReceiver5IpByte3->setValue(m_pConfig->getValue( + ConfigKey("[OSC]", "OscReceiver5IpByte3"), 1)); + OscReceiver5IpByte4->setValue(m_pConfig->getValue( + ConfigKey("[OSC]", "OscReceiver5IpByte4"), 1)); + +} + +void DlgPrefOsc::slotApply() { + m_pConfig->setValue(ConfigKey("[OSC]", "OscEnabled"), + OscEnabledCheckBox->isChecked()); + + m_pConfig->setValue(ConfigKey("[OSC]", "OscPortIn"), + OscPortIn->value()); + + m_pConfig->setValue(ConfigKey("[OSC]", "OscPortOut"), + OscPortOut->value()); + +// m_pConfig->setValue(ConfigKey("[OSC]", "OscOutputBufferSize"), +// OscOutputBufferSize->value()); + +// m_pConfig->setValue(ConfigKey("[OSC]", "OscIpMtuSize"), +// OscIpMtuSize->value()); + + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver1Active"), + OscReceiver1ActiveCheckBox->isChecked()); + + QString OscReceiverIp, OscReceiverIpByte1, OscReceiverIpByte2, OscReceiverIpByte3, OscReceiverIpByte4; + OscReceiverIpByte1 = QString("%1").arg(OscReceiver1IpByte1->value()); + OscReceiverIpByte2 = QString("%1").arg(OscReceiver1IpByte2->value()); + OscReceiverIpByte3 = QString("%1").arg(OscReceiver1IpByte3->value()); + OscReceiverIpByte4 = QString("%1").arg(OscReceiver1IpByte4->value()); + OscReceiverIp = QString("%1.%2.%3.%4").arg(OscReceiverIpByte1) + .arg(OscReceiverIpByte2).arg(OscReceiverIpByte3).arg(OscReceiverIpByte4); + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver1Ip"), OscReceiverIp); + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver1IpByte1"), + OscReceiver1IpByte1->value()); + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver1IpByte2"), + OscReceiver1IpByte2->value()); + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver1IpByte3"), + OscReceiver1IpByte3->value()); + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver1IpByte4"), + OscReceiver1IpByte4->value()); + + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver2Active"), + OscReceiver2ActiveCheckBox->isChecked()); + + OscReceiverIpByte1 = QString("%1").arg(OscReceiver2IpByte1->value()); + OscReceiverIpByte2 = QString("%1").arg(OscReceiver2IpByte2->value()); + OscReceiverIpByte3 = QString("%1").arg(OscReceiver2IpByte3->value()); + OscReceiverIpByte4 = QString("%1").arg(OscReceiver2IpByte4->value()); + OscReceiverIp = QString("%1.%2.%3.%4").arg(OscReceiverIpByte1).arg(OscReceiverIpByte2).arg(OscReceiverIpByte3).arg(OscReceiverIpByte4); + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver2Ip"), OscReceiverIp); + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver2IpByte1"), + OscReceiver2IpByte1->value()); + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver2IpByte2"), + OscReceiver2IpByte2->value()); + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver2IpByte3"), + OscReceiver2IpByte3->value()); + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver2IpByte4"), + OscReceiver2IpByte4->value()); + + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver3Active"), + OscReceiver3ActiveCheckBox->isChecked()); + + OscReceiverIpByte1 = QString("%1").arg(OscReceiver3IpByte1->value()); + OscReceiverIpByte2 = QString("%1").arg(OscReceiver3IpByte2->value()); + OscReceiverIpByte3 = QString("%1").arg(OscReceiver3IpByte3->value()); + OscReceiverIpByte4 = QString("%1").arg(OscReceiver3IpByte4->value()); + OscReceiverIp = QString("%1.%2.%3.%4").arg(OscReceiverIpByte1).arg(OscReceiverIpByte2).arg(OscReceiverIpByte3).arg(OscReceiverIpByte4); + + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver3Ip"), OscReceiverIp); + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver3IpByte1"), + OscReceiver3IpByte1->value()); + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver3IpByte2"), + OscReceiver3IpByte2->value()); + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver3IpByte3"), + OscReceiver3IpByte3->value()); + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver3IpByte4"), + OscReceiver3IpByte4->value()); + + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver4Active"), + OscReceiver4ActiveCheckBox->isChecked()); + + OscReceiverIpByte1 = QString("%1").arg(OscReceiver4IpByte1->value()); + OscReceiverIpByte2 = QString("%1").arg(OscReceiver4IpByte2->value()); + OscReceiverIpByte3 = QString("%1").arg(OscReceiver4IpByte3->value()); + OscReceiverIpByte4 = QString("%1").arg(OscReceiver4IpByte4->value()); + OscReceiverIp = QString("%1.%2.%3.%4").arg(OscReceiverIpByte1).arg(OscReceiverIpByte2).arg(OscReceiverIpByte3).arg(OscReceiverIpByte4); + + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver4Ip"), OscReceiverIp); + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver4IpByte1"), + OscReceiver4IpByte1->value()); + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver4IpByte2"), + OscReceiver4IpByte2->value()); + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver4IpByte3"), + OscReceiver4IpByte3->value()); + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver4IpByte4"), + OscReceiver4IpByte4->value()); + + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver5Active"), + OscReceiver5ActiveCheckBox->isChecked()); + + OscReceiverIpByte1 = QString("%1").arg(OscReceiver5IpByte1->value()); + OscReceiverIpByte2 = QString("%1").arg(OscReceiver5IpByte2->value()); + OscReceiverIpByte3 = QString("%1").arg(OscReceiver5IpByte3->value()); + OscReceiverIpByte4 = QString("%1").arg(OscReceiver5IpByte4->value()); + OscReceiverIp = QString("%1.%2.%3.%4").arg(OscReceiverIpByte1).arg(OscReceiverIpByte2).arg(OscReceiverIpByte3).arg(OscReceiverIpByte4); + + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver5Ip"), OscReceiverIp); + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver5IpByte1"), + OscReceiver5IpByte1->value()); + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver5IpByte2"), + OscReceiver5IpByte2->value()); + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver5IpByte3"), + OscReceiver5IpByte3->value()); + m_pConfig->setValue(ConfigKey("[OSC]", "OscReceiver5IpByte4"), + OscReceiver5IpByte4->value()); +} + +void DlgPrefOsc::slotResetToDefaults() { + OscEnabledCheckBox->setChecked(false); + OscReceiver1ActiveCheckBox->setChecked(false); + OscReceiver2ActiveCheckBox->setChecked(false); + OscReceiver3ActiveCheckBox->setChecked(false); + OscReceiver4ActiveCheckBox->setChecked(false); + OscReceiver5ActiveCheckBox->setChecked(false); +} + +void DlgPrefOsc::slotToggleOscReceiver1Active(int buttonState) { +} +void DlgPrefOsc::slotToggleOscReceiver2Active(int buttonState) { +} +void DlgPrefOsc::slotToggleOscReceiver3Active(int buttonState) { +} +void DlgPrefOsc::slotToggleOscReceiver4Active(int buttonState) { +} +void DlgPrefOsc::slotToggleOscReceiver5Active(int buttonState) { +} diff --git a/src/preferences/dialog/dlgprefosc.h b/src/preferences/dialog/dlgprefosc.h new file mode 100644 index 00000000000..a45a15b5118 --- /dev/null +++ b/src/preferences/dialog/dlgprefosc.h @@ -0,0 +1,30 @@ +#pragma once + +#include "preferences/dialog/dlgpreferencepage.h" +#include "preferences/dialog/ui_dlgprefoscdlg.h" +// #include "preferences/dialog/dlgprefoscdlg.ui" +#include "preferences/usersettings.h" + +class QWidget; + +class DlgPrefOsc : public DlgPreferencePage, public Ui::DlgPrefOscDlg { + Q_OBJECT + public: + DlgPrefOsc(QWidget* pParent, UserSettingsPointer pConfig); + + public slots: + void slotUpdate() override; + void slotApply() override; + void slotResetToDefaults() override; + + private slots: + void slotToggleOscReceiver1Active(int buttonState); + void slotToggleOscReceiver2Active(int buttonState); + void slotToggleOscReceiver3Active(int buttonState); + void slotToggleOscReceiver4Active(int buttonState); + void slotToggleOscReceiver5Active(int buttonState); + + private: + + UserSettingsPointer m_pConfig; +}; diff --git a/src/preferences/dialog/dlgprefoscdlg.ui b/src/preferences/dialog/dlgprefoscdlg.ui new file mode 100644 index 00000000000..cb4ed6dd60d --- /dev/null +++ b/src/preferences/dialog/dlgprefoscdlg.ui @@ -0,0 +1,1014 @@ + + + DlgPrefOscDlg + + + + 0 + 0 + 865 + 619 + + + + OSC Preferences + + + + + + + + + 16777215 + 200 + + + + + 0 + 150 + + + + Mixxx as an OSC-Receiver(Client) + + + + + 10 + 20 + 120 + 20 + + + + + 0 + 0 + + + + Uncheck, to ignore all played tracks. + + + OSC-In Port (UDP) + + + OscEnabledCheckBox + + + + + + 150 + 20 + 120 + 20 + + + + + 150 + 16777215 + + + + + 150 + 0 + + + + 1 + + + 9999 + + + + + + 0 + 80 + 809 + 13 + + + + + 16777215 + 150 + + + + + 0 + 150 + + + + Mixxx as an OSC-Sender (Server) + + + + + + 10 + 100 + 120 + 20 + + + + OSC-Out Port (UDP) + + + + + + 150 + 100 + 120 + 20 + + + + 1 + + + 9999 + + + + + + + + + 16777215 + 80 + + + + OSC Settings + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + Uncheck, to ignore all played tracks. + + + + + + + + + + Enable OSC in Mixxx + + + + + + + Mixxx must be restarted for changes to take effect + + + + + + + + + + OSC Receivers (Clients) (Machines that need to receive OSC-messages from Mixxx) + + + + + 10 + 30 + 50 + 20 + + + + Receiver + + + + + + 120 + 60 + 77 + 19 + + + + Active + + + 0 + + + + + + 220 + 60 + 80 + 20 + + + + 1 + + + 255 + + + + + + 220 + 30 + 60 + 20 + + + + 1st byte + + + + + + 10 + 60 + 80 + 13 + + + + Receiver 1 + + + + + + 120 + 30 + 50 + 20 + + + + Active + + + + + + 320 + 30 + 60 + 20 + + + + 2nd byte + + + + + + 420 + 30 + 60 + 20 + + + + 3rd byte + + + + + + 520 + 30 + 60 + 20 + + + + 4th byte + + + + + + 200 + 30 + 20 + 20 + + + + IP: + + + + + + 320 + 60 + 80 + 20 + + + + 0 + + + 255 + + + + + + 420 + 60 + 80 + 20 + + + + 0 + + + 255 + + + + + + 520 + 60 + 80 + 20 + + + + 1 + + + 255 + + + + + + 10 + 90 + 80 + 20 + + + + Receiver 2 + + + + + + 120 + 90 + 80 + 20 + + + + Active + + + + + + 10 + 120 + 80 + 20 + + + + Receiver 3 + + + + + + 120 + 120 + 80 + 20 + + + + Active + + + + + + 10 + 150 + 80 + 20 + + + + Receiver 4 + + + + + + 120 + 150 + 80 + 20 + + + + Active + + + + + + 10 + 180 + 80 + 20 + + + + Receiver 5 + + + + + + 120 + 180 + 80 + 20 + + + + Active + + + + + + 520 + 90 + 80 + 20 + + + + 1 + + + 255 + + + + + + 220 + 90 + 80 + 20 + + + + 1 + + + 255 + + + + + + 420 + 90 + 80 + 20 + + + + 0 + + + 255 + + + + + + 320 + 90 + 80 + 20 + + + + 0 + + + 255 + + + + + + 520 + 120 + 80 + 20 + + + + 1 + + + 255 + + + + + + 220 + 120 + 80 + 20 + + + + 1 + + + 255 + + + + + + 420 + 120 + 80 + 20 + + + + 0 + + + 255 + + + + + + 320 + 120 + 80 + 20 + + + + 0 + + + 255 + + + + + + 520 + 150 + 80 + 20 + + + + 1 + + + 255 + + + + + + 320 + 150 + 80 + 20 + + + + 0 + + + 255 + + + + + + 220 + 150 + 80 + 20 + + + + 1 + + + 255 + + + + + + 420 + 150 + 80 + 20 + + + + 0 + + + 255 + + + + + + 520 + 180 + 80 + 20 + + + + 1 + + + 255 + + + + + + 320 + 180 + 80 + 20 + + + + 0 + + + 255 + + + + + + 220 + 180 + 80 + 20 + + + + 1 + + + 255 + + + + + + 420 + 180 + 80 + 20 + + + + 0 + + + 255 + + + + + + 300 + 60 + 10 + 20 + + + + . + + + Qt::AlignCenter + + + + + + 400 + 60 + 10 + 20 + + + + . + + + Qt::AlignCenter + + + + + + 500 + 60 + 10 + 20 + + + + . + + + Qt::AlignCenter + + + + + + 400 + 90 + 10 + 20 + + + + . + + + Qt::AlignCenter + + + + + + 300 + 90 + 10 + 20 + + + + . + + + Qt::AlignCenter + + + + + + 500 + 90 + 10 + 20 + + + + . + + + Qt::AlignCenter + + + + + + 400 + 120 + 10 + 20 + + + + . + + + Qt::AlignCenter + + + + + + 300 + 120 + 10 + 20 + + + + . + + + Qt::AlignCenter + + + + + + 500 + 120 + 10 + 20 + + + + . + + + Qt::AlignCenter + + + + + + 400 + 150 + 10 + 20 + + + + . + + + Qt::AlignCenter + + + + + + 300 + 150 + 10 + 20 + + + + . + + + Qt::AlignCenter + + + + + + 500 + 150 + 10 + 20 + + + + . + + + Qt::AlignCenter + + + + + + 400 + 180 + 10 + 20 + + + + . + + + Qt::AlignCenter + + + + + + 300 + 180 + 10 + 20 + + + + . + + + Qt::AlignCenter + + + + + + 500 + 180 + 10 + 20 + + + + . + + + Qt::AlignCenter + + + + + + + + + + OscEnabledCheckBox + OscPortIn + OscPortOut + OscReceiver1ActiveCheckBox + OscReceiver1IpByte1 + OscReceiver1IpByte2 + OscReceiver1IpByte3 + OscReceiver1IpByte4 + OscReceiver2ActiveCheckBox + OscReceiver2IpByte1 + OscReceiver2IpByte2 + OscReceiver2IpByte3 + OscReceiver2IpByte4 + OscReceiver3ActiveCheckBox + OscReceiver3IpByte1 + OscReceiver3IpByte2 + OscReceiver3IpByte3 + OscReceiver3IpByte4 + OscReceiver4ActiveCheckBox + OscReceiver4IpByte1 + OscReceiver4IpByte2 + OscReceiver4IpByte3 + OscReceiver4IpByte4 + OscReceiver5ActiveCheckBox + OscReceiver5IpByte1 + OscReceiver5IpByte2 + OscReceiver5IpByte3 + OscReceiver5IpByte4 + + + + diff --git a/src/qml/qmlcontrolproxy.cpp b/src/qml/qmlcontrolproxy.cpp index 6a32e239c0a..a4e99565a42 100644 --- a/src/qml/qmlcontrolproxy.cpp +++ b/src/qml/qmlcontrolproxy.cpp @@ -78,7 +78,7 @@ void QmlControlProxy::setValue(double newValue) { return; } m_pControlProxy->set(newValue); - slotControlProxyValueChanged(newValue); + slotControlProxyValueChanged(newValue); } double QmlControlProxy::getValue() const { diff --git a/src/skin/legacy/legacyskinparser.cpp b/src/skin/legacy/legacyskinparser.cpp index 5a4dfe5b970..1c3db136894 100644 --- a/src/skin/legacy/legacyskinparser.cpp +++ b/src/skin/legacy/legacyskinparser.cpp @@ -74,6 +74,9 @@ #include "widget/wsplitter.h" #include "widget/wstarrating.h" #include "widget/wstatuslight.h" +#ifdef __STEM__ +#include "widget/wstemlabel.h" +#endif #include "widget/wtime.h" #include "widget/wtrackproperty.h" #include "widget/wtrackwidgetgroup.h" @@ -559,7 +562,13 @@ QList LegacySkinParser::parseNode(const QDomElement& node) { result = wrapWidget(parseLabelWidget(node)); } else if (nodeName == "Label") { result = wrapWidget(parseLabelWidget(node)); - } else if (nodeName == "Knob") { + } +#ifdef __STEM__ + else if (nodeName == "StemLabel") { + result = wrapWidget(parseStemLabelWidget(node)); + } +#endif + else if (nodeName == "Knob") { result = wrapWidget(parseStandardWidget(node)); } else if (nodeName == "KnobComposed") { result = wrapWidget(parseStandardWidget(node)); @@ -967,6 +976,55 @@ void LegacySkinParser::setupLabelWidget(const QDomElement& element, WLabel* pLab pLabel->Init(); } +#ifdef __STEM__ +QWidget* LegacySkinParser::parseStemLabelWidget(const QDomElement& element) { + WStemLabel* pLabel = new WStemLabel(m_pParent); + setupStemLabelWidget(element, pLabel); + + QString group = lookupNodeGroup(element); + BaseTrackPlayer* pPlayer = m_pPlayerManager->getPlayer(group); + if (!pPlayer) { + SKIN_WARNING(element, + *m_pContext, + QStringLiteral("No player found for group: %1").arg(group)); + return nullptr; + } + + connect(pPlayer, + &BaseTrackPlayer::newTrackLoaded, + pLabel, + &WStemLabel::slotTrackLoaded); + + connect(pPlayer, + &BaseTrackPlayer::trackUnloaded, + pLabel, + &WStemLabel::slotTrackUnloaded); + + TrackPointer pTrack = pPlayer->getLoadedTrack(); + if (pTrack) { + // Set the trackpoinnter to the already loaded track, + // needed at skin change + pLabel->slotTrackLoaded(pTrack); + } + + return pLabel; +} + +void LegacySkinParser::setupStemLabelWidget(const QDomElement& element, WStemLabel* pLabel) { + // NOTE(rryan): To support color schemes, the WWidget::setup() call must + // come first. This is because WLabel derivatives change the palette based + // on the node and setupWidget() will set the widget style. If the style is + // set before the palette is set then the custom palette will not take + // effect which breaks color scheme support. + pLabel->setup(element, *m_pContext); + commonWidgetSetup(element, pLabel); + pLabel->installEventFilter(m_pKeyboard); + pLabel->installEventFilter( + m_pControllerManager->getControllerLearningEventFilter()); + pLabel->Init(); +} +#endif + QWidget* LegacySkinParser::parseOverview(const QDomElement& node) { #ifdef MIXXX_USE_QML if (CmdlineArgs::Instance().isQml()) { @@ -1038,6 +1096,17 @@ QWidget* LegacySkinParser::parseVisual(const QDomElement& node) { &BaseTrackPlayer::loadingTrack, viewer, &WWaveformViewer::slotLoadingTrack); +#ifdef __STEM__ + QObject::connect(pPlayer, + &BaseTrackPlayer::selectedStems, + viewer, + &WWaveformViewer::slotSelectStem); +#endif + + QObject::connect(pPlayer, + &BaseTrackPlayer::trackUnloaded, + viewer, + &WWaveformViewer::slotTrackUnloaded); connect(viewer, &WWaveformViewer::trackDropped, diff --git a/src/skin/legacy/legacyskinparser.h b/src/skin/legacy/legacyskinparser.h index 11cb4639b71..1c5f733bf0f 100644 --- a/src/skin/legacy/legacyskinparser.h +++ b/src/skin/legacy/legacyskinparser.h @@ -22,6 +22,7 @@ class RecordingManager; class ControllerManager; class SkinContext; class WLabel; +class WStemLabel; class ControlObject; class LaunchImage; class WWidgetGroup; @@ -75,6 +76,12 @@ class LegacySkinParser : public QObject, public SkinParser { template QWidget* parseLabelWidget(const QDomElement& element); void setupLabelWidget(const QDomElement& element, WLabel* pLabel); + +#ifdef __STEM__ + QWidget* parseStemLabelWidget(const QDomElement& element); + void setupStemLabelWidget(const QDomElement& element, WStemLabel* pLabel); +#endif + QWidget* parseText(const QDomElement& node); QWidget* parseTrackProperty(const QDomElement& node); QWidget* parseStarRating(const QDomElement& node); diff --git a/src/skin/legacy/tooltips.cpp b/src/skin/legacy/tooltips.cpp index bc033a15ffc..0623c2a2df0 100644 --- a/src/skin/legacy/tooltips.cpp +++ b/src/skin/legacy/tooltips.cpp @@ -269,6 +269,9 @@ void Tooltips::addStandardTooltips() { add("show_beatgrid_controls") << tr("Show/hide the beatgrid controls section"); + add("show_stem_controls") + << tr("Show/hide the stem mixing controls section"); + add("show_library") << tr("Show Library") << tr("Show or hide the track library."); @@ -1150,6 +1153,22 @@ void Tooltips::addStandardTooltips() { << tr("Toggle the current effect.") << eqKillLatch; + // Stem Channel Controls + add("StemLabel") + << tr("Stem Label") + << tr("Name of the stem stored in the stem file") + << tr("Text is displayed in the stem color stored in the stem file") + << tr("this stem color is also used for the waveform of this stem"); + + add("StemMuteButton") + << tr("Stem Mute") + << tr("Toggle the stem mute/unmuted"); + + add("StemVolumeKnob") + << tr("Stem Volume Knob") + << tr("Adjusts the volume of the stem") + << resetWithRightAndDoubleClick; + // Equalizer Rack Controls add("EqualizerRack_effect_parameter") << tr("Equalizer Parameter") diff --git a/src/sources/audiosource.h b/src/sources/audiosource.h index 8c007848d23..705a71be51c 100644 --- a/src/sources/audiosource.h +++ b/src/sources/audiosource.h @@ -193,7 +193,24 @@ class AudioSource : public UrlResource, public virtual /*implements*/ IAudioSour // Parameters for opening audio sources class OpenParams { public: +#ifdef __STEM__ + OpenParams() + : m_signalInfo(), + m_stemMask(mixxx::kNoStemSelected) { + } + + OpenParams( + audio::ChannelCount channelCount, + audio::SampleRate sampleRate, + uint stemMask = mixxx::kNoStemSelected) + : m_signalInfo( + channelCount, + sampleRate), + m_stemMask(stemMask) { + } +#else OpenParams() = default; + OpenParams( audio::ChannelCount channelCount, audio::SampleRate sampleRate) @@ -201,16 +218,33 @@ class AudioSource : public UrlResource, public virtual /*implements*/ IAudioSour channelCount, sampleRate) { } +#endif const audio::SignalInfo& getSignalInfo() const { return m_signalInfo; } +#ifdef __STEM__ + uint stemMask() const { + return m_stemMask; + } +#endif + void setChannelCount( audio::ChannelCount channelCount) { m_signalInfo.setChannelCount(channelCount); } +#ifdef __STEM__ + void setStemMask( + uint stemMask) { + VERIFY_OR_DEBUG_ASSERT(stemMask <= 2 << mixxx::kMaxSupportedStems) { + return; + } + m_stemMask = stemMask; + } +#endif + void setSampleRate( audio::SampleRate sampleRate) { m_signalInfo.setSampleRate(sampleRate); @@ -218,6 +252,9 @@ class AudioSource : public UrlResource, public virtual /*implements*/ IAudioSour private: audio::SignalInfo m_signalInfo; +#ifdef __STEM__ + uint m_stemMask; +#endif }; // Opens the AudioSource for reading audio data. diff --git a/src/sources/soundsourcestem.cpp b/src/sources/soundsourcestem.cpp index 76105135f2d..5e6ec817398 100644 --- a/src/sources/soundsourcestem.cpp +++ b/src/sources/soundsourcestem.cpp @@ -323,6 +323,12 @@ SoundSource::OpenResult SoundSourceSTEM::tryOpen( AVStream* firstAudioStream = nullptr; int stemCount = 0; + uint selectedStemMask = params.stemMask(); + VERIFY_OR_DEBUG_ASSERT(selectedStemMask <= 2 << mixxx::kMaxSupportedStems) { + kLogger.warning().noquote() + << "Invalid selected stem mask" << selectedStemMask; + return OpenResult::Failed; + } OpenParams stemParam = params; stemParam.setChannelCount(mixxx::audio::ChannelCount::stereo()); for (unsigned int streamIdx = 0; streamIdx < pavInputFormatContext->nb_streams; streamIdx++) { @@ -365,6 +371,11 @@ SoundSource::OpenResult SoundSourceSTEM::tryOpen( stemCount++; } + // StemIdx is equal to StreamIdx -1 (the main mix) + if (selectedStemMask && !(selectedStemMask & 1 << (streamIdx - 1))) { + continue; + } + m_pStereoStreams.emplace_back(std::make_unique(getUrl(), streamIdx)); if (m_pStereoStreams.back()->open(OpenMode::Strict /*Unused*/, stemParam) != OpenResult::Succeeded) { @@ -380,7 +391,16 @@ SoundSource::OpenResult SoundSourceSTEM::tryOpen( return OpenResult::Failed; } - if (params.getSignalInfo().getChannelCount() == mixxx::audio::ChannelCount::stereo()) { + VERIFY_OR_DEBUG_ASSERT(!m_pStereoStreams.empty()) { + kLogger.warning().noquote() + << "no stem track were selected"; + close(); + return OpenResult::Failed; + } + + if (params.getSignalInfo().getChannelCount() == + mixxx::audio::ChannelCount::stereo() || + selectedStemMask) { // Requesting a stereo stream (used for samples and preview decks) m_requestedChannelCount = mixxx::audio::ChannelCount::stereo(); initChannelCountOnce(mixxx::audio::ChannelCount::stereo()); @@ -434,12 +454,17 @@ ReadableSampleFrames SoundSourceSTEM::readSampleFramesClamped( int stemCount = m_pStereoStreams.size(); CSAMPLE* pBuffer = globalSampleFrames.writableData(); - if (m_requestedChannelCount == mixxx::audio::ChannelCount::stereo()) { + if (m_requestedChannelCount == mixxx::audio::ChannelCount::stereo() && stemCount != 1) { SampleUtil::clear(pBuffer, globalSampleFrames.writableLength()); } else { DEBUG_ASSERT(stemSampleLength * stemCount == globalSampleFrames.writableLength()); } + if (stemCount == 1) { + m_pStereoStreams[0]->readSampleFrames(globalSampleFrames); + return read; + } + for (int streamIdx = 0; streamIdx < stemCount; streamIdx++) { WritableSampleFrames currentStemFrame = WritableSampleFrames( globalSampleFrames.frameIndexRange(), diff --git a/src/test/autodjprocessor_test.cpp b/src/test/autodjprocessor_test.cpp index 7d4c3c5f7cc..53b59e17a66 100644 --- a/src/test/autodjprocessor_test.cpp +++ b/src/test/autodjprocessor_test.cpp @@ -94,7 +94,11 @@ class FakeDeck : public BaseTrackPlayer { // a success or failure signal. To simulate a load success, call // fakeTrackLoadedEvent. To simulate a failure, call // fakeTrackLoadFailedEvent. - void slotLoadTrack(TrackPointer pTrack, bool bPlay) override { + void slotLoadTrack(TrackPointer pTrack, +#ifdef __STEM__ + uint, +#endif + bool bPlay) override { loadedTrack = pTrack; samplerate.set(pTrack->getSampleRate()); play.set(bPlay); @@ -263,7 +267,11 @@ TEST_F(AutoDJProcessorTest, FullIntroOutro_LongerIntro) { // Pretend that track is 1 minute and 40 seconds long. pTrack->setDuration(100); // Load track and mark it playing. - deck1.slotLoadTrack(pTrack, true); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + true); // Indicate the track loaded successfully. deck1.fakeTrackLoadedEvent(pTrack); @@ -280,7 +288,11 @@ TEST_F(AutoDJProcessorTest, FullIntroOutro_LongerIntro) { EXPECT_EQ(AutoDJProcessor::ADJ_IDLE, pProcessor->getState()); // Pretend the track load succeeds. - deck2.slotLoadTrack(pTrack, false); + deck2.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); // Set intro + outro cues. Outro is 10 seconds long; intro is 30 seconds. const double kSamplesPerSecond = kChannelCount * pTrack->getSampleRate(); @@ -337,7 +349,11 @@ TEST_F(AutoDJProcessorTest, FullIntroOutro_LongerOutro) { // Pretend that track is 1 minute and 40 seconds long. pTrack->setDuration(100); // Load track and mark it playing. - deck1.slotLoadTrack(pTrack, true); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + true); // Indicate the track loaded successfully. deck1.fakeTrackLoadedEvent(pTrack); @@ -354,7 +370,11 @@ TEST_F(AutoDJProcessorTest, FullIntroOutro_LongerOutro) { EXPECT_EQ(AutoDJProcessor::ADJ_IDLE, pProcessor->getState()); // Pretend the track load succeeds. - deck2.slotLoadTrack(pTrack, false); + deck2.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); // Set intro + outro cues. Outro is 20 seconds long; intro is 10 seconds. const double kSamplesPerSecond = kChannelCount * pTrack->getSampleRate(); @@ -417,7 +437,11 @@ TEST_F(AutoDJProcessorTest, FadeAtOutroStart_LongerIntro) { // Pretend that track is 1 minute and 40 seconds long. pTrack->setDuration(100); // Load track and mark it playing. - deck1.slotLoadTrack(pTrack, true); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + true); // Indicate the track loaded successfully. deck1.fakeTrackLoadedEvent(pTrack); @@ -434,7 +458,11 @@ TEST_F(AutoDJProcessorTest, FadeAtOutroStart_LongerIntro) { EXPECT_EQ(AutoDJProcessor::ADJ_IDLE, pProcessor->getState()); // Pretend the track load succeeds. - deck2.slotLoadTrack(pTrack, false); + deck2.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); // Set intro + outro cues. Outro is 10 seconds long; intro is 20 seconds. const double kSamplesPerSecond = kChannelCount * pTrack->getSampleRate(); @@ -493,7 +521,11 @@ TEST_F(AutoDJProcessorTest, FadeAtOutroStart_LongerOutro) { // Pretend that track is 1 minute and 40 seconds long. pTrack->setDuration(100); // Load track and mark it playing. - deck1.slotLoadTrack(pTrack, true); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + true); // Indicate the track loaded successfully. deck1.fakeTrackLoadedEvent(pTrack); @@ -510,7 +542,11 @@ TEST_F(AutoDJProcessorTest, FadeAtOutroStart_LongerOutro) { EXPECT_EQ(AutoDJProcessor::ADJ_IDLE, pProcessor->getState()); // Pretend the track load succeeds. - deck2.slotLoadTrack(pTrack, false); + deck2.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); // Set intro + outro cues. Outro is 20 seconds long; intro is 10 seconds. const double kSamplesPerSecond = kChannelCount * pTrack->getSampleRate(); @@ -640,7 +676,11 @@ TEST_F(AutoDJProcessorTest, EnabledSuccess_DecksStopped) { // Load the track and mark it playing (as the loadTrackToPlayer signal would // have connected to this eventually). TrackPointer pTrack = trackCollectionManager()->getTrackById(testId); - deck1.slotLoadTrack(pTrack, true); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + true); // Signal that the request to load pTrack succeeded. deck1.fakeTrackLoadedEvent(pTrack); @@ -686,7 +726,11 @@ TEST_F(AutoDJProcessorTest, EnabledSuccess_DecksStopped_TrackLoadFails) { // Load the track and mark it playing (as the loadTrackToPlayer signal would // have connected to this eventually). TrackPointer pTrack(newTestTrack(testId)); - deck1.slotLoadTrack(pTrack, true); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + true); // Signal that the request to load pTrack failed. deck1.fakeTrackLoadFailedEvent(pTrack); @@ -704,7 +748,11 @@ TEST_F(AutoDJProcessorTest, EnabledSuccess_DecksStopped_TrackLoadFails) { EXPECT_DOUBLE_EQ(0.0, deck2.play.get()); // Now pretend that the follow-up load request succeeded. - deck1.slotLoadTrack(pTrack, true); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + true); deck1.fakeTrackLoadedEvent(pTrack); // Expect that we will receive a load call for [Channel2] after we get the @@ -752,7 +800,11 @@ TEST_F(AutoDJProcessorTest, EnabledSuccess_DecksStopped_TrackLoadFailsRightDeck) // Load the track and mark it playing (as the loadTrackToPlayer signal would // have connected to this eventually). TrackPointer pTrack(newTestTrack(testId)); - deck1.slotLoadTrack(pTrack, true); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + true); // Signal that the request to load pTrack to deck1 succeeded. deck1.fakeTrackLoadedEvent(pTrack); @@ -776,14 +828,22 @@ TEST_F(AutoDJProcessorTest, EnabledSuccess_DecksStopped_TrackLoadFailsRightDeck) EXPECT_CALL(*pProcessor, emitLoadTrackToPlayer(_, QString("[Channel2]"), false)); // Now pretend that the deck2 load request failed. - deck2.slotLoadTrack(pTrack, false); + deck2.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); deck2.fakeTrackLoadFailedEvent(pTrack); // Check that we are still in ADJ_IDLE mode. EXPECT_EQ(AutoDJProcessor::ADJ_IDLE, pProcessor->getState()); // Pretend the deck2 load request succeeded. - deck2.slotLoadTrack(pTrack, false); + deck2.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); deck2.fakeTrackLoadedEvent(pTrack); // Check that we are still in ADJ_IDLE mode and the left deck is playing. @@ -800,7 +860,11 @@ TEST_F(AutoDJProcessorTest, EnabledSuccess_PlayingDeck1) { // Pretend a track is playing on deck 1. TrackPointer pTrack = newTestTrack(); // Load track and mark it playing. - deck1.slotLoadTrack(pTrack, true); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + true); // Indicate the track loaded successfully. deck1.fakeTrackLoadedEvent(pTrack); @@ -822,7 +886,11 @@ TEST_F(AutoDJProcessorTest, EnabledSuccess_PlayingDeck1) { EXPECT_DOUBLE_EQ(0.0, deck2.play.get()); // Pretend the track load succeeds. - deck2.slotLoadTrack(pTrack, false); + deck2.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); deck2.fakeTrackLoadedEvent(pTrack); // No change to the mode, crossfader or play states. @@ -839,7 +907,11 @@ TEST_F(AutoDJProcessorTest, EnabledSuccess_PlayingDeck1_TrackLoadFailed) { // Pretend a track is playing on deck 1. TrackPointer pTrack = newTestTrack(); // Load track and mark it playing. - deck1.slotLoadTrack(pTrack, true); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + true); // Indicate the track loaded successfully. deck1.fakeTrackLoadedEvent(pTrack); @@ -868,7 +940,11 @@ TEST_F(AutoDJProcessorTest, EnabledSuccess_PlayingDeck1_TrackLoadFailed) { EXPECT_CALL(*pProcessor, emitLoadTrackToPlayer(_, QString("[Channel2]"), false)); // Pretend the track load fails. - deck2.slotLoadTrack(pTrack, false); + deck2.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); deck2.fakeTrackLoadFailedEvent(pTrack); // No change to the mode, crossfader, or play states. @@ -878,7 +954,11 @@ TEST_F(AutoDJProcessorTest, EnabledSuccess_PlayingDeck1_TrackLoadFailed) { EXPECT_DOUBLE_EQ(0.0, deck2.play.get()); // Pretend the track load succeeds. - deck2.slotLoadTrack(pTrack, false); + deck2.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); deck2.fakeTrackLoadedEvent(pTrack); // No change to the mode, crossfader, or play states. @@ -895,7 +975,11 @@ TEST_F(AutoDJProcessorTest, EnabledSuccess_PlayingDeck2) { // Pretend a track is playing on deck 2. TrackPointer pTrack = newTestTrack(); // Load track and mark it playing. - deck2.slotLoadTrack(pTrack, true); + deck2.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + true); // Indicate the track loaded successfully. deck2.fakeTrackLoadedEvent(pTrack); @@ -918,7 +1002,11 @@ TEST_F(AutoDJProcessorTest, EnabledSuccess_PlayingDeck2) { EXPECT_DOUBLE_EQ(1.0, deck2.play.get()); // Pretend the track load succeeds. - deck1.slotLoadTrack(pTrack, false); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); deck1.fakeTrackLoadedEvent(pTrack); // No change to the mode, crossfader or play states. @@ -935,7 +1023,11 @@ TEST_F(AutoDJProcessorTest, EnabledSuccess_PlayingDeck2_TrackLoadFailed) { // Pretend a track is playing on deck 2. TrackPointer pTrack = newTestTrack(); // Load track and mark it playing. - deck2.slotLoadTrack(pTrack, true); + deck2.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + true); // Indicate the track loaded successfully. deck2.fakeTrackLoadedEvent(pTrack); @@ -964,7 +1056,11 @@ TEST_F(AutoDJProcessorTest, EnabledSuccess_PlayingDeck2_TrackLoadFailed) { EXPECT_CALL(*pProcessor, emitLoadTrackToPlayer(_, QString("[Channel1]"), false)); // Pretend the track load fails. - deck1.slotLoadTrack(pTrack, false); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); deck1.fakeTrackLoadFailedEvent(pTrack); // No change to the mode, crossfader, or play states. @@ -974,7 +1070,11 @@ TEST_F(AutoDJProcessorTest, EnabledSuccess_PlayingDeck2_TrackLoadFailed) { EXPECT_DOUBLE_EQ(1.0, deck2.play.get()); // Pretend the track load succeeds. - deck1.slotLoadTrack(pTrack, false); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); deck1.fakeTrackLoadedEvent(pTrack); // No change to the mode, crossfader, or play states. @@ -1009,7 +1109,11 @@ TEST_F(AutoDJProcessorTest, FadeToDeck1_LoadOnDeck2_TrackLoadSuccess) { // Pretend a track is playing on deck 2. TrackPointer pTrack = newTestTrack(); // Load track and mark it playing. - deck2.slotLoadTrack(pTrack, true); + deck2.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + true); // Indicate the track loaded successfully. deck2.fakeTrackLoadedEvent(pTrack); @@ -1034,7 +1138,11 @@ TEST_F(AutoDJProcessorTest, FadeToDeck1_LoadOnDeck2_TrackLoadSuccess) { EXPECT_DOUBLE_EQ(1.0, deck2.play.get()); // Pretend the track load succeeds. - deck1.slotLoadTrack(pTrack, false); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); deck1.fakeTrackLoadedEvent(pTrack); // No change to the mode, crossfader or play states. @@ -1077,7 +1185,11 @@ TEST_F(AutoDJProcessorTest, FadeToDeck1_LoadOnDeck2_TrackLoadSuccess) { EXPECT_DOUBLE_EQ(0.0, deck2.play.get()); // Pretend the track load request succeeds. - deck2.slotLoadTrack(pTrack, false); + deck2.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); deck2.fakeTrackLoadedEvent(pTrack); // No change to the mode, crossfader, or play states. @@ -1096,7 +1208,11 @@ TEST_F(AutoDJProcessorTest, FadeToDeck1_LoadOnDeck2_TrackLoadFailed) { // Pretend a track is playing on deck 2. TrackPointer pTrack = newTestTrack(); // Load track and mark it playing. - deck2.slotLoadTrack(pTrack, true); + deck2.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + true); // Indicate the track loaded successfully. deck2.fakeTrackLoadedEvent(pTrack); @@ -1123,7 +1239,11 @@ TEST_F(AutoDJProcessorTest, FadeToDeck1_LoadOnDeck2_TrackLoadFailed) { EXPECT_DOUBLE_EQ(1.0, deck2.play.get()); // Pretend the track load succeeds. - deck1.slotLoadTrack(pTrack, false); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); deck1.fakeTrackLoadedEvent(pTrack); // No change to the mode, crossfader or play states. @@ -1170,7 +1290,11 @@ TEST_F(AutoDJProcessorTest, FadeToDeck1_LoadOnDeck2_TrackLoadFailed) { EXPECT_CALL(*pProcessor, emitLoadTrackToPlayer(_, QString("[Channel2]"), false)); // Pretend the track load request fails. - deck2.slotLoadTrack(pTrack, false); + deck2.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); deck2.fakeTrackLoadFailedEvent(pTrack); // No change to the mode, crossfader, or play states. @@ -1180,7 +1304,11 @@ TEST_F(AutoDJProcessorTest, FadeToDeck1_LoadOnDeck2_TrackLoadFailed) { EXPECT_DOUBLE_EQ(0.0, deck2.play.get()); // Pretend the second track load request succeeds. - deck2.slotLoadTrack(pTrack, false); + deck2.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); deck2.fakeTrackLoadedEvent(pTrack); // No change to the mode, crossfader, or play states. @@ -1199,7 +1327,11 @@ TEST_F(AutoDJProcessorTest, FadeToDeck2_LoadOnDeck1_TrackLoadSuccess) { // Pretend a track is playing on deck 1. TrackPointer pTrack = newTestTrack(); // Load track and mark it playing. - deck1.slotLoadTrack(pTrack, true); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + true); // Indicate the track loaded successfully. deck1.fakeTrackLoadedEvent(pTrack); @@ -1224,7 +1356,11 @@ TEST_F(AutoDJProcessorTest, FadeToDeck2_LoadOnDeck1_TrackLoadSuccess) { EXPECT_DOUBLE_EQ(0.0, deck2.play.get()); // Pretend the track load succeeds. - deck2.slotLoadTrack(pTrack, false); + deck2.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); deck2.fakeTrackLoadedEvent(pTrack); // No change to the mode, crossfader or play states. @@ -1267,7 +1403,11 @@ TEST_F(AutoDJProcessorTest, FadeToDeck2_LoadOnDeck1_TrackLoadSuccess) { EXPECT_DOUBLE_EQ(1.0, deck2.play.get()); // Pretend the track load request succeeds. - deck1.slotLoadTrack(pTrack, false); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); deck1.fakeTrackLoadedEvent(pTrack); // No change to the mode, crossfader, or play states. @@ -1286,7 +1426,11 @@ TEST_F(AutoDJProcessorTest, FadeToDeck2_LoadOnDeck1_TrackLoadFailed) { // Pretend a track is playing on deck 1. TrackPointer pTrack = newTestTrack(); // Load track and mark it playing. - deck1.slotLoadTrack(pTrack, true); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + true); // Indicate the track loaded successfully. deck1.fakeTrackLoadedEvent(pTrack); @@ -1313,7 +1457,11 @@ TEST_F(AutoDJProcessorTest, FadeToDeck2_LoadOnDeck1_TrackLoadFailed) { EXPECT_DOUBLE_EQ(0.0, deck2.play.get()); // Pretend the track load succeeds. - deck2.slotLoadTrack(pTrack, false); + deck2.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); deck2.fakeTrackLoadedEvent(pTrack); // No change to the mode, crossfader or play states. @@ -1360,7 +1508,11 @@ TEST_F(AutoDJProcessorTest, FadeToDeck2_LoadOnDeck1_TrackLoadFailed) { EXPECT_CALL(*pProcessor, emitLoadTrackToPlayer(_, QString("[Channel1]"), false)); // Pretend the track load request fails. - deck1.slotLoadTrack(pTrack, false); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); deck1.fakeTrackLoadFailedEvent(pTrack); // No change to the mode, crossfader, or play states. @@ -1370,7 +1522,11 @@ TEST_F(AutoDJProcessorTest, FadeToDeck2_LoadOnDeck1_TrackLoadFailed) { EXPECT_DOUBLE_EQ(1.0, deck2.play.get()); // Pretend the track load request succeeds. - deck1.slotLoadTrack(pTrack, false); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); deck1.fakeTrackLoadedEvent(pTrack); // No change to the mode, crossfader, or play states. @@ -1392,7 +1548,11 @@ TEST_F(AutoDJProcessorTest, FadeToDeck2_Long_Transition) { // Pretend a track is playing on deck 1. TrackPointer pTrack = newTestTrack(); // Load track and mark it playing. - deck1.slotLoadTrack(pTrack, true); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + true); // Indicate the track loaded successfully. deck1.fakeTrackLoadedEvent(pTrack); @@ -1409,7 +1569,11 @@ TEST_F(AutoDJProcessorTest, FadeToDeck2_Long_Transition) { EXPECT_EQ(AutoDJProcessor::ADJ_IDLE, pProcessor->getState()); // Pretend the track load succeeds. - deck2.slotLoadTrack(pTrack, false); + deck2.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); deck2.fakeTrackLoadedEvent(pTrack); // Set a long transition time @@ -1481,7 +1645,11 @@ TEST_F(AutoDJProcessorTest, FadeToDeck2_Pause_Transition) { // Pretend that track is 2 minutes long. pTrack->setDuration(120); // Load track and mark it playing. - deck1.slotLoadTrack(pTrack, true); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + true); // Indicate the track loaded successfully. deck1.fakeTrackLoadedEvent(pTrack); @@ -1497,7 +1665,11 @@ TEST_F(AutoDJProcessorTest, FadeToDeck2_Pause_Transition) { EXPECT_EQ(AutoDJProcessor::ADJ_IDLE, pProcessor->getState()); // Pretend the track load succeeds. - deck2.slotLoadTrack(pTrack, false); + deck2.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); deck2.fakeTrackLoadedEvent(pTrack); // The track should have been cued at 0.0. @@ -1511,7 +1683,11 @@ TEST_F(AutoDJProcessorTest, FadeToDeck2_Pause_Transition) { EXPECT_DOUBLE_EQ(0.0, deck2.playposition.get()); // Pretend the track load succeeds. - deck2.slotLoadTrack(pTrack, false); + deck2.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); deck2.fakeTrackLoadedEvent(pTrack); // The newly loaded track should have been seeked back by the trackSamples of transition. @@ -1561,7 +1737,11 @@ TEST_F(AutoDJProcessorTest, FadeToDeck2_SeekEnd) { // Pretend a track is playing on deck 1. TrackPointer pTrack = newTestTrack(); // Load track and mark it playing. - deck1.slotLoadTrack(pTrack, true); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + true); // Indicate the track loaded successfully. deck1.fakeTrackLoadedEvent(pTrack); @@ -1578,7 +1758,11 @@ TEST_F(AutoDJProcessorTest, FadeToDeck2_SeekEnd) { EXPECT_EQ(AutoDJProcessor::ADJ_IDLE, pProcessor->getState()); // Pretend the track load succeeds. - deck2.slotLoadTrack(pTrack, false); + deck2.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); deck2.fakeTrackLoadedEvent(pTrack); // No change to the mode, crossfader or play states. @@ -1613,7 +1797,11 @@ TEST_F(AutoDJProcessorTest, FadeToDeck2_SeekBeforeTransition) { // Pretend a track is playing on deck 1. TrackPointer pTrack = newTestTrack(); // Load track and mark it playing. - deck1.slotLoadTrack(pTrack, true); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + true); // Indicate the track loaded successfully. deck1.fakeTrackLoadedEvent(pTrack); @@ -1630,7 +1818,11 @@ TEST_F(AutoDJProcessorTest, FadeToDeck2_SeekBeforeTransition) { EXPECT_EQ(AutoDJProcessor::ADJ_IDLE, pProcessor->getState()); // Pretend the track load succeeds. - deck2.slotLoadTrack(pTrack, false); + deck2.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); deck2.fakeTrackLoadedEvent(pTrack); // No change to the mode, crossfader or play states. @@ -1689,7 +1881,11 @@ TEST_F(AutoDJProcessorTest, TrackZeroLength) { // have connected to this eventually). TrackPointer pTrack(newTestTrack(testId)); pTrack->setDuration(0); - deck1.slotLoadTrack(pTrack, true); + deck1.slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + true); // Expect that the track is rejected an a new one is loaded // Signal that the request to load pTrack succeeded. diff --git a/src/test/cuecontrol_test.cpp b/src/test/cuecontrol_test.cpp index 5666a9bf284..d4aa2aabb7a 100644 --- a/src/test/cuecontrol_test.cpp +++ b/src/test/cuecontrol_test.cpp @@ -48,7 +48,11 @@ class CueControlTest : public BaseSignalPathTest { } void unloadTrack() { - m_pMixerDeck1->slotLoadTrack(TrackPointer(), false); + m_pMixerDeck1->slotLoadTrack(TrackPointer(), +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); } mixxx::audio::FramePos getCurrentFramePos() { diff --git a/src/test/hotcuecontrol_test.cpp b/src/test/hotcuecontrol_test.cpp index 73eca9f01fe..303a00ad036 100644 --- a/src/test/hotcuecontrol_test.cpp +++ b/src/test/hotcuecontrol_test.cpp @@ -79,7 +79,11 @@ class HotcueControlTest : public BaseSignalPathTest { } void unloadTrack() { - m_pMixerDeck1->slotLoadTrack(TrackPointer(), false); + m_pMixerDeck1->slotLoadTrack(TrackPointer(), +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); } mixxx::audio::FramePos currentFramePosition() { diff --git a/src/test/playermanagertest.cpp b/src/test/playermanagertest.cpp index fbd28e0e91b..27a9aa9c351 100644 --- a/src/test/playermanagertest.cpp +++ b/src/test/playermanagertest.cpp @@ -149,7 +149,11 @@ TEST_F(PlayerManagerTest, UnEjectTest) { ASSERT_NE(nullptr, pTrack1); TrackId testId1 = pTrack1->getId(); ASSERT_TRUE(testId1.isValid()); - deck1->slotLoadTrack(pTrack1, false); + deck1->slotLoadTrack(pTrack1, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); ASSERT_NE(nullptr, deck1->getLoadedTrack()); m_pEngine->process(1024); @@ -162,7 +166,11 @@ TEST_F(PlayerManagerTest, UnEjectTest) { // Load another track. TrackPointer pTrack2 = getOrAddTrackByLocation(getTestDir().filePath(kTrackLocationTest2)); ASSERT_NE(nullptr, pTrack2); - deck1->slotLoadTrack(pTrack2, false); + deck1->slotLoadTrack(pTrack2, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); // Ejecting in an empty deck loads the last-ejected track. auto deck2 = m_pPlayerManager->getDeck(2); @@ -183,7 +191,11 @@ TEST_F(PlayerManagerTest, UnEjectReplaceTrackTest) { ASSERT_NE(nullptr, pTrack1); TrackId testId1 = pTrack1->getId(); ASSERT_TRUE(testId1.isValid()); - deck1->slotLoadTrack(pTrack1, false); + deck1->slotLoadTrack(pTrack1, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); ASSERT_NE(nullptr, deck1->getLoadedTrack()); m_pEngine->process(1024); @@ -192,7 +204,11 @@ TEST_F(PlayerManagerTest, UnEjectReplaceTrackTest) { // Load another track, replacing the first, causing it to be unloaded. TrackPointer pTrack2 = getOrAddTrackByLocation(getTestDir().filePath(kTrackLocationTest2)); ASSERT_NE(nullptr, pTrack2); - deck1->slotLoadTrack(pTrack2, false); + deck1->slotLoadTrack(pTrack2, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); m_pEngine->process(1024); waitForTrackToBeLoaded(deck1); @@ -228,7 +244,11 @@ TEST_F(PlayerManagerTest, UnReplaceTest) { ASSERT_NE(nullptr, pTrack1); TrackId testId1 = pTrack1->getId(); ASSERT_TRUE(testId1.isValid()); - deck1->slotLoadTrack(pTrack1, false); + deck1->slotLoadTrack(pTrack1, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); m_pEngine->process(1024); waitForTrackToBeLoaded(deck1); ASSERT_NE(nullptr, deck1->getLoadedTrack()); @@ -236,7 +256,11 @@ TEST_F(PlayerManagerTest, UnReplaceTest) { // Load another track. TrackPointer pTrack2 = getOrAddTrackByLocation(getTestDir().filePath(kTrackLocationTest2)); ASSERT_NE(nullptr, pTrack2); - deck1->slotLoadTrack(pTrack2, false); + deck1->slotLoadTrack(pTrack2, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); m_pEngine->process(1024); waitForTrackToBeLoaded(deck1); ASSERT_NE(nullptr, deck1->getLoadedTrack()); diff --git a/src/test/signalpathtest.h b/src/test/signalpathtest.h index 4cf38a46532..902bbbbdb6c 100644 --- a/src/test/signalpathtest.h +++ b/src/test/signalpathtest.h @@ -169,7 +169,11 @@ class BaseSignalPathTest : public MixxxTest, SoundSourceProviderRegistration { if (pEngineDeck->getEngineBuffer()->isTrackLoaded()) { pEngineDeck->getEngineBuffer()->ejectTrack(); } - pDeck->slotLoadTrack(pTrack, false); + pDeck->slotLoadTrack(pTrack, +#ifdef __STEM__ + mixxx::kNoStemSelected, +#endif + false); // Wait for the track to load. ProcessBuffer(); diff --git a/src/track/track.h b/src/track/track.h index f053b6b2166..0d181d3ab00 100644 --- a/src/track/track.h +++ b/src/track/track.h @@ -435,6 +435,16 @@ class Track : public QObject { void setAudioProperties( const mixxx::audio::StreamInfo& streamInfo); + // Information about the actual properties of the + // audio stream is only available after opening the + // source at least once. On this occasion the metadata + // stream info of the track need to be updated to reflect + // these values. + bool hasStreamInfoFromSource() const { + const auto locked = lockMutex(&m_qMutex); + return m_record.hasStreamInfoFromSource(); + } + signals: void artistChanged(const QString&); void titleChanged(const QString&); @@ -568,16 +578,6 @@ class Track : public QObject { ExportTrackMetadataResult exportMetadata( const mixxx::MetadataSource& metadataSource, const SyncTrackMetadataParams& syncParams); - - // Information about the actual properties of the - // audio stream is only available after opening the - // source at least once. On this occasion the metadata - // stream info of the track need to be updated to reflect - // these values. - bool hasStreamInfoFromSource() const { - const auto locked = lockMutex(&m_qMutex); - return m_record.hasStreamInfoFromSource(); - } void updateStreamInfoFromSource( mixxx::audio::StreamInfo&& streamInfo); diff --git a/src/waveform/renderers/allshader/waveformrendererfiltered.cpp b/src/waveform/renderers/allshader/waveformrendererfiltered.cpp index 0ddf164cc3e..ee9ee1b4041 100644 --- a/src/waveform/renderers/allshader/waveformrendererfiltered.cpp +++ b/src/waveform/renderers/allshader/waveformrendererfiltered.cpp @@ -45,8 +45,9 @@ void WaveformRendererFiltered::paintGL() { } #ifdef __STEM__ auto stemInfo = pTrack->getStemInfo(); - // If this track is a stem track, skip the rendering - if (!stemInfo.isEmpty() && waveform->hasStem()) { + uint selectedStems = m_waveformRenderer->getSelectedStems(); + // If this track is a stem track and is not fully loaded as a stereo track, skip the rendering + if (!stemInfo.isEmpty() && waveform->hasStem() && selectedStems != 0xf) { return; } #endif diff --git a/src/waveform/renderers/allshader/waveformrendererhsv.cpp b/src/waveform/renderers/allshader/waveformrendererhsv.cpp index b03a17a304e..c99183a14ea 100644 --- a/src/waveform/renderers/allshader/waveformrendererhsv.cpp +++ b/src/waveform/renderers/allshader/waveformrendererhsv.cpp @@ -45,8 +45,9 @@ void WaveformRendererHSV::paintGL() { } #ifdef __STEM__ auto stemInfo = pTrack->getStemInfo(); - // If this track is a stem track, skip the rendering - if (!stemInfo.isEmpty() && waveform->hasStem()) { + uint selectedStems = m_waveformRenderer->getSelectedStems(); + // If this track is a stem track and is not fully loaded as a stereo track, skip the rendering + if (!stemInfo.isEmpty() && waveform->hasStem() && selectedStems != 0xf) { return; } #endif diff --git a/src/waveform/renderers/allshader/waveformrendererrgb.cpp b/src/waveform/renderers/allshader/waveformrendererrgb.cpp index 828a8a71bbb..65e4fc85e7a 100644 --- a/src/waveform/renderers/allshader/waveformrendererrgb.cpp +++ b/src/waveform/renderers/allshader/waveformrendererrgb.cpp @@ -56,8 +56,9 @@ void WaveformRendererRGB::paintGL() { } #ifdef __STEM__ auto stemInfo = pTrack->getStemInfo(); - // If this track is a stem track, skip the rendering - if (!stemInfo.isEmpty() && waveform->hasStem()) { + uint selectedStems = m_waveformRenderer->getSelectedStems(); + // If this track is a stem track and is not fully loaded as a stereo track, skip the rendering + if (!stemInfo.isEmpty() && waveform->hasStem() && selectedStems != 0xf) { return; } #endif diff --git a/src/waveform/renderers/allshader/waveformrendererstem.cpp b/src/waveform/renderers/allshader/waveformrendererstem.cpp index bacb18d32ca..232163fde42 100644 --- a/src/waveform/renderers/allshader/waveformrendererstem.cpp +++ b/src/waveform/renderers/allshader/waveformrendererstem.cpp @@ -4,6 +4,7 @@ #include #include +#include "engine/engine.h" #include "track/track.h" #include "util/math.h" #include "waveform/renderers/allshader/matrixforwidgetgeometry.h" @@ -11,10 +12,6 @@ #include "waveform/renderers/waveformwidgetrenderer.h" #include "waveform/waveform.h" -namespace { -constexpr int kMaxSupportedStems = 4; -} // anonymous namespace - namespace allshader { WaveformRendererStem::WaveformRendererStem( @@ -33,7 +30,7 @@ void WaveformRendererStem::initializeGL() { m_shader.init(); m_textureShader.init(); auto group = m_pEQEnabled->getKey().group; - for (int stemIdx = 1; stemIdx <= kMaxSupportedStems; stemIdx++) { + for (int stemIdx = 1; stemIdx <= mixxx::kMaxSupportedStems; stemIdx++) { DEBUG_ASSERT(group.endsWith("]")); QString stemGroup = QStringLiteral("%1Stem%2]") .arg(group.left(group.size() - 1), @@ -53,8 +50,10 @@ void WaveformRendererStem::paintGL() { } auto stemInfo = pTrack->getStemInfo(); - // If this track isn't a stem track, skip the rendering - if (stemInfo.isEmpty()) { + uint selectedStems = m_waveformRenderer->getSelectedStems(); + + // If this track isn't a stem track, or if it is fully loaded as stereo, skip the rendering + if (stemInfo.isEmpty() || selectedStems == 0xf) { return; } auto positionType = m_isSlipRenderer ? ::WaveformRendererAbstract::Slip @@ -125,7 +124,7 @@ void WaveformRendererStem::paintGL() { const double maxSamplingRange = visualIncrementPerPixel / 2.0; for (int visualIdx = 0; visualIdx < length; ++visualIdx) { - for (int stemIdx = 0; stemIdx < 4; stemIdx++) { + for (int stemIdx = 0; stemIdx < mixxx::kMaxSupportedStems; stemIdx++) { // Stem is drawn twice with different opacity level, this allow to // see the maximum signal by transparency for (int layerIdx = 0; layerIdx < 2; layerIdx++) { @@ -160,7 +159,9 @@ void WaveformRendererStem::paintGL() { // Apply the gains if (layerIdx) { - max *= m_pStemMute[stemIdx]->toBool() + max *= m_pStemMute[stemIdx]->toBool() || + (selectedStems && + !(selectedStems & 1 << stemIdx)) ? 0.f : static_cast(m_pStemGain[stemIdx]->get()); } diff --git a/src/waveform/renderers/waveformwidgetrenderer.cpp b/src/waveform/renderers/waveformwidgetrenderer.cpp index ffefe5d6d45..f21dcf2a8eb 100644 --- a/src/waveform/renderers/waveformwidgetrenderer.cpp +++ b/src/waveform/renderers/waveformwidgetrenderer.cpp @@ -21,6 +21,9 @@ constexpr int kDefaultDimBrightThreshold = 127; WaveformWidgetRenderer::WaveformWidgetRenderer(const QString& group) : m_group(group), +#ifdef __STEM__ + m_selectedStems(mixxx::kNoStemSelected), +#endif m_orientation(Qt::Horizontal), m_dimBrightThreshold(kDefaultDimBrightThreshold), m_height(-1), @@ -416,6 +419,12 @@ void WaveformWidgetRenderer::setDisplayBeatGridAlpha(int alpha) { m_alphaBeatGrid = alpha; } +#ifdef __STEM__ +void WaveformWidgetRenderer::selectStem(uint stemMask) { + m_selectedStems = stemMask; +} +#endif + void WaveformWidgetRenderer::setTrack(TrackPointer track) { m_pTrack = track; //used to postpone first display until track sample is actually available diff --git a/src/waveform/renderers/waveformwidgetrenderer.h b/src/waveform/renderers/waveformwidgetrenderer.h index 47cd21b67a6..2d4cb4cf3e8 100644 --- a/src/waveform/renderers/waveformwidgetrenderer.h +++ b/src/waveform/renderers/waveformwidgetrenderer.h @@ -46,6 +46,12 @@ class WaveformWidgetRenderer { return m_pTrack; } +#ifdef __STEM__ + uint getSelectedStems() const { + return m_selectedStems; + } +#endif + bool isSlipActive() const { return m_pos[::WaveformRendererAbstract::Play] != m_pos[::WaveformRendererAbstract::Slip]; } @@ -170,6 +176,9 @@ class WaveformWidgetRenderer { return renderer; } +#ifdef __STEM__ + void selectStem(uint stemMask); +#endif void setTrack(TrackPointer track); void setMarkPositions(const QList& markPositions) { m_markPositions = markPositions; @@ -195,6 +204,9 @@ class WaveformWidgetRenderer { protected: const QString m_group; TrackPointer m_pTrack; +#ifdef __STEM__ + uint m_selectedStems; +#endif QList m_rendererStack; Qt::Orientation m_orientation; int m_dimBrightThreshold; diff --git a/src/widget/wlibrarytableview.h b/src/widget/wlibrarytableview.h index 6a63ecc2de5..78977d1e166 100644 --- a/src/widget/wlibrarytableview.h +++ b/src/widget/wlibrarytableview.h @@ -56,7 +56,12 @@ class WLibraryTableView : public QTableView, public virtual LibraryView { signals: void loadTrack(TrackPointer pTrack); - void loadTrackToPlayer(TrackPointer pTrack, const QString& group, bool play = false); + void loadTrackToPlayer(TrackPointer pTrack, + const QString& group, +#ifdef __STEM__ + uint stemMask, +#endif + bool play = false); void trackSelected(TrackPointer pTrack); void onlyCachedCoverArt(bool); void scrollValueChanged(int); diff --git a/src/widget/wstemlabel.cpp b/src/widget/wstemlabel.cpp new file mode 100644 index 00000000000..f9181c9af35 --- /dev/null +++ b/src/widget/wstemlabel.cpp @@ -0,0 +1,51 @@ +#include "wstemlabel.h" + +#include "moc_wstemlabel.cpp" + +WStemLabel::WStemLabel(QWidget* pParent) + : WLabel(pParent), + m_stemInfo(QString(), QColor()), + m_stemNo(0) { +} + +void WStemLabel::setup(const QDomNode& node, const SkinContext& context) { + m_stemNo = context.selectInt(node, "StemNum"); +} + +void WStemLabel::slotTrackUnloaded(TrackPointer track) { + Q_UNUSED(track); + m_stemInfo = StemInfo(); + updateLabel(); +} + +void WStemLabel::slotTrackLoaded(TrackPointer track) { + if (!track) { + return; + } + + auto stemInfo = track->getStemInfo(); + + if (stemInfo.isEmpty()) { + return; + } + + m_stemInfo = stemInfo[m_stemNo - 1]; + updateLabel(); +} + +void WStemLabel::updateLabel() { + QColor color = m_stemInfo.getColor(); + QString text = m_stemInfo.getLabel(); + setTextColor(color); + setLabelText(text); +} + +void WStemLabel::setTextColor(const QColor& color) { + QPalette palette = this->palette(); + palette.setColor(QPalette::WindowText, color); + this->setPalette(palette); +} + +void WStemLabel::setLabelText(const QString& text) { + this->setText(text); +} diff --git a/src/widget/wstemlabel.h b/src/widget/wstemlabel.h new file mode 100644 index 00000000000..52cb22707f6 --- /dev/null +++ b/src/widget/wstemlabel.h @@ -0,0 +1,29 @@ +#pragma once + +#include "control/controlproxy.h" +#include "skin/legacy/skincontext.h" +#include "track/track.h" +#include "widget/wlabel.h" + +class WStemLabel : public WLabel { + Q_OBJECT + public: + explicit WStemLabel(QWidget* pParent = nullptr); + + void setup(const QDomNode& node, const SkinContext& context) override; + + public slots: + void slotTrackLoaded(TrackPointer track); + void slotTrackUnloaded(TrackPointer track); + + private slots: + void updateLabel(); + + private: + void setTextColor(const QColor& color); + void setLabelText(const QString& text); + + StemInfo m_stemInfo; + QString m_group; + int m_stemNo; +}; diff --git a/src/widget/wtrackmenu.cpp b/src/widget/wtrackmenu.cpp index 2854c6fa408..f633cdce217 100644 --- a/src/widget/wtrackmenu.cpp +++ b/src/widget/wtrackmenu.cpp @@ -50,6 +50,9 @@ // WStarRating is required for DlgTrackInfo #include "widget/wstarrating.h" #include "widget/wstarratingaction.h" +#ifdef __STEM__ +#include "widget/wtrackstemmenu.h" +#endif constexpr WTrackMenu::Features WTrackMenu::kDeckTrackMenuFeatures; @@ -293,13 +296,6 @@ void WTrackMenu::createActions() { connect(m_pAutoDJReplaceAct, &QAction::triggered, this, &WTrackMenu::slotAddToAutoDJReplace); } - if (featureIsEnabled(Feature::LoadTo)) { - m_pAddToPreviewDeck = new QAction(tr("Preview Deck"), m_pLoadToMenu); - // currently there is only one preview deck so just map it here. - QString previewDeckGroup = PlayerManager::groupForPreviewDeck(0); - connect(m_pAddToPreviewDeck, &QAction::triggered, this, [this, previewDeckGroup] { loadSelectionToGroup(previewDeckGroup); }); - } - if (featureIsEnabled(Feature::Remove)) { // Keyboard shortcuts are set here just to have them displayed in the menu. // Actual keypress is handled in WTrackTableView::keyPressEvent(). @@ -582,14 +578,6 @@ void WTrackMenu::setupActions() { } if (featureIsEnabled(Feature::LoadTo)) { - m_pLoadToMenu->addMenu(m_pDeckMenu); - - m_pLoadToMenu->addMenu(m_pSamplerMenu); - - if (m_pNumPreviewDecks.get() > 0.0) { - m_pLoadToMenu->addAction(m_pAddToPreviewDeck); - } - addMenu(m_pLoadToMenu); addSeparator(); } @@ -890,11 +878,53 @@ CoverInfo WTrackMenu::getCoverInfoOfLastTrack() const { } } +void WTrackMenu::generateTrackLoadMenu(const QString& group, + const QString& label, + TrackPointer pTrack, + QMenu* pParentMenu, + bool primaryDeck, + bool enabled) { +#ifdef __STEM__ + if (pTrack && !pTrack->hasStreamInfoFromSource()) { + // The stem metadata are loaded on stream info refresh, which occurs + // when the file gets loaded for the time in the session. If there is no + // stream info from source, when open the file, which lead to loading + // the stem manifest. + mixxx::AudioSource::OpenParams config; + config.setChannelCount(mixxx::kMaxEngineChannelInputCount); + SoundSourceProxy(pTrack).openAudioSource(config); + } + if (enabled && pTrack && pTrack->hasStem()) { + auto* pStemMenu = new WTrackStemMenu( + label, pParentMenu, primaryDeck, group, pTrack->getStemInfo()); + connect(pStemMenu, + &WTrackStemMenu::selectedStem, + this, + [this](const QString& group, uint stemMask) { + loadSelectionToGroup(group, stemMask); + close(); + }); + pParentMenu->addMenu(pStemMenu); + } else { +#endif + QAction* pAction = new QAction(label, this); + pAction->setEnabled(enabled); + pParentMenu->addAction(pAction); + connect(pAction, &QAction::triggered, this, [this, group] { loadSelectionToGroup(group); }); +#ifdef __STEM__ + } +#endif +} + void WTrackMenu::updateMenus() { if (isEmpty()) { return; } + if (m_pLoadToMenu) { + m_pLoadToMenu->clear(); + } + // Gray out some stuff if multiple songs were selected. const bool singleTrackSelected = getTrackCount() == 1; @@ -927,12 +957,16 @@ void WTrackMenu::updateMenus() { bool deckEnabled = (!deckPlaying || allowLoadTrackIntoPlayingDeck) && singleTrackSelected; - QAction* pAction = new QAction(tr("Deck %1").arg(i), this); - pAction->setEnabled(deckEnabled); - m_pDeckMenu->addAction(pAction); - connect(pAction, &QAction::triggered, this, [this, deckGroup] { loadSelectionToGroup(deckGroup); }); + auto pTrack = getFirstTrackPointer(); + generateTrackLoadMenu(deckGroup, + tr("Deck %1").arg(i), + getFirstTrackPointer(), + m_pDeckMenu, + true, + deckEnabled); } } + m_pLoadToMenu->addMenu(m_pDeckMenu); int iNumSamplers = static_cast(m_pNumSamplers.get()); const int maxSamplersPerMenu = 16; @@ -940,6 +974,7 @@ void WTrackMenu::updateMenus() { m_pSamplerMenu->clear(); QMenu* pMenu = m_pSamplerMenu; int samplersInMenu = 0; + TrackPointer pTrack = getFirstTrackPointer(); for (int i = 1; i <= iNumSamplers; ++i) { if (samplersInMenu == maxSamplersPerMenu) { samplersInMenu = 0; @@ -954,17 +989,25 @@ void WTrackMenu::updateMenus() { bool samplerPlaying = ControlObject::get( ConfigKey(samplerGroup, "play")) > 0.0; bool samplerEnabled = !samplerPlaying && singleTrackSelected; - QAction* pAction = new QAction(samplerTrString(i), pMenu); - pAction->setEnabled(samplerEnabled); - pMenu->addAction(pAction); - connect(pAction, - &QAction::triggered, - this, - [this, samplerGroup] { - loadSelectionToGroup(samplerGroup); - }); + + generateTrackLoadMenu(samplerGroup, + samplerTrString(i), + pTrack, + pMenu, + false, + samplerEnabled); } } + m_pLoadToMenu->addMenu(m_pSamplerMenu); + + if (m_pNumPreviewDecks.get() > 0.0) { + // currently there is only one preview deck so just map it here. + generateTrackLoadMenu(PlayerManager::groupForPreviewDeck(0), + tr("Preview Deck"), + getFirstTrackPointer(), + m_pLoadToMenu, + false); + } } if (featureIsEnabled(Feature::Playlist)) { @@ -1821,7 +1864,14 @@ void WTrackMenu::slotColorPicked(const mixxx::RgbColor::optional_t& color) { hide(); } -void WTrackMenu::loadSelectionToGroup(const QString& group, bool play) { +#ifdef __STEM__ +void WTrackMenu::loadSelectionToGroup(const QString& group, + uint stemMask, + bool play) { +#else +void WTrackMenu::loadSelectionToGroup(const QString& group, + bool play) { +#endif TrackPointer pTrack = getFirstTrackPointer(); if (!pTrack) { return; @@ -1841,7 +1891,11 @@ void WTrackMenu::loadSelectionToGroup(const QString& group, bool play) { // TODO: load track from this class without depending on // external slot to load track +#ifdef __STEM__ + emit loadTrackToPlayer(pTrack, group, stemMask, play); +#else emit loadTrackToPlayer(pTrack, group, play); +#endif } namespace { diff --git a/src/widget/wtrackmenu.h b/src/widget/wtrackmenu.h index 23b0efda0b7..9a9e641e577 100644 --- a/src/widget/wtrackmenu.h +++ b/src/widget/wtrackmenu.h @@ -110,7 +110,16 @@ class WTrackMenu : public QMenu { const QString getDeckGroup() const; signals: - void loadTrackToPlayer(TrackPointer pTrack, const QString& group, bool play = false); +#ifdef __STEM__ + void loadTrackToPlayer(TrackPointer pTrack, + const QString& group, + uint stemMask, + bool play = false); +#else + void loadTrackToPlayer(TrackPointer pTrack, + const QString& group, + bool play = false); +#endif void trackMenuVisible(bool visible); void saveCurrentViewState(); void restoreCurrentViewStateOrIndex(); @@ -214,6 +223,13 @@ class WTrackMenu : public QMenu { void setupActions(); void updateMenus(); + void generateTrackLoadMenu(const QString& group, + const QString& label, + TrackPointer pTrack, + QMenu* pParentMenu, + bool primaryDeck, + bool enabled = true); + bool featureIsEnabled(Feature flag) const; void addSelectionToPlaylist(int iPlaylistId); @@ -225,7 +241,14 @@ class WTrackMenu : public QMenu { void clearBeats(); void lockBpm(bool lock); - void loadSelectionToGroup(const QString& group, bool play = false); +#ifdef __STEM__ + void loadSelectionToGroup(const QString& group, + uint stemMask = mixxx::kNoStemSelected, + bool play = false); +#else + void loadSelectionToGroup(const QString& group, + bool play = false); +#endif void clearTrackSelection(); std::pair getTrackBpmLockStates() const; @@ -279,9 +302,6 @@ class WTrackMenu : public QMenu { // Save Track Metadata Action: QAction* m_pExportMetadataAct{}; - // Load Track to PreviewDeck - QAction* m_pAddToPreviewDeck{}; - // Send to Auto-DJ Action QAction* m_pAutoDJBottomAct{}; QAction* m_pAutoDJTopAct{}; diff --git a/src/widget/wtrackstemmenu.cpp b/src/widget/wtrackstemmenu.cpp new file mode 100644 index 00000000000..02d6d90e4e9 --- /dev/null +++ b/src/widget/wtrackstemmenu.cpp @@ -0,0 +1,99 @@ +#include "widget/wtrackstemmenu.h" + +#include + +#include "engine/engine.h" +#include "moc_wtrackstemmenu.cpp" + +WTrackStemMenu::WTrackStemMenu(const QString& label, + QWidget* parent, + bool primaryDeck, + const QString& group, + const QList& stemInfo) + : QMenu(label, parent), + m_group(group), + m_selectMode(false), + m_stemInfo(stemInfo), + m_currentSelection(mixxx::kNoStemSelected) { + if (primaryDeck) { + QAction* pAction = new QAction(tr("Load for stem mixing"), this); + addAction(pAction); + connect(pAction, &QAction::triggered, this, [this, group] { + emit selectedStem(group, mixxx::kNoStemSelected); + }); + } + + QAction* pAction = new QAction(tr("Load pre-mixed stereo track"), this); + addAction(pAction); + connect(pAction, &QAction::triggered, this, [this, group] { + emit selectedStem(group, 0xf); + }); + addSeparator(); + + for (uint stemIdx = 0; stemIdx < mixxx::kMaxSupportedStems; stemIdx++) { + m_stemActions.emplace_back( + make_parented(tr("Load the \"%1\" stem") + .arg(m_stemInfo.at(stemIdx).getLabel()), + this)); + addAction(m_stemActions.back().get()); + connect(m_stemActions.back().get(), &QAction::triggered, this, [this, stemIdx] { + emit selectedStem(m_group, 1 << stemIdx); + }); + connect(m_stemActions.back().get(), &QAction::toggled, this, [this, stemIdx](bool checked) { + if (checked) { + m_currentSelection |= 1 << stemIdx; + } else { + m_currentSelection ^= 1 << stemIdx; + } + }); + } + m_selectAction = make_parented(this); + m_selectAction->setToolTip(tr("Load multiple stem into a stereo deck")); + m_selectAction->setDisabled(true); + addAction(m_selectAction.get()); + installEventFilter(this); +} + +bool WTrackStemMenu::eventFilter(QObject* pObj, QEvent* e) { + if (e->type() == QEvent::MouseButtonPress) { + QAction* pAction = activeAction(); + if (pAction && pAction->isCheckable() && m_selectMode) { + pAction->setChecked(!pAction->isChecked()); + updateActions(); + return true; + } + } + return QObject::eventFilter(pObj, e); +} +void WTrackStemMenu::updateActions() { + for (const auto& pAction : m_stemActions) { + pAction->setCheckable(m_selectMode); + } + m_selectAction->setText(m_selectMode + ? !m_currentSelection ? tr("Select stems to load") + : tr("Release \"CTRL\" to load the " + "current selection") + : tr("Use \"CTRL\" to select multiple stems")); +} + +void WTrackStemMenu::showEvent(QShowEvent* pQEvent) { + updateActions(); + QMenu::showEvent(pQEvent); +} + +void WTrackStemMenu::keyPressEvent(QKeyEvent* pQEvent) { + m_selectMode = pQEvent->modifiers() & Qt::ControlModifier; + updateActions(); + pQEvent->accept(); +} + +void WTrackStemMenu::keyReleaseEvent(QKeyEvent* pQEvent) { + bool selectMode = pQEvent->modifiers() & Qt::ControlModifier; + if (!selectMode && m_selectMode && m_currentSelection) { + emit selectedStem(m_group, m_currentSelection); + m_currentSelection = mixxx::kNoStemSelected; + } + m_selectMode = selectMode; + updateActions(); + pQEvent->accept(); +} diff --git a/src/widget/wtrackstemmenu.h b/src/widget/wtrackstemmenu.h new file mode 100644 index 00000000000..915f45f8339 --- /dev/null +++ b/src/widget/wtrackstemmenu.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +#include "track/steminfo.h" +#include "util/parented_ptr.h" + +class QAction; + +class WTrackStemMenu : public QMenu { + Q_OBJECT + public: + WTrackStemMenu(const QString& label, + QWidget* parent, + bool primaryDeck, + const QString& group, + const QList& stemInfo); + + signals: + void selectedStem(const QString& group, uint stemMask); + + protected: + virtual void showEvent(QShowEvent* pQEvent) override; + virtual void keyPressEvent(QKeyEvent* pQEvent) override; + virtual void keyReleaseEvent(QKeyEvent* pQEvent) override; + virtual bool eventFilter(QObject* pObj, QEvent* e) override; + + private: + void updateActions(); + + QString m_group; + bool m_selectMode; + QList m_stemInfo; + uint m_currentSelection; + + std::vector> m_stemActions; + parented_ptr m_selectAction; +}; diff --git a/src/widget/wtracktableview.cpp b/src/widget/wtracktableview.cpp index 8cfda9a4c01..cb8daf5b1b0 100644 --- a/src/widget/wtracktableview.cpp +++ b/src/widget/wtracktableview.cpp @@ -1265,7 +1265,14 @@ void WTrackTableView::activateSelectedTrack() { slotMouseDoubleClicked(indices.at(0)); } -void WTrackTableView::loadSelectedTrackToGroup(const QString& group, bool play) { +#ifdef __STEM__ +void WTrackTableView::loadSelectedTrackToGroup(const QString& group, + uint stemMask, + bool play) { +#else +void WTrackTableView::loadSelectedTrackToGroup(const QString& group, + bool play) { +#endif const QModelIndexList indices = getSelectedRows(); if (indices.isEmpty()) { return; @@ -1299,7 +1306,12 @@ void WTrackTableView::loadSelectedTrackToGroup(const QString& group, bool play) auto* trackModel = getTrackModel(); TrackPointer pTrack; if (trackModel && (pTrack = trackModel->getTrack(index))) { +#ifdef __STEM__ + DEBUG_ASSERT(!stemMask || pTrack->hasStem()); + emit loadTrackToPlayer(pTrack, group, stemMask, play); +#else emit loadTrackToPlayer(pTrack, group, play); +#endif } } diff --git a/src/widget/wtracktableview.h b/src/widget/wtracktableview.h index 7ec93a3ae67..e5c0b932ff7 100644 --- a/src/widget/wtracktableview.h +++ b/src/widget/wtracktableview.h @@ -38,7 +38,14 @@ class WTrackTableView : public WLibraryTableView { void keyPressEvent(QKeyEvent* event) override; void resizeEvent(QResizeEvent* event) override; void activateSelectedTrack(); - void loadSelectedTrackToGroup(const QString& group, bool play); +#ifdef __STEM__ + void loadSelectedTrackToGroup(const QString& group, + uint stemMask, + bool play); +#else + void loadSelectedTrackToGroup(const QString& group, + bool play); +#endif void assignNextTrackColor() override; void assignPreviousTrackColor() override; TrackModel::SortColumnId getColumnIdFromCurrentIndex() override; diff --git a/src/widget/wwaveformviewer.cpp b/src/widget/wwaveformviewer.cpp index 22629abc884..54b2283c1de 100644 --- a/src/widget/wwaveformviewer.cpp +++ b/src/widget/wwaveformviewer.cpp @@ -227,6 +227,19 @@ void WWaveformViewer::slotTrackLoaded(TrackPointer track) { } } +#ifdef __STEM__ +void WWaveformViewer::slotSelectStem(uint stemMask) { + if (m_waveformWidget) { + m_waveformWidget->selectStem(stemMask); + update(); + } +} +#endif + +void WWaveformViewer::slotTrackUnloaded(TrackPointer pOldTrack) { + slotLoadingTrack(pOldTrack, TrackPointer()); +} + void WWaveformViewer::slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack) { Q_UNUSED(pNewTrack); Q_UNUSED(pOldTrack); diff --git a/src/widget/wwaveformviewer.h b/src/widget/wwaveformviewer.h index eead266f540..5848e7c1bad 100644 --- a/src/widget/wwaveformviewer.h +++ b/src/widget/wwaveformviewer.h @@ -43,7 +43,11 @@ class WWaveformViewer : public WWidget, public TrackDropTarget { public slots: void slotTrackLoaded(TrackPointer track); + void slotTrackUnloaded(TrackPointer pOldTrack); void slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack); +#ifdef __STEM__ + void slotSelectStem(uint stemMask); +#endif protected: void showEvent(QShowEvent* event) override;