diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml index 57bdc60f2f9e..c1e6a0480cb2 100644 --- a/.github/ISSUE_TEMPLATE/bug.yaml +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -1,7 +1,7 @@ name: 🐛 Bug Report description: | Describe your problem here. -labels: [bug] +type: "bug" body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 18489e986b0c..2ce3e4bd9aed 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -1,7 +1,7 @@ name: 🚀 Feature Request description: | What feature would you like to see added to Mixxx? -labels: [feature] +type: "feature" body: - type: markdown attributes: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3b77b37d6090..5812cd6cf442 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -126,7 +126,7 @@ repos: - id: prettier types: [yaml] - repo: https://github.com/qarmin/qml_formatter.git - rev: 37c2513b1b8275a475a160ed2f5b044910335d5f # No release tag yet including #6 fix + rev: 16f651d727652dffff92678f4b602df9bfb45eb7 # No release tag yet including #7 fix hooks: - id: qml_formatter - repo: https://github.com/BlankSpruce/gersemi @@ -168,6 +168,8 @@ repos: language: system types: [text] files: ^.*\.qml$ + stages: + - manual - id: metainfo name: metainfo description: Update AppStream metainfo releases from CHANGELOG.md. diff --git a/.tx/config b/.tx/config index 2fec46c1f484..7b5c8801a000 100644 --- a/.tx/config +++ b/.tx/config @@ -1,7 +1,7 @@ [main] host = https://www.transifex.com -[o:mixxx-dj-software:p:mixxxdj:r:mixxx2-6] +[o:mixxx-dj-software:p:mixxxdj:r:mixxx2-7] file_filter = res/translations/mixxx_.ts source_file = res/translations/mixxx.ts source_lang = en diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ccc67080c3f..d4d620d83462 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [2.7.0](https://github.com/mixxxdj/mixxx/milestone/47) (Unreleased) + ## [2.6.0](https://github.com/mixxxdj/mixxx/milestone/44) (Unreleased) ### STEM file support diff --git a/CMakeLists.txt b/CMakeLists.txt index 205a5c06a1b3..36d5fd482517 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -431,7 +431,7 @@ elseif(APPLE) endif() endif() -project(mixxx VERSION 2.6.0 LANGUAGES C CXX) +project(mixxx VERSION 2.7.0 LANGUAGES C CXX) # Work around missing version suffixes support https://gitlab.kitware.com/cmake/cmake/-/issues/16716 set(MIXXX_VERSION_PRERELEASE "beta") # set to "alpha" "beta" or "" @@ -1729,6 +1729,8 @@ add_library( src/widget/wwidget.cpp src/widget/wwidgetgroup.cpp src/widget/wwidgetstack.cpp + src/controllers/scripting/javascriptplayerproxy.cpp + src/controllers/scripting/javascriptplayerproxy.h ) set(MIXXX_COMMON_PRECOMPILED_HEADER src/util/assert.h) set( @@ -3306,8 +3308,8 @@ else() set(CMAKE_FIND_FRAMEWORK FIRST) endif() set(OpenGL_GL_PREFERENCE "GLVND") - find_package(OpenGL REQUIRED) if(EMSCRIPTEN) + find_package(OpenGL REQUIRED) # Emscripten's FindOpenGL.cmake does not create OpenGL::GL target_link_libraries(mixxx-lib PRIVATE ${OPENGL_gl_LIBRARY}) target_compile_definitions(mixxx-lib PUBLIC QT_OPENGL_ES_2) @@ -3319,7 +3321,12 @@ else() PUBLIC -sMIN_WEBGL_VERSION=2 -sMAX_WEBGL_VERSION=2 -sFULL_ES2=1 ) else() - target_link_libraries(mixxx-lib PRIVATE OpenGL::GL) + find_package(WrapOpenGL REQUIRED) + if(OPENGL_opengl_LIBRARY) + target_link_libraries(mixxx-lib PRIVATE OpenGL::OpenGL) + else() + target_link_libraries(mixxx-lib PRIVATE OpenGL::GL) + endif() endif() if(UNIX AND QGLES2) target_compile_definitions(mixxx-lib PUBLIC QT_OPENGL_ES_2) @@ -3530,23 +3537,25 @@ if(QML) src/qml/qmlapplication.cpp src/qml/qmlautoreload.cpp src/qml/qmlbeatsmodel.cpp - src/qml/qmlcuesmodel.cpp - src/qml/qmlcontrolproxy.cpp + src/qml/qmlchainpresetmodel.cpp src/qml/qmlconfigproxy.cpp + src/qml/qmlcontrolproxy.cpp + src/qml/qmlcuesmodel.cpp src/qml/qmldlgpreferencesproxy.cpp src/qml/qmleffectmanifestparametersmodel.cpp - src/qml/qmleffectsmanagerproxy.cpp src/qml/qmleffectslotproxy.cpp + src/qml/qmleffectsmanagerproxy.cpp src/qml/qmllibraryproxy.cpp src/qml/qmllibrarytracklistmodel.cpp + src/qml/qmlmixxxcontrollerscreen.cpp src/qml/qmlplayermanagerproxy.cpp src/qml/qmlplayerproxy.cpp src/qml/qmlvisibleeffectsmodel.cpp - src/qml/qmlchainpresetmodel.cpp - src/qml/qmlwaveformoverview.cpp - src/qml/qmlmixxxcontrollerscreen.cpp src/qml/qmlwaveformdisplay.cpp + src/qml/qmlwaveformoverview.cpp src/qml/qmlwaveformrenderer.cpp + src/qml/qmlsettingparameter.cpp + src/qml/qmltrackproxy.cpp src/waveform/renderers/allshader/digitsrenderer.cpp src/waveform/renderers/allshader/waveformrenderbeat.cpp src/waveform/renderers/allshader/waveformrenderer.cpp @@ -3998,6 +4007,7 @@ elseif(UNIX AND NOT APPLE AND NOT EMSCRIPTEN) if(${X11_FOUND}) target_include_directories(mixxx-lib SYSTEM PUBLIC "${X11_INCLUDE_DIR}") target_link_libraries(mixxx-lib PRIVATE "${X11_LIBRARIES}") + target_compile_definitions(mixxx-lib PUBLIC __X11__) endif() find_package(Qt${QT_VERSION_MAJOR} COMPONENTS DBus REQUIRED) target_link_libraries(mixxx-lib PUBLIC Qt${QT_VERSION_MAJOR}::DBus) diff --git a/LICENSE b/LICENSE index 006f0725fcc2..1bfecb19b6d8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Mixxx 2.6-beta, Digital DJ'ing software. +Mixxx 2.7-alpha, Digital DJ'ing software. Copyright (C) 2001-2025 Mixxx Development Team Mixxx is free software; you can redistribute it and/or modify diff --git a/eslint.config.cjs b/eslint.config.cjs index a9f3a3f107ac..61e0c7d0a20e 100644 --- a/eslint.config.cjs +++ b/eslint.config.cjs @@ -48,6 +48,7 @@ module.exports = tseslint.config( // Mixxx custom "ColorMapper": "readonly", "components": "readonly", + "controller": "readonly", "engine": "readonly", "midi": "readonly", // common-controller-scripts globals diff --git a/res/controllers/Traktor Kontrol S4 MK3.hid.xml b/res/controllers/Traktor Kontrol S4 MK3.hid.xml index 69d65c1037b3..25f9a6a44998 100644 --- a/res/controllers/Traktor Kontrol S4 MK3.hid.xml +++ b/res/controllers/Traktor Kontrol S4 MK3.hid.xml @@ -424,6 +424,188 @@ + + + + + + + + + + + + + + @@ -676,22 +858,49 @@ + + + + - + diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js index e3623c870bc3..b3a9dd726658 100644 --- a/res/controllers/Traktor-Kontrol-S4-MK3.js +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -1,5 +1,9 @@ /// Created by Be and A. Colombier +/******************************************************** + LED Color Constants + *******************************************************/ +// Color codes for controller RGB LEDs const LedColors = { off: 0, red: 4, @@ -21,6 +25,30 @@ const LedColors = { white: 68, }; +const LedColorMap = { + 0xCC0000: LedColors.red, + 0xCC5E00: LedColors.carrot, + 0xCC7800: LedColors.orange, + 0xCC9200: LedColors.honey, + + 0xCCCC00: LedColors.yellow, + 0x81CC00: LedColors.lime, + 0x00CC00: LedColors.green, + 0x00CC49: LedColors.aqua, + + 0x00CCCC: LedColors.celeste, + 0x0091CC: LedColors.sky, + 0x0000CC: LedColors.blue, + 0xCC00CC: LedColors.purple, + + 0xAD65FF: LedColors.fuscia, + 0xCC0079: LedColors.magenta, + 0xCC477E: LedColors.azalea, + 0xCC4761: LedColors.salmon, + + 0xCCCCCC: LedColors.white, +}; + // This define the sequence of color to use for pad button when in keyboard mode. This should make them look like an actual keyboard keyboard octave, except for C, which is green to help spotting it. const KeyboardColors = [ @@ -117,8 +145,7 @@ const MixerControlsMixAuxOnShift = !!engine.getSetting("mixerControlsMicAuxOnShi // Default: false const UseBeatloopRollInsteadOfSampler = !!engine.getSetting("useBeatloopRollInsteadOfSampler"); -// Predefined beatlooproll sizes. Note that if you use AddLoopHalveAndDoubleOnBeatloopRollTab, the first and -// last size will be ignored +// Predefined beatlooproll sizes. const BeatLoopRolls = [ engine.getSetting("beatLoopRollsSize1") || 1/8, engine.getSetting("beatLoopRollsSize2") || 1/4, @@ -130,11 +157,23 @@ const BeatLoopRolls = [ engine.getSetting("beatLoopRollsSize8") || "double" ]; +// Predefined beatjump. +const BeatJumps = [ + engine.getSetting("beatJumpSize1"), // Default to 1 + engine.getSetting("beatJumpSize2"), // Default to 2 + engine.getSetting("beatJumpSize3"), // Default to 4 + engine.getSetting("beatJumpSize4"), // Default to 8 + engine.getSetting("beatJumpSize5"), // Default to 16 + engine.getSetting("beatJumpSize6"), // Default to 32 + engine.getSetting("beatJumpSize7"), // Default to 64 + engine.getSetting("beatJumpSize8"), // Default to "beatjump" +]; + // Define the speed of the jogwheel. This will impact the speed of the LED playback indicator, the scratch, and the speed of // the motor if enable. Recommended value are 33 + 1/3 or 45. // Default: 33 + 1/3 -const BaseRevolutionsPerMinute = engine.getSetting("baseRevolutionsPerMinute") || 33 + 1/3; +const BaseRevolutionsPerMinute = engine.getSetting("baseRevolutionsPerMinute") || 100/3; // aka 33 + 1/3 // Define whether or not to use motors. // This is a BETA feature! Please use at your own risk. Setting this off means that below settings are inactive @@ -152,7 +191,8 @@ const TightnessFactor = engine.getSetting("tightnessFactor") || 0.5; // Define how much force can the motor use. This defines how much the wheel will "fight" you when you block it in TT mode // This will also affect how quick the wheel starts spinning when enabling motor mode, or starting a deck with motor mode on -const MaxWheelForce = engine.getSetting("maxWheelForce") || 25000; // Traktor seems to cap the max value at 60000, which just sounds insane +const MaxWheelForce = engine.getSetting("maxWheelForce") || 60000; // Traktor seems to cap the max value at 60000, which just sounds insane +// But insane or not, it makes sense to follow the manufacturer's spec // Map the mixer potentiometers to different components of the software mixer in Mixxx, on top of the physical control of the hardware // mixer embedded in the S4 Mk3. This is useful if you are not using certain S4 Mk3 outputs. @@ -163,6 +203,11 @@ const SoftwareMixerHeadphone = !!engine.getSetting("softwareMixerHeadphone"); // Define custom default layout used by the pads, instead of intro/outro and first 4 hotcues. const DefaultPadLayout = engine.getSetting("defaultPadLayout"); +// Force that simulates the pull of a slipmat when scratching +const SlipFrictionForce = engine.getSetting("slipFrictionForce") || 12000; + +// Adjust the degree to which pushing/dragging the crown affects playrate +const TurnTableNudgeSensitivity = engine.getSetting("turnTableNudgeSensitivity") || 0.1; // The LEDs only support 16 base colors. Adding 1 in addition to // the normal 2 for Button.prototype.brightnessOn changes the color @@ -201,11 +246,431 @@ const QuickEffectPresetColors = [ LedColors.fuscia + 1, ]; +/******************************************************** + MIXER CONSTANTS + *******************************************************/ // assign samplers to the crossfader on startup const SamplerCrossfaderAssign = true; -const MotorWindUpMilliseconds = 1200; -const MotorWindDownMilliseconds = 900; +/******************************************************** + JOGWHEEL-RELATED CONSTANTS + *******************************************************/ + +// The mode available, which the wheel can be used for. +const WheelModes = { + jog: 0, + vinyl: 1, + motor: 2, + loopIn: 3, + loopOut: 4, +}; + +const MoveModes = { + beat: 0, + bpm: 1, + grid: 2, + keyboard: 3, +}; + +// motor wind up/down +const MotorWindUpMilliseconds = 0; +const MotorWindDownMilliseconds = 0; + +// Motor PID controller coefficients +const ProportionalGain = 80000; +const IntegrativeGain = 1000; +const DerivativeGain = 50000; + +//---------------- +// Input filtering of wheel velocity signal +//---------------- +// Coefficients generated in scipy +// eg., for a 5-tap filter given a sampling rate of 500Hz +// and applying a hamming window +// const VelFilterTaps = 5; +// 50Hz filter +// const VelFilterCoeffs = [0.02840647, 0.23700821, 0.46917063, 0.23700821, 0.02840647]; +// 10Hz filter +// const VelFilterCoeffs = [0.03541093, 0.24092353, 0.44733108, 0.24092353, 0.03541093]; + +// Using a 9-tap filter for better smoothness, even though it introduces slightly more delay: +const VelFilterTaps = 9; + +// 10Hz weighted average lowpass FIR filter. +const VelFilterCoeffs = [0.01755602, 0.04801081, 0.12234688, 0.19760069, 0.2289712, 0.19760069, 0.12234688, 0.04801081, 0.01755602] + +// Can use a simple sliding average. If we need a target tick per measure of 3.2, but we can only get +// whole numbers as inputs, then we need at least 5 samples to get a steady velocity of 3.2 (equiv. to 33.3rpm) +// const VelFilterCoeffs = [1/9, 1/9, 1/9, 1/9, 1/9, 1/9, 1/9, 1/9, 1/9]; + +// Maximum position value for 32-bit unsigned integer +const WheelPositionMax = 2 ** 32 - 1; + +// The fractional revolution that gets multiplied +// with this value cannot in practice reach 2880 +const wheelAbsoluteMax = 2880; //FIXME: nomenclature + +const wheelTimerMax = 2 ** 32 - 1; +const WheelClockFreq = 100000000; // One tick every 10ns (100MHz) + +// Establish rotational constants for wheel math + +// Target motor outputs will ideally be calibrated based on real hardware data specific to the device +// so that variations of construction and wear can be accommodated. For now, these outputs are based on +// a data collection experiment I performed during testing --ZT +const TargetMotorOutput33RPM = 4600; //measured in a rough calibration test, not exact. TODO: refine this value +const TargetMotorOutput45RPM = 5600; //measured in a rough calibration test, not exact. TODO: refine this value + +// Make sure the RPM for 33.3 is as precise as possible, +// And set the target motor output for nudging +let rps = 0; +let TargetMotorOutput = 0; +if (BaseRevolutionsPerMinute == 33) { + rps = (100/3) / 60; + TargetMotorOutput = TargetMotorOutput33RPM; +} else { // 45RPM + rps = BaseRevolutionsPerMinute / 60; + TargetMotorOutput = TargetMotorOutput45RPM; +} +const baseRevolutionsPerSecond = rps; +const BaseDegreesPerSecond = baseRevolutionsPerSecond * 360; +const BaseEncoderTicksPerDegree = BaseDegreesPerSecond * 8; + +// Motor output smoothing damps some of the jitter. +// Incremental change from previous to current sample is reduced by this factor. +const MotorOutSmoothingFactor = 1/5; // 0.5; // smaller is smoother but slower + +// When not scratching, the input velocity goes through an extra smoothing filter +// to help with determining the nudge/jog factor of crown adjustments +const NonSlipPitchSmoothing = 0.5; + +// Slipmat starts slipping when this error level is surpassed +const SlipmatErrorThresh = 0.05; // 5% velocity tolerance for slipping + +// Integrator suppression slip threshold +// this is an attempt to stop the cumulative error from causing big overshoots +// when adjusting via the crown. Without this, the integrator will grow more and more +// during the adjustment, and when you finally let go it slams back so hard that it can +// stop the platter. You can feel this as an increasing resistance to the crown adjustment. +// suppressing the integrator beyond a certain error threshold solves this issue, and +// causes no detrimental effects as long as the error threshold is large enough. If the +// threshold is too small (below 0.2 in my testing) the integrator loses its power +// and the turntable won't be able to reach the target angular velocity. +const IntegratorSuppressionErrorThresh = 0.3; + +// TESTING ONLY. These are configuration values for a motor test routine that I used to collect +// data for output-to-input mapping. -ZT +// Leaving them in because this might prove useful in future development work. +// const S4MK3DEBUG = true; +const S4MK3MOTORTEST_ENABLE = false; +const S4MK3MOTORTEST_UPTIME = 10000; //milliseconds +const S4MK3MOTORTEST_DOWNTIME = 0; //milliseconds +const S4MK3MOTORTEST_STARTLVL= 4500; +const S4MK3MOTORTEST_STEPSIZE = 0; +const S4MK3MOTORTEST_ENDLVL = 6000; + +/******************************************************** + HID REPORT IDs + *******************************************************/ +// ======= +// INPUTS: +// ======= +// There are 3 types of input reports. Components in the +// mapping get associated with one of these three reports, +// or if left undefined, have a default association given +// by their class constructor. + +// All button/boolean interface elements +const HIDInputButtonsReportID = 1 + +// Potentiometers, and a couple of FX select buttons +const HIDInputPotsReportID = 2 + +// Wheel position and timing data +// Also includes touch sensors +// (touch sensors are also sent along with button report) +const HIDInputWheelsReportID = 3 + +// ======= +// OUTPUTS: +// ======= +// TODO: incomplete list of output IDs. Many are still hard-coded +const HIDOutputMotorsReportID = 49 +const HIDOutputVUMeterReportID = 129; + +/******************************************************** + HARDWARE ADDRESSES + *******************************************************/ +//TODO: would be a good idea to put all the byte/bit offsets +// for the HID reports here. For anyone who hasn't studied +// the message protocols for this device, it's a pain +// to get them from further down in the class definitions. +// Additionally, a lot of them are 1 byte off from their +// actual positions in the data stream (because of slicing +// the first byte off the message before passing it to +// the method in question). + +/* + ======================================================== + + CLASS DEFINITIONS + + ======================================================== +*/ + + /* + * Circular buffer for running FIR filter. + * This is used for low-passing the incoming velocity data + * before it reaches the motor controller. Uses a + * weighted moving-average to implement a + * FIR filter whose coefficients were generated separately + * in Python with Scipy + * + * Resource links and Python code: + * https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.firwin.html + * https://howthefouriertransformworks.com/2022/12/23/how-fir-filters-work-applying-the-filter/ + * filt_taps = 5 + * filt_cutoff = 50 # Hertz + * filt_samplingFreq = 500 # Hertz. Assumes the position is coming in every 2ms + * filt_window = 'hamming' + * filt_lpf = scipy.signal.firwin(filt_taps,filt_cutoff,window=filt_window,fs=filt_samplingFreq) + */ +class FilterBuffer { + constructor(numElements, filterCoefficients) { + this.size = numElements; + this.data = new Float64Array(numElements); + // filterCoefficients should be an array of floats + // the same size as the buffer + this.coeffs = filterCoefficients + // this buffer will run 'backwards' to make + // convolution simpler, so the pointer + // starts at the end of the buffer. Doesn't + // really matter though, it could start anywhere + this.writePt = this.size-1; + + // hold the last calculated output value for reference + this.velFiltered = 0; + + // initialize the buffer with zeros + for (let i = 0; i < this.size; i++) { + this.data[i] = 0; + } + } + // get the most recent output result + getCurrentVel() { + return this.velFiltered; + } + // Produce the next filtered sample + runFilter() { + let runningSum = 0; + // Starting at the write pointer, + // multiply the next N elements + // with sequential values from the + // coefficients array, summing the + // results. + for (let i = 0; i < this.size; i++) { + runningSum += this.coeffs[i] * this.data[(this.writePt + i)%this.size]; + } + this.velFiltered = runningSum; + return runningSum; + } + // Write a new value to the buffer, forgetting the oldest one + insert(newVel) { + // First decrement (and wrap) the write pointer + this.writePt--; + if (this.writePt < 0) { + this.writePt = this.size-1; + } + // Second replace the value at the write pointer + this.data[this.writePt] = newVel; + } +} + +/* + * Motor output buffer manager + * + * This initializes and manages a single data buffer + * for the jogwheel motor commands. It takes care of + * using the correct codes for CW/CCW rotation, and + * streamlines the data access so that we don't have + * to declare unnecessary Uint8Arrays in the time-sensitive + * code blocks such as S4Mk3MotorManager.tick() + */ +const MotorBuffIDLeft = 0; +const MotorBuffIDRight = 1; + +const MotorBuffOffsetLeft = 0; +const MotorBuffOffsetRight = 5; +// There are 2 instruction bytes that specify forward or reverse +// motor direction. +const MotorDirFwd = 0; +const MotorDirRev = 1; +const MotorBuffFwdCode01 = 0x20; +const MotorBuffFwdCode02 = 0x01; +const MotorBuffRevCode01 = 0xe0; +const MotorBuffRevCode02 = 0xfe; + +class MotorOutputBuffMgr { + constructor() { + // the output buffer itself: + this.outputBuffer = new Uint8Array([ + 1, MotorBuffFwdCode01, MotorBuffFwdCode02, 0, 0, + 1, MotorBuffFwdCode01, MotorBuffFwdCode02, 0, 0, + ]); + // direction flags for each deck + this.dir = new Uint8Array([MotorDirFwd,MotorDirFwd]); + this.maxOutput = MaxWheelForce; + } + getBuff() { + return this.outputBuffer.buffer; + } + setMaxTorque(newMaxOutput) { + // Set a different maximum output torque + if (newMaxOutput > 0 && newMaxOutput < MaxWheelForce) { + this.maxOutput = newMaxOutput; + // if it's not within a valid range, + // set it to default. + } else { + this.maxOutput = MaxWheelForce; + } + } + setMotorOutput(motorID,outputLvl=0) { + let offset = 0; + + // Make sure the output drive is an integer: + let outputInt = Math.round(outputLvl); + + // Set the correct offset for this motor's data in the output buffer + if (motorID === MotorBuffIDLeft) { + offset = MotorBuffOffsetLeft; + } else if (motorID === MotorBuffIDRight) { + offset = MotorBuffOffsetRight; + } else { + // invalid motor ID + return -1; // error + } + + // Setting the direction of the motor. + // If the output is negative + if (outputInt < 0) { + // remove the negative sign + outputInt = -outputInt; + // if the direction was previously forward, set reverse + // and update the direction code + if (this.dir[motorID] === MotorDirFwd) { + this.dir[motorID] = MotorDirRev; + this.outputBuffer[offset + 1] = MotorBuffRevCode01; + this.outputBuffer[offset + 2] = MotorBuffRevCode02; + } + // Otherwise, the output is zero or positive + } else { + // if the direction was previously reverse, set forward + // and update the direction code + if (this.dir[motorID] === MotorDirRev) { + this.dir[motorID] = MotorDirFwd; + this.outputBuffer[offset + 1] = MotorBuffFwdCode01; + this.outputBuffer[offset + 2] = MotorBuffFwdCode02; + } + } + + // Finally, write the output data into the output buffer. + // It is little endian, so write the 2 bytes thusly + this.outputBuffer[offset + 3] = outputInt & 0xff; + this.outputBuffer[offset + 4] = outputInt >> 8; + + return 0; + } +} + +// The active tab ID. This is used when SharedDataAPI is active, to communicate with the screens which tab is currently selected. +const ActiveTabPadID = { + jump: 1, + hotcue: 2, + roll: 3, + samples: 4, + loop: 5, + mute: 7, + record: 8, + tone: 11, + fxbank1: 12, + fxbank2: 13, +}; + +const wheelLEDmodes = { + off: 0, + dimFlash: 1, + spot: 2, + ringFlash: 3, + dimSpot: 4, + individuallyAddressable: 5, // set byte 4 to 0 and set byes 8 - 40 to color values +}; + +// The mode available, which the wheel can be used for. +const wheelModes = { + jog: 0, + vinyl: 1, + motor: 2, + loopIn: 3, + loopOut: 4, +}; + +const moveModes = { + beat: 0, + bpm: 1, + grid: 2, + keyboard: 3, + hotcueColor: 4, +}; + +// tracks state across input reports +let wheelTimer = null; +// This is a global variable so the S4Mk3Deck Components have access +// to it and it is guaranteed to be calculated before processing +// input for the Components. +let wheelTimerDelta = 0; + +/* + * helper function + */ + +const quickFxChannel = (group) => { + return `[QuickEffectRack1_${group}]`; +}; + +const stemChannel = (group, idx) => { + return `${group.substr(0, group.length - 1)}_Stem${idx + 1}]`; +}; + +const isObject = (item) => { + return (item && typeof item === "object" && !Array.isArray(item)); +}; + +const mergeDeep = (target, ...sources) => { + if (!sources.length) { return target; } + const source = sources.shift(); + + if (isObject(target) && isObject(source)) { + for (const key in source) { + if (isObject(source[key])) { + if (!target[key]) { Object.assign(target, {[key]: {}}); } + mergeDeep(target[key], source[key]); + } else { + Object.assign(target, {[key]: source[key]}); + } + } + } + + return mergeDeep(target, ...sources); +}; + +const hasRuntimeDataAPI = () => typeof engine.getSharedData === "function"; + +const updateRuntimeData = (patch) => { + if (!hasRuntimeDataAPI()) { + return; + } + engine.setSharedData(mergeDeep(engine.getSharedData() || {}, patch)); +}; /* * HID report parsing library @@ -346,13 +811,13 @@ class Component { this.send(value); } outConnect() { - if (this.outKey !== undefined && this.group !== undefined) { + if (this.outKey !== undefined && this.group !== undefined && this.outConnections.length === 0) { const connection = engine.makeConnection(this.group, this.outKey, this.output.bind(this)); // This is useful for case where effect would have been fully disabled in Mixxx. This appears to be the case during unit tests. if (connection) { - this.outConnections[0] = connection; + this.outConnections.push(connection); } else { - console.warn(`Unable to connect ${this.group}.${this.outKey}' to the controller output. The control appears to be unavailable.`); + console.warn(`Unable to connect '${this.group}.${this.outKey}' to the controller output. The control appears to be unavailable.`); } } } @@ -364,6 +829,7 @@ class Component { } outTrigger() { for (const connection of this.outConnections) { + if (!connection) { continue; } connection.trigger(); } } @@ -391,6 +857,9 @@ class ComponentContainer extends Component { } reconnectComponents(callback) { for (const component of this) { + if (typeof component.unshift === "function" && component.unshift.length === 0) { + component.unshift(); + } if (typeof component.outDisconnect === "function" && component.outDisconnect.length === 0) { component.outDisconnect(); } @@ -448,6 +917,13 @@ class Deck extends ComponentContainer { } this.settings = settings; this.secondDeckModes = null; + this.selectedHotcue = null; + + updateRuntimeData({ + selectedHotcue: { + [this.group]: this.selectedHotcue + } + }); } toggleDeck() { if (this.decks === undefined) { @@ -460,14 +936,35 @@ class Deck extends ComponentContainer { newDeckIndex = 0; } - this.switchDeck(Deck.groupForNumber(this.decks[newDeckIndex])); + this.switchDeck(this.decks[newDeckIndex]); } - switchDeck(newGroup) { + switchDeck(newDeck) { + const newGroup = Deck.groupForNumber(newDeck); + + switch (this.moveMode) { + case moveModes.beat: + case moveModes.bpm: + case moveModes.grid: + case moveModes.hotcueColor: + this.moveMode = null; + this.selectedHotcue = null; + + + updateRuntimeData({ + selectedHotcue: { + [this.group]: this.selectedHotcue + } + }); + break; + } + const currentModes = { wheelMode: this.wheelMode, moveMode: this.moveMode, }; + this.selectedStem.fill(false); + engine.setValue(this.group, "scratch2_enable", false); this.group = newGroup; this.color = this.groupsToColors[newGroup]; @@ -478,7 +975,7 @@ class Deck extends ComponentContainer { if (this.wheelMode === wheelModes.motor) { engine.beginTimer(MotorWindUpMilliseconds, () => { - engine.setValue(newGroup, "scratch2_enable", true); + engine.setValue(newGroup, "scratch2_enable", false); }, true); } } @@ -489,12 +986,19 @@ class Deck extends ComponentContainer { } else if (component.group.search(script.eqRegEx) !== -1) { component.group = `[EqualizerRack1_${newGroup}_Effect1]`; } else if (component.group.search(script.quickEffectRegEx) !== -1) { - component.group = `[QuickEffectRack1_${newGroup}]`; + component.group = quickFxChannel(newGroup); } component.color = this.groupsToColors[newGroup]; }); this.secondDeckModes = currentModes; + this.currentDeckNumber = newDeck; + + updateRuntimeData({ + group: { + [this.decks[0] === 1 ? "leftdeck":"rightdeck"]: this.group + } + }); } static groupForNumber(deckNumber) { return `[Channel${deckNumber}]`; @@ -507,13 +1011,19 @@ class Button extends Component { super(options); - if (this.input === undefined) { + if (this.input === undefined + || (typeof this.onLongPress === "function" && this.onLongPress.length === 0) + || (typeof this.onLongRelease === "function" && this.onLongRelease.length === 0) + || (typeof this.onShortPress === "function" && this.onShortPress.length === 0) + || (typeof this.onShortRelease === "function" && this.onShortRelease.length === 0) + || (typeof this.onPress === "function" && this.onPress.length === 0) + || (typeof this.onRelease === "function" && this.onRelease.length === 0)) { this.input = this.defaultInput; - if (typeof this.input === "function" - && this.inReport instanceof HIDInputReport - && this.input.length === 0) { - this.inConnect(); - } + } + if (typeof this.input === "function" + && this.inReport instanceof HIDInputReport + && this.input.length === 0) { + this.inConnect(); } if (this.longPressTimeOutMillis === undefined) { @@ -558,7 +1068,7 @@ class Button extends Component { } indicatorCallback() { this.indicatorState = !this.indicatorState; - this.send((this.indicatorColor || this.color || LedColors.white) + (this.indicatorState ? this.brightnessOn : this.brightnessOff)); + this.send((this.indicatorColor ?? this.color ?? LedColors.white) + (this.indicatorState ? this.brightnessOn : this.brightnessOff)); } indicator(on) { if (on && this.indicatorTimer === 0) { @@ -573,24 +1083,32 @@ class Button extends Component { } } defaultInput(pressed) { + this.pressed = pressed; if (pressed) { + this.isShortPress = true; this.isLongPress = false; + if (typeof this.onPress === "function" && this.onPress.length === 0) { this.onPress(); } if (typeof this.onShortPress === "function" && this.onShortPress.length === 0) { this.onShortPress(); } if ((typeof this.onLongPress === "function" && this.onLongPress.length === 0) || (typeof this.onLongRelease === "function" && this.onLongRelease.length === 0)) { this.longPressTimer = engine.beginTimer(this.longPressTimeOutMillis, () => { this.isLongPress = true; + this.isShortPress = false; this.longPressTimer = 0; if (typeof this.onLongPress !== "function") { return; } this.onLongPress(this); }, true); } } else if (this.isLongPress) { + this.isLongPress = false; + if (typeof this.onRelease === "function" && this.onRelease.length === 0) { this.onRelease(); } if (typeof this.onLongRelease === "function" && this.onLongRelease.length === 0) { this.onLongRelease(); } } else { + this.isShortPress = false; if (this.longPressTimer !== 0) { engine.stopTimer(this.longPressTimer); this.longPressTimer = 0; } + if (typeof this.onRelease === "function" && this.onRelease.length === 0) { this.onRelease(); } if (typeof this.onShortRelease === "function" && this.onShortRelease.length === 0) { this.onShortRelease(); } } } @@ -629,8 +1147,6 @@ class TriggerButton extends Button { class PowerWindowButton extends Button { constructor(options) { super(options); - this.isLongPressed = false; - this.longPressTimer = 0; } onShortPress() { script.toggleControl(this.group, this.inKey); @@ -684,7 +1200,7 @@ class CueButton extends PushButton { if (this.deck.wheelMode === wheelModes.motor) { engine.setValue(this.group, "scratch2_enable", false); engine.beginTimer(MotorWindDownMilliseconds, () => { - engine.setValue(this.group, "scratch2_enable", true); + engine.setValue(this.group, "scratch2_enable", false); }, true); } } @@ -727,30 +1243,50 @@ class HotcueButton extends PushButton { } this.outKey = `hotcue_${this.number}_status`; this.colorKey = `hotcue_${this.number}_color`; + this.indicatorColor = LedColors.off; this.outConnect(); } unshift() { this.inKey = `hotcue_${this.number}_activate`; + this.indicator(false); } shift() { this.inKey = `hotcue_${this.number}_clear`; + this.indicator(true); } input(pressed) { - engine.setValue(this.group, "scratch2_enable", false); - engine.setValue(this.group, this.inKey, pressed); + if (this.deck.moveMode === moveModes.hotcueColor) { + this.deck.selectedHotcue = pressed ? this.number : null; + + updateRuntimeData({ + selectedHotcue: { + [this.group]: this.deck.selectedHotcue + } + }); + } else if (this.deck.libraryPlayButton.pressed) { + engine.setValue(this.deck.libraryPlayButton.group, this.inKey, pressed); + } else { + engine.setValue(this.group, "scratch2_enable", false); + engine.setValue(this.group, this.inKey, pressed); + if (this.shifted) { + this.indicatorColor = LedColors.off; + } + } } output(value) { if (value) { + this.indicatorColor = LedColors.red; this.send(this.color + this.brightnessOn); } else { + this.indicatorColor = LedColors.off; this.send(LedColors.off); } } outConnect() { - if (undefined !== this.group) { + if (undefined !== this.group && this.outConnections.length === 0) { const connection0 = engine.makeConnection(this.group, this.outKey, this.output.bind(this)); if (connection0) { - this.outConnections[0] = connection0; + this.outConnections.push(connection0); } else { console.warn(`Unable to connect ${this.group}.${this.outKey}' to the controller output. The control appears to be unavailable.`); } @@ -759,12 +1295,16 @@ class HotcueButton extends PushButton { this.output(engine.getValue(this.group, this.outKey)); }); if (connection1) { - this.outConnections[1] = connection1; + this.outConnections.push(connection1); } else { console.warn(`Unable to connect ${this.group}.${this.colorKey}' to the controller output. The control appears to be unavailable.`); } } } + outDisconnect() { + this.indicator(false); + super.outDisconnect(); + } } /* @@ -815,17 +1355,17 @@ class KeyboardButton extends PushButton { if (this.number + offset < 1 || this.number + offset > 24) { this.send(0); } else { - this.send(color + (value ? this.brightnessOn : this.brightnessOff)); + this.send(value ? LedColors.yellow : color); } } outConnect() { - if (undefined !== this.group) { + if (undefined !== this.group && this.outConnections.length === 0) { const connection = engine.makeConnection(this.group, "key", (key) => { const offset = this.deck.keyboardOffset - (this.shifted ? 8 : 0); this.output(key === this.number + offset); }); if (connection) { - this.outConnections[0] = connection; + this.outConnections.push(connection); } else { console.warn(`Unable to connect ${this.group}.key' to the controller output. The control appears to be unavailable.`); } @@ -833,6 +1373,146 @@ class KeyboardButton extends PushButton { } } +/* + * Represent a pad button that acts as a stem controller. It will be used to mute or unmute a stem or select it for other operation such as volume or quick effect control + */ +class StemButton extends PushButton { + constructor(options) { + super(options); + if (this.number === undefined || !Number.isInteger(this.number) || this.number < 1 || this.number > 4) { + throw Error("StemButton must have a number property of an integer between 1 and 4"); + } + if (this.deck === undefined) { + throw Error("StemButton must have a deck attached to it"); + } + if (this.deck.mixer === undefined) { + throw Error("StemButton must have a deck with a mixer attached to it"); + } + this.color = 0; + this.muted = 0; + this.outConnect(); + } + unshift() { + this.outTrigger(); + } + shift() { + this.outTrigger(); + } + input(pressed) { + if (!this.enabled) { + return; + } + if (this.shifted && pressed) { + script.toggleControl(stemChannel(this.group, this.number - 1), "mute"); + } + if (!this.shifted) { + this.deck.selectedStem[this.number - 1] = pressed; + updateRuntimeData({ + selectedStems: { + [this.group]: this.deck.selectedStem + } + }); + } + if (!this.shifted && pressed && this.deck.mixer.firstPressedFxSelector !== null) { + const presetNumber = this.deck.mixer.calculatePresetNumber(); + this.color = QuickEffectPresetColors[presetNumber - 1]; + engine.setValue(quickFxChannel(stemChannel(this.group, this.number - 1)), "loaded_chain_preset", presetNumber); + this.deck.mixer.firstPressedFxSelector = null; + this.deck.mixer.secondPressedFxSelector = null; + this.deck.mixer.resetFxSelectorColors(); + + + updateRuntimeData({ + selectedQuickFX: null + }); + } + } + output() { + if (!this.color || !this.enabled) { + this.send(0); + } else { + this.send(this.color + (this.muted ? this.brightnessOff : this.brightnessOn)); + } + } + outConnect() { + if (undefined !== this.group) { + const muteConnection = engine.makeConnection(stemChannel(this.group, this.number - 1), "mute", (mute) => { + this.muted = mute; + this.output(); + }); + if (muteConnection) { + this.outConnections[0] = muteConnection; + } else { + console.warn(`Unable to connect '${stemChannel(this.group, this.number)}.mute' to the controller output. The control appears to be unavailable.`); + } + const colorConnection = engine.makeConnection(stemChannel(this.group, this.number - 1), "color", (color) => { + this.color = this.colorMap.getValueForNearestColor(color); + this.output(); + }); + if (colorConnection) { + this.outConnections[1] = colorConnection; + } else { + console.warn(`Unable to connect '${stemChannel(this.group, this.number)}.color' to the controller output. The control appears to be unavailable.`); + } + const enabledConnection = engine.makeConnection(this.group, "stem_count", (count) => { + this.enabled = count >= this.number; + this.output(); + }); + if (enabledConnection) { + this.outConnections[2] = enabledConnection; + } else { + console.warn(`Unable to connect '${this.group}.stem_count' to the controller output. The control appears to be unavailable.`); + } + } + } +} + +class StemMuteButton extends PushButton { + constructor(options) { + if (options.number === undefined || !Number.isInteger(options.number) || options.number < 1 || options.number > 4) { + throw Error("StemMuteButton must have a number property of an integer between 1 and 4"); + } + super(options); + this.color = 0; + this.muted = 0; + this.outConnect(); + } + output() { + if (!this.enabled) { + this.send(0); + } else { + this.send(LedColors.white + (this.muted ? this.brightnessOff : this.brightnessOn)); + } + } + input(pressed) { + if (pressed) { + script.toggleControl(stemChannel(this.group, this.number - 1), "mute"); + } + } + outConnect() { + if (undefined !== this.group) { + const muteConnection = engine.makeConnection(stemChannel(this.group, this.number - 1), "mute", (mute) => { + this.muted = mute; + this.output(); + }); + if (muteConnection) { + this.outConnections[0] = muteConnection; + } else { + console.warn(`Unable to connect '${stemChannel(this.group, this.number)}.mute' to the controller output. The control appears to be unavailable.`); + } + const enabledConnection = engine.makeConnection(this.group, "stem_count", (count) => { + this.enabled = count >= this.number; + this.output(); + }); + if (enabledConnection) { + this.outConnections[1] = enabledConnection; + } else { + console.warn(`Unable to connect '${this.group}.stem_count' to the controller output. The control appears to be unavailable.`); + } + } + } +} + /* * Represent a pad button that will trigger a pre-defined beatloop size as set in BeatLoopRolls. */ @@ -851,6 +1531,27 @@ class BeatLoopRollButton extends TriggerButton { throw Error(`BeatLoopRollButton ${options.number}'s size "${BeatLoopRolls[options.number]}" is invalid. Must be a float, or the literal 'half' or 'double'`); } options.key = `beatlooproll_${size}_activate`; + options.onShortPress = function() { + if (!this.deck.beatloop) { + this.deck.beatloop = { + size: engine.getValue(this.group, "beatloop_size"), + start: engine.getValue(this.group, "loop_start_position"), + end: engine.getValue(this.group, "loop_end_position"), + enabled: engine.getValue(this.group, "loop_enabled"), + }; + } + engine.setValue(this.group, this.inKey, true); + }; + options.onShortRelease = function() { + engine.setValue(this.group, this.inKey, false); + if (this.deck.beatloop) { + engine.setValue(this.group, "loop_start_position", this.deck.beatloop.start); + engine.setValue(this.group, "loop_end_position", this.deck.beatloop.end); + engine.setValue(this.group, "beatloop_size", this.deck.beatloop.size); + engine.setValue(this.group, "loop_enabled", this.deck.beatloop.enabled); + this.deck.beatloop = undefined; + } + }; } super(options); if (this.deck === undefined) { @@ -868,6 +1569,61 @@ class BeatLoopRollButton extends TriggerButton { } } +/* + * Represent a pad button that will trigger a pre-defined beatjump as set in BeatJumps. + */ +class BeatJumpButton extends TriggerButton { + constructor(options) { + if (options.number === undefined || !Number.isInteger(options.number) || options.number < 0 || options.number > 7) { + throw Error("BeatJumpButton must have a number property of an integer between 0 and 7"); + } + if (BeatJumps[options.number] === "beatjump") { + options.key = "beatjump_forward"; + } else if (BeatJumps[options.number] === "half") { + options.key = "beatjump_size_halve"; + } else if (BeatJumps[options.number] === "double") { + options.key = "beatjump_size_double"; + } else { + const size = parseFloat(BeatJumps[options.number]); + if (isNaN(size)) { + throw Error(`BeatJumpButton ${options.number}'s size "${BeatJumps[options.number]}" is invalid. Must be a float, or the literal 'beatjump', 'half' or 'double'`); + } + options.key = `beatjump_${size}_forward`; + } + super(options); + if (this.deck === undefined) { + throw Error("BeatJumpButton must have a deck attached to it"); + } + + this.outConnect(); + } + shift() { + if (BeatJumps[this.number] === "beatjump") { + this.setKey("beatjump_backward"); + } else if (!isNaN(parseFloat(BeatJumps[this.number]))) { + const size = parseFloat(BeatJumps[this.number]); + this.setKey(`beatjump_${size}_backward`); + } + } + unshift() { + if (BeatJumps[this.number] === "beatjump") { + this.setKey("beatjump_forward"); + } else if (!isNaN(parseFloat(BeatJumps[this.number]))) { + const size = parseFloat(BeatJumps[this.number]); + this.setKey(`beatjump_${size}_forward`); + } + } + output(value) { + if (BeatJumps[this.number] === "beatjump") { + this.send(LedColors.salmon); + } else if (!isNaN(parseFloat(BeatJumps[this.number]))) { + this.send(this.color + (value ? this.brightnessOn : this.brightnessOff)); + } else { + this.send(LedColors.white); + } + } +} + /* * Represent a pad button that interact with a sampler (load, play/pause, cue, eject) */ @@ -883,7 +1639,12 @@ class SamplerButton extends Button { onShortPress() { if (!this.shifted) { if (engine.getValue(this.group, "track_loaded") === 0) { - engine.setValue(this.group, "LoadSelectedTrack", 1); + if (this.deck.samplerStemSelection !== null) { + engine.setValue(this.group, "load_selected_track_stems", this.deck.samplerStemSelection); + this.deck.samplerStemSelection = null; + } else { + engine.setValue(this.group, "LoadSelectedTrack", 1); + } } else { engine.setValue(this.group, "cue_gotoandplay", 1); } @@ -915,16 +1676,16 @@ class SamplerButton extends Button { } } outConnect() { - if (undefined !== this.group) { + if (undefined !== this.group && this.outConnections.length === 0) { const connection0 = engine.makeConnection(this.group, "play", this.output.bind(this)); if (connection0) { - this.outConnections[0] = connection0; + this.outConnections.push(connection0); } else { console.warn(`Unable to connect ${this.group}.play' to the controller output. The control appears to be unavailable.`); } const connection1 = engine.makeConnection(this.group, "track_loaded", this.output.bind(this)); if (connection1) { - this.outConnections[1] = connection1; + this.outConnections.push(connection1); } else { console.warn(`Unable to connect ${this.group}.track_loaded' to the controller output. The control appears to be unavailable.`); } @@ -1068,6 +1829,7 @@ class Mixer extends ComponentContainer { this.secondPressedFxSelector = null; this.comboSelected = false; + // FIXME: hardcoded const fxSelectsInputs = [ {inByte: 8, inBit: 5}, {inByte: 8, inBit: 1}, @@ -1105,14 +1867,12 @@ class Mixer extends ComponentContainer { this.resetFxSelectorColors(); this.quantizeButton = new Button({ - input: function(pressed) { - if (pressed) { - this.globalQuantizeOn = !this.globalQuantizeOn; - for (let deckIdx = 1; deckIdx <= 4; deckIdx++) { - engine.setValue(`[Channel${deckIdx}]`, "quantize", this.globalQuantizeOn); - } - this.send(this.globalQuantizeOn ? 127 : 0); + onPress: function() { + this.globalQuantizeOn = !this.globalQuantizeOn; + for (let deckIdx = 1; deckIdx <= 4; deckIdx++) { + engine.setValue(`[Channel${deckIdx}]`, "quantize", this.globalQuantizeOn); } + this.send(this.globalQuantizeOn ? 127 : 0); }, globalQuantizeOn: false, inByte: 11, @@ -1124,7 +1884,7 @@ class Mixer extends ComponentContainer { group: "[Master]", inKey: "crossfader", inByte: 0, - inReport: inReports[2], + inReport: inReports[HIDInputPotsReportID], }); this.crossfaderCurveSwitch = new Component({ inByte: 18, @@ -1158,7 +1918,7 @@ class Mixer extends ComponentContainer { inKey: "gain", inByte: 22, bitLength: 12, - inReport: inReports[2] + inReport: inReports[HIDInputPotsReportID] }); } if (SoftwareMixerBooth) { @@ -1167,7 +1927,7 @@ class Mixer extends ComponentContainer { inKey: "booth_gain", inByte: 24, bitLength: 12, - inReport: inReports[2] + inReport: inReports[HIDInputPotsReportID] }); } if (SoftwareMixerHeadphone) { @@ -1176,7 +1936,7 @@ class Mixer extends ComponentContainer { inKey: "headMix", inByte: 28, bitLength: 12, - inReport: inReports[2] + inReport: inReports[HIDInputPotsReportID] }); this.pflGain = new Pot({ @@ -1184,13 +1944,13 @@ class Mixer extends ComponentContainer { inKey: "headGain", inByte: 26, bitLength: 12, - inReport: inReports[2] + inReport: inReports[HIDInputPotsReportID] }); } for (const component of this) { if (component.inReport === undefined) { - component.inReport = inReports[1]; + component.inReport = inReports[HIDInputButtonsReportID]; } component.outReport = this.outReport; component.inConnect(); @@ -1249,8 +2009,16 @@ class FXSelect extends Button { } } this.outReport.send(); + + updateRuntimeData({ + selectedQuickFX: this.mixer.calculatePresetNumber() + }); } else { this.mixer.secondPressedFxSelector = this.number; + + updateRuntimeData({ + selectedQuickFX: this.mixer.calculatePresetNumber() + }); } } @@ -1270,7 +2038,7 @@ class FXSelect extends Button { if (this.mixer.firstPressedFxSelector !== null) { for (const deck of [1, 2, 3, 4]) { const presetNumber = this.mixer.calculatePresetNumber(); - engine.setValue(`[QuickEffectRack1_[Channel${deck}]]`, "loaded_chain_preset", presetNumber); + engine.setValue(quickFxChannel(`[Channel${deck}]`), "loaded_chain_preset", presetNumber); } } if (this.mixer.firstPressedFxSelector === this.number) { @@ -1281,6 +2049,10 @@ class FXSelect extends Button { this.mixer.comboSelected = true; } this.mixer.secondPressedFxSelector = null; + + updateRuntimeData({ + selectedQuickFX: null + }); } } @@ -1295,7 +2067,7 @@ class QuickEffectButton extends Button { if (this.number === undefined || !Number.isInteger(this.number) || this.number < 1) { throw Error("number attribute must be an integer >= 1"); } - this.group = `[QuickEffectRack1_[Channel${this.number}]]`; + this.group = quickFxChannel(`[Channel${this.number}]`); this.outConnect(); } onShortPress() { @@ -1329,16 +2101,16 @@ class QuickEffectButton extends Button { this.outConnections[1].trigger(); } outConnect() { - if (this.group !== undefined) { + if (this.group !== undefined && this.outConnections.length === 0) { const connection0 = engine.makeConnection(this.group, "loaded_chain_preset", this.presetLoaded.bind(this)); if (connection0) { - this.outConnections[0] = connection0; + this.outConnections.push(connection0); } else { console.warn(`Unable to connect ${this.group}.loaded_chain_preset' to the controller output. The control appears to be unavailable.`); } const connection1 = engine.makeConnection(this.group, "enabled", this.output.bind(this)); if (connection1) { - this.outConnections[1] = connection1; + this.outConnections.push(connection1); } else { console.warn(`Unable to connect ${this.group}.enabled' to the controller output. The control appears to be unavailable.`); } @@ -1347,7 +2119,7 @@ class QuickEffectButton extends Button { } /* - * Kontrol S4 Mk3 hardware-specific constants + * Kontrol S4 Mk3 hardware-specific member constants */ Pot.prototype.max = 2 ** 12 - 1; @@ -1355,81 +2127,19 @@ Pot.prototype.inBit = 0; Pot.prototype.inBitLength = 16; Encoder.prototype.inBitLength = 4; +Encoder.prototype.tickDelta = 1 / (2 << Encoder.prototype.inBitLength); // valid range 0 - 3, but 3 makes some colors appear whitish Button.prototype.brightnessOff = 0; -Button.prototype.brightnessOn = 2; -Button.prototype.uncoloredOutput = function(value) { - if (this.indicatorTimer !== 0) { - return; - } - const color = (value > 0) ? (this.color || LedColors.white) + this.brightnessOn : LedColors.off; - this.send(color); -}; -Button.prototype.colorMap = new ColorMapper({ - 0xCC0000: LedColors.red, - 0xCC5E00: LedColors.carrot, - 0xCC7800: LedColors.orange, - 0xCC9200: LedColors.honey, - - 0xCCCC00: LedColors.yellow, - 0x81CC00: LedColors.lime, - 0x00CC00: LedColors.green, - 0x00CC49: LedColors.aqua, - - 0x00CCCC: LedColors.celeste, - 0x0091CC: LedColors.sky, - 0x0000CC: LedColors.blue, - 0xCC00CC: LedColors.purple, - - 0xCC0091: LedColors.fuscia, - 0xCC0079: LedColors.magenta, - 0xCC477E: LedColors.azalea, - 0xCC4761: LedColors.salmon, - - 0xCCCCCC: LedColors.white, -}); - -const wheelRelativeMax = 2 ** 32 - 1; -const wheelAbsoluteMax = 2879; - -const wheelTimerMax = 2 ** 32 - 1; -const wheelTimerTicksPerSecond = 100000000; // One tick every 10ns - -const baseRevolutionsPerSecond = BaseRevolutionsPerMinute / 60; -const wheelTicksPerTimerTicksToRevolutionsPerSecond = wheelTimerTicksPerSecond / wheelAbsoluteMax; - -const wheelLEDmodes = { - off: 0, - dimFlash: 1, - spot: 2, - ringFlash: 3, - dimSpot: 4, - individuallyAddressable: 5, // set byte 4 to 0 and set byes 8 - 40 to color values -}; - -// The mode available, which the wheel can be used for. -const wheelModes = { - jog: 0, - vinyl: 1, - motor: 2, - loopIn: 3, - loopOut: 4, -}; - -const moveModes = { - beat: 0, - bpm: 1, - grid: 2, - keyboard: 3, +Button.prototype.brightnessOn = 2; +Button.prototype.uncoloredOutput = function(value) { + if (this.indicatorTimer !== 0) { + return; + } + const color = (value > 0) ? (this.color || LedColors.white) + this.brightnessOn : LedColors.off; + this.send(color); }; - -// tracks state across input reports -let wheelTimer = null; -// This is a global variable so the S4Mk3Deck Components have access -// to it and it is guaranteed to be calculated before processing -// input for the Components. -let wheelTimerDelta = 0; +Button.prototype.colorMap = new ColorMapper(LedColorMap); /* * Kontrol S4 Mk3 hardware specific mapping logic @@ -1445,13 +2155,13 @@ class S4Mk3EffectUnit extends ComponentContainer { this.mixKnob = new Pot({ inKey: "mix", group: this.group, - inReport: inReports[2], + inReport: inReports[HIDInputPotsReportID], inByte: io.mixKnob.inByte, }); this.mainButton = new PowerWindowButton({ unit: this, - inReport: inReports[1], + inReport: inReports[HIDInputButtonsReportID], inByte: io.mainButton.inByte, inBit: io.mainButton.inBit, outByte: io.mainButton.outByte, @@ -1468,14 +2178,14 @@ class S4Mk3EffectUnit extends ComponentContainer { this.group = undefined; this.output(false); }, - input: function(pressed) { + onPress: function() { if (!this.shifted) { for (const index of [0, 1, 2]) { const effectGroup = `[EffectRack1_EffectUnit${unitNumber}_Effect${index + 1}]`; - engine.setValue(effectGroup, "enabled", pressed); + engine.setValue(effectGroup, "enabled", true); } - this.output(pressed); - } else if (pressed) { + this.output(true); + } else { if (this.unit.focusedEffect !== null) { this.unit.setFocusedEffect(null); } else { @@ -1483,6 +2193,15 @@ class S4Mk3EffectUnit extends ComponentContainer { this.shift(); } } + }, + onRelease: function() { + if (!this.shifted) { + for (const index of [0, 1, 2]) { + const effectGroup = `[EffectRack1_EffectUnit${unitNumber}_Effect${index + 1}]`; + engine.setValue(effectGroup, "enabled", false); + } + this.output(false); + } } }); @@ -1493,14 +2212,14 @@ class S4Mk3EffectUnit extends ComponentContainer { this.knobs[index] = new Pot({ inKey: "meta", group: effectGroup, - inReport: inReports[2], + inReport: inReports[HIDInputPotsReportID], inByte: io.knobs[index].inByte, }); this.buttons[index] = new Button({ unit: this, key: "enabled", group: effectGroup, - inReport: inReports[1], + inReport: inReports[HIDInputButtonsReportID], inByte: io.buttons[index].inByte, inBit: io.buttons[index].inBit, outByte: io.buttons[index].outByte, @@ -1573,9 +2292,20 @@ class S4Mk3EffectUnit extends ComponentContainer { } class S4Mk3Deck extends Deck { - constructor(decks, colors, settings, effectUnit, mixer, inReports, outReport, io) { + constructor(decks, colors, settings, effectUnit, mixer, inReports, outReport, motorBuffMgr, io) { super(decks, colors, settings); + // buffer used for lowpassing the input velocity + this.velFilter = new FilterBuffer(VelFilterTaps,VelFilterCoeffs); + + // state variable for whether the disc + slipmat are slipping + // FIXME: nomenclature clash with the Mixxx "slip" mode (called "flux" in Traktor) + // curiously, it is referred to as "censor" mode in part of the Mixxx documentation + this.isSlipping = false; + + // manager for the motor output buffer on this deck + this.motorBuffMgr = motorBuffMgr; + this.playButton = new PlayButton({ output: InactiveLightsAlwaysBacklit ? undefined : Button.prototype.uncoloredOutput }); @@ -1774,10 +2504,10 @@ class S4Mk3Deck extends Deck { this.setKey("loop_enabled"); }, outConnect: function() { - if (this.outKey !== undefined && this.group !== undefined) { + if (this.outKey !== undefined && this.group !== undefined && this.outConnections.length === 0) { const connection = engine.makeConnection(this.group, this.outKey, this.output.bind(this)); if (connection) { - this.outConnections[0] = connection; + this.outConnections.push(connection); } else { console.warn(`Unable to connect ${this.group}.${this.outKey}' to the controller output. The control appears to be unavailable.`); } @@ -1795,7 +2525,7 @@ class S4Mk3Deck extends Deck { this.indicator(false); const wheelOutput = new Uint8Array(40).fill(0); wheelOutput[0] = decks[0] - 1; - controller.sendOutputReport(wheelOutput.buffer, null, 50, true); + controller.sendOutputReport(50, wheelOutput.buffer, true); if (!skipRestore) { this.deck.wheelMode = this.previousWheelMode; } @@ -1852,14 +2582,11 @@ class S4Mk3Deck extends Deck { this.output(false); } : undefined, onShortPress: function() { - this.deck.libraryEncoder.gridButtonPressed = true; - if (this.shift) { engine.setValue(this.group, "bpm_tap", true); } }, onLongPress: function() { - this.deck.libraryEncoder.gridButtonPressed = true; this.previousMoveMode = this.deck.moveMode; if (this.shifted) { @@ -1871,7 +2598,6 @@ class S4Mk3Deck extends Deck { this.indicator(true); }, onLongRelease: function() { - this.deck.libraryEncoder.gridButtonPressed = false; if (this.previousMoveMode !== null) { this.deck.moveMode = this.previousMoveMode; this.previousMoveMode = null; @@ -1879,7 +2605,6 @@ class S4Mk3Deck extends Deck { this.indicator(false); }, onShortRelease: function() { - this.deck.libraryEncoder.gridButtonPressed = false; script.triggerControl(this.group, "beats_translate_curpos"); if (this.shift) { @@ -1892,7 +2617,7 @@ class S4Mk3Deck extends Deck { deck: this, input: function(value) { if (value) { - this.deck.switchDeck(Deck.groupForNumber(decks[0])); + this.deck.switchDeck(decks[0]); this.outReport.data[io.deckButtonOutputByteOffset] = colors[0] + this.brightnessOn; // turn off the other deck selection button's LED this.outReport.data[io.deckButtonOutputByteOffset + 1] = DeckSelectAlwaysBacklit ? colors[1] + this.brightnessOff : 0; @@ -1904,7 +2629,7 @@ class S4Mk3Deck extends Deck { deck: this, input: function(value) { if (value) { - this.deck.switchDeck(Deck.groupForNumber(decks[1])); + this.deck.switchDeck(decks[1]); // turn off the other deck selection button's LED this.outReport.data[io.deckButtonOutputByteOffset] = DeckSelectAlwaysBacklit ? colors[0] + this.brightnessOff : 0; this.outReport.data[io.deckButtonOutputByteOffset + 1] = colors[1] + this.brightnessOn; @@ -1927,18 +2652,37 @@ class S4Mk3Deck extends Deck { shift: function() { this.output(true); }, - input: function(pressed) { - if (pressed) { - this.deck.shift(); - } else { - this.deck.unshift(); - } - } + onPress: function() { + this.deck.shift(); + + updateRuntimeData({ + shift: { + [decks[0] === 1 ? "leftdeck":"rightdeck"]: true + } + }); + }, + onRelease: function() { + this.deck.unshift(); + + updateRuntimeData({ + shift: { + [decks[0] === 1 ? "leftdeck":"rightdeck"]: false + } + }); + }, }); this.leftEncoder = new Encoder({ deck: this, onChange: function(right) { + if (this.deck.hasSelectedStem()) { + this.deck.selectedStem.forEach((selected, stemIdx) => { + if (!selected) { return; } + + engine.setValue(stemChannel(this.group, stemIdx), "volume", engine.getValue(stemChannel(this.group, stemIdx), "volume") + (right ? this.tickDelta : -this.tickDelta)); + }); + return; + } switch (this.deck.moveMode) { case moveModes.grid: @@ -1946,18 +2690,34 @@ class S4Mk3Deck extends Deck { break; case moveModes.keyboard: if ( - this.deck.keyboard[0].offset === (right ? 16 : 0) + this.deck.pads[0].offset === (right ? 16 : 0) ) { return; } this.deck.keyboardOffset += (right ? 1 : -1); - this.deck.keyboard.forEach(function(pad) { + this.deck.pads.forEach(function(pad) { pad.outTrigger(); }); break; case moveModes.bpm: script.triggerControl(this.group, right ? "beats_translate_later" : "beats_translate_earlier"); break; + case moveModes.hotcueColor:{ + if (this.deck.selectedHotcue === null) { + return; + } + const currentColor = Button.prototype.colorMap.getValueForNearestColor(engine.getValue(this.deck.group, `hotcue_${this.deck.selectedHotcue}_color`)); + let currentColorIdx = Object.keys(LedColorMap).indexOf(Object.keys(LedColorMap).find(key => LedColorMap[key] === currentColor)); + currentColorIdx = Math.max( + Math.min( + Object.keys(LedColorMap).length - 2, // Last color is reserved for loop hotcue + currentColorIdx + (right ? 1:-1) + ), + 0 + ); + engine.setValue(this.deck.group, `hotcue_${this.deck.selectedHotcue}_color`, Object.keys(LedColorMap)[currentColorIdx]); + break; + } default: if (!this.shifted) { if (!this.deck.leftEncoderPress.pressed) { @@ -1987,18 +2747,45 @@ class S4Mk3Deck extends Deck { } }); this.leftEncoderPress = new PushButton({ - input: function(pressed) { - this.pressed = pressed; - if (pressed) { + deck: this, + onPress: function() { + if (this.deck.hasSelectedStem()) { + this.deck.selectedStem.forEach((selected, stemIdx) => { + if (!selected) { return; } + + engine.setValue(stemChannel(this.group, stemIdx), "volume", engine.getValue(stemChannel(this.group, stemIdx), "volume") === 1.0 ? 0 : 1); + }); + return; + } + if (this.shifted) { script.toggleControl(this.group, "pitch_adjust_set_default"); } + + updateRuntimeData({ + displayBeatloopSize: { + [this.group]: true + } + }); }, + onRelease: hasRuntimeDataAPI() ? function() { + updateRuntimeData({ + displayBeatloopSize: { + [this.group]: false + } + }); + } : undefined }); this.rightEncoder = new Encoder({ deck: this, onChange: function(right) { - if (this.deck.wheelMode === wheelModes.loopIn || this.deck.wheelMode === wheelModes.loopOut) { + if (this.deck.hasSelectedStem()) { + this.deck.selectedStem.forEach((selected, stemIdx) => { + if (!selected) { return; } + + engine.setValue(quickFxChannel(stemChannel(this.group, stemIdx)), "super1", engine.getValue(quickFxChannel(stemChannel(this.group, stemIdx)), "super1") + (right ? this.tickDelta : -this.tickDelta)); + }); + } else if (this.deck.wheelMode === wheelModes.loopIn || this.deck.wheelMode === wheelModes.loopOut) { const moveFactor = this.shifted ? LoopEncoderShiftMoveFactor : LoopEncoderMoveFactor; const valueIn = engine.getValue(this.group, "loop_start_position") + (right ? moveFactor : -moveFactor); const valueOut = engine.getValue(this.group, "loop_end_position") + (right ? moveFactor : -moveFactor); @@ -2012,12 +2799,15 @@ class S4Mk3Deck extends Deck { } }); this.rightEncoderPress = new PushButton({ - input: function(pressed) { - if (!pressed) { - return; - } - const loopEnabled = engine.getValue(this.group, "loop_enabled"); - if (!this.shifted) { + deck: this, + onPress: function() { + if (this.deck.hasSelectedStem()) { + this.deck.selectedStem.forEach((selected, stemIdx) => { + if (!selected) { return; } + + script.toggleControl(quickFxChannel(stemChannel(this.group, stemIdx)), "enabled"); + }); + } else if (!this.shifted) { script.triggerControl(this.group, "beatloop_activate"); } else { script.triggerControl(this.group, "reloop_toggle"); @@ -2026,26 +2816,34 @@ class S4Mk3Deck extends Deck { }); this.libraryEncoder = new Encoder({ - libraryPlayButtonPressed: false, - gridButtonPressed: false, - starButtonPressed: false, - libraryViewButtonPressed: false, - libraryPlaylistButtonPressed: false, + deck: this, currentSortedColumnIdx: -1, onChange: function(right) { - if (this.libraryViewButtonPressed) { + let fxChanged = false; + for (const fxButton of this.deck.effectUnit.buttons) { + if (fxButton.pressed) { + script.triggerControl(fxButton.group, right ? "next_effect" : "prev_effect"); + fxChanged = true; + } + } + + if (fxChanged) { + return; + } + + if (this.deck.libraryViewButton.pressed) { this.currentSortedColumnIdx = (LibrarySortableColumns.length + this.currentSortedColumnIdx + (right ? 1 : -1)) % LibrarySortableColumns.length; engine.setValue("[Library]", "sort_column", LibrarySortableColumns[this.currentSortedColumnIdx]); - } else if (this.starButtonPressed) { + } else if (this.deck.libraryStarButton.pressed) { if (this.shifted) { // FIXME doesn't exist, feature request needed script.triggerControl(this.group, right ? "track_color_prev" : "track_color_next"); } else { script.triggerControl(this.group, right ? "stars_up" : "stars_down"); } - } else if (this.gridButtonPressed) { + } else if (this.deck.gridButton.pressed) { script.triggerControl(this.group, right ? "waveform_zoom_up" : "waveform_zoom_down"); - } else if (this.libraryPlayButtonPressed) { + } else if (this.deck.libraryPlayButton.pressed) { script.triggerControl("[PreviewDeck1]", right ? "beatjump_16_forward" : "beatjump_16_backward"); } else { // FIXME there is a bug where this action has no effect when the Mixxx window has no focused. https://github.com/mixxxdj/mixxx/issues/11285 @@ -2059,15 +2857,15 @@ class S4Mk3Deck extends Deck { } } else { engine.setValue("[Library]", "focused_widget", this.shifted ? 2 : 3); - engine.setValue("[Library]", "MoveVertical", right ? 1 : -1); + engine.setValue("[Library]", this.deck.turntableButton.pressed ? "ScrollVertical" : "MoveVertical", right ? 1 : -1); } } } }); this.libraryEncoderPress = new Button({ - libraryViewButtonPressed: false, + deck: this, onShortPress: function() { - if (this.libraryViewButtonPressed) { + if (this.deck.libraryViewButton.pressed) { script.toggleControl("[Library]", "sort_order"); } else { const currentlyFocusWidget = engine.getValue("[Library]", "focused_widget"); @@ -2075,7 +2873,11 @@ class S4Mk3Deck extends Deck { if (this.shifted && currentlyFocusWidget === 0) { script.triggerControl("[Playlist]", "ToggleSelectedSidebarItem"); } else if (currentlyFocusWidget === 3 || currentlyFocusWidget === 0) { - script.triggerControl(this.group, "LoadSelectedTrack"); + if (this.deck.hasSelectedStem()) { + engine.setValue(this.group, "load_selected_track_stems", this.deck.stemSelection()); + } else { + script.triggerControl(this.group, "LoadSelectedTrack"); + } } else { script.triggerControl("[Library]", "GoToItem"); } @@ -2088,15 +2890,17 @@ class S4Mk3Deck extends Deck { }); this.libraryPlayButton = new PushButton({ group: "[PreviewDeck1]", - libraryEncoder: this.libraryEncoder, - input: function(pressed) { - if (pressed) { - script.triggerControl(this.group, "LoadSelectedTrackAndPlay"); + deck: this, + onPress: function() { + if (this.shifted) { + engine.setValue(this.group, "CloneFromDeck", this.deck.currentDeckNumber); } else { - engine.setValue(this.group, "play", 0); - script.triggerControl(this.group, "eject"); + script.triggerControl(this.group, "LoadSelectedTrackAndPlay"); } - this.libraryEncoder.libraryPlayButtonPressed = pressed; + }, + onRelease: function() { + engine.setValue(this.group, "play", 0); + script.triggerControl(this.group, "eject"); }, outKey: "play", }); @@ -2106,12 +2910,6 @@ class S4Mk3Deck extends Deck { onShortRelease: function() { script.triggerControl(this.group, this.shifted ? "track_color_prev" : "track_color_next"); }, - onLongPress: function() { - this.libraryEncoder.starButtonPressed = true; - }, - onLongRelease: function() { - this.libraryEncoder.starButtonPressed = false; - }, }); // FIXME there is no feature about playlist at the moment, so we use this button to control the context menu, which has playlist control this.libraryPlaylistButton = new Button({ @@ -2124,7 +2922,7 @@ class S4Mk3Deck extends Deck { }); // This is useful for case where effect would have been fully disabled in Mixxx. This appears to be the case during unit tests. if (connection) { - this.outConnections[0] = connection; + this.outConnections.push(connection); } else { console.warn(`Unable to connect ${this.group}.focused_widget' to the controller output. The control appears to be unavailable.`); } @@ -2137,18 +2935,11 @@ class S4Mk3Deck extends Deck { return; } script.toggleControl("[Library]", "show_track_menu"); - this.libraryEncoder.libraryPlayButtonPressed = false; if (currentlyFocusWidget === 4) { engine.setValue("[Library]", "focused_widget", 3); } }, - onShortPress: function() { - this.libraryEncoder.libraryPlayButtonPressed = true; - }, - onLongRelease: function() { - this.libraryEncoder.libraryPlayButtonPressed = false; - }, onLongPress: function() { engine.setValue("[Library]", "clear_search", 1); } @@ -2161,14 +2952,7 @@ class S4Mk3Deck extends Deck { onShortRelease: function() { script.toggleControl(this.group, this.inKey, true); }, - onLongPress: function() { - this.libraryEncoder.libraryViewButtonPressed = true; - this.libraryEncoderPress.libraryViewButtonPressed = true; - }, - onLongRelease: function() { - this.libraryEncoder.libraryViewButtonPressed = false; - this.libraryEncoderPress.libraryViewButtonPressed = false; - } + onLongPress: function() {}, // This is needed to make difference between a shot and long press }); this.keyboardPlayMode = null; @@ -2189,28 +2973,63 @@ class S4Mk3Deck extends Deck { cueBaseName: "outro_end", }), new HotcueButton({ - number: 1 + number: 1, deck: this }), new HotcueButton({ - number: 2 + number: 2, deck: this }), new HotcueButton({ - number: 3 + number: 3, deck: this }), new HotcueButton({ - number: 4 + number: 4, deck: this }) ]; const hotcuePage2 = Array(8).fill({}); const hotcuePage3 = Array(8).fill({}); + const beatJumpPage = Array(8).fill({}); const samplerOrBeatloopRollPage = Array(8).fill({}); - this.keyboard = Array(8).fill({}); + const keyboard = Array(8).fill({}); + const stem = [ + new StemButton({ + number: 1, + deck: this, + }), + new StemButton({ + number: 2, + deck: this, + }), + new StemButton({ + number: 3, + deck: this, + }), + new StemButton({ + number: 4, + deck: this, + }), + new StemMuteButton({ + number: 1, + }), + new StemMuteButton({ + number: 2, + }), + new StemMuteButton({ + number: 3, + }), + new StemMuteButton({ + number: 4, + }), + ]; let i = 0; /* eslint no-unused-vars: "off" */ for (const pad of hotcuePage2) { // start with hotcue 5; hotcues 1-4 are in defaultPadLayer - hotcuePage2[i] = new HotcueButton({number: i + 1}); - hotcuePage3[i] = new HotcueButton({number: i + 13}); + hotcuePage2[i] = new HotcueButton({number: i + 1, deck: this}); + hotcuePage3[i] = new HotcueButton({number: i + 13, deck: this}); + beatJumpPage[i] = new BeatJumpButton({ + number: i, + deck: this, + }); if (UseBeatloopRollInsteadOfSampler) { samplerOrBeatloopRollPage[i] = new BeatLoopRollButton({ number: i, @@ -2227,6 +3046,7 @@ class S4Mk3Deck extends Deck { } samplerOrBeatloopRollPage[i] = new SamplerButton({ number: samplerNumber, + deck: this, }); if (SamplerCrossfaderAssign) { engine.setValue( @@ -2236,7 +3056,7 @@ class S4Mk3Deck extends Deck { ); } } - this.keyboard[i] = new KeyboardButton({ + keyboard[i] = new KeyboardButton({ number: i + 1, deck: this, }); @@ -2245,9 +3065,13 @@ class S4Mk3Deck extends Deck { const switchPadLayer = (deck, newLayer) => { let index = 0; + if (newLayer === samplerOrBeatloopRollPage && deck.hasSelectedStem()) { + deck.samplerStemSelection = deck.stemSelection(); + } for (let pad of deck.pads) { pad.outDisconnect(); pad.inDisconnect(); + const shifted = pad.shifted; pad = newLayer[index]; Object.assign(pad, io.pads[index]); @@ -2259,7 +3083,17 @@ class S4Mk3Deck extends Deck { pad.group = deck.group; } if (pad.inReport === undefined) { - pad.inReport = inReports[1]; + pad.inReport = inReports[HIDInputButtonsReportID]; + } + if (shifted && typeof pad.shift === "function" && pad.shift.length === 0) { + pad.shift(); + } else if (typeof pad.unshift === "function" && pad.unshift.length === 0) { + pad.unshift(); + } + if (shifted && typeof pad.shift === "function" && pad.shift.length === 0) { + pad.shift(); + } else if (typeof pad.unshift === "function" && pad.unshift.length === 0) { + pad.unshift(); } pad.outReport = outReport; pad.inConnect(); @@ -2276,18 +3110,24 @@ class S4Mk3Deck extends Deck { hotcuePage3: 2, samplerPage: 3, keyboard: 5, + stem: 6, + beatJump: 6, }; switch (DefaultPadLayout) { case DefaultPadLayoutHotcue: switchPadLayer(this, hotcuePage2); this.currentPadLayer = this.padLayers.hotcuePage2; break; + case DefaultPadLayoutSamplerBeatloop: + switchPadLayer(this, beatJumpPage); + this.currentPadLayer = this.padLayers.beatJump; + break; case DefaultPadLayoutSamplerBeatloop: switchPadLayer(this, samplerOrBeatloopRollPage); this.currentPadLayer = this.padLayers.samplerPage; break; case DefaultPadLayoutKeyboard: - switchPadLayer(this, this.keyboard); + switchPadLayer(this, keyboard); this.currentPadLayer = this.padLayers.keyboard; break; default: @@ -2298,7 +3138,7 @@ class S4Mk3Deck extends Deck { this.hotcuePadModeButton = new Button({ deck: this, - onShortPress: function() { + onShortRelease: function() { if (!this.shifted) { if (this.deck.currentPadLayer !== this.deck.padLayers.hotcuePage2) { switchPadLayer(this.deck, hotcuePage2); @@ -2315,34 +3155,87 @@ class S4Mk3Deck extends Deck { } }, + onShortPress: hasRuntimeDataAPI() ? function() { + updateRuntimeData({ + padsMode: { + [this.group]: ActiveTabPadID.hotcue + } + }); + } : undefined, + onLongPress: function() { + this.previousMoveMode = this.deck.moveMode; + this.deck.moveMode = moveModes.hotcueColor; + + }, + onLongRelease: function() { + this.deck.moveMode = this.previousMoveMode; + this.previousMoveMode = null; + }, + // hack to switch the LED color when changing decks + outTrigger: function() { + this.deck.lightPadMode(); + } + }); + this.recordPadModeButton = new Button({ + deck: this, + onShortPress: hasRuntimeDataAPI() ? function() { + updateRuntimeData({ + padsMode: { + [this.deck.group]: ActiveTabPadID.jump + } + }); + switchPadLayer(this.deck, beatJumpPage); + this.deck.lightPadMode(); + } : undefined, // hack to switch the LED color when changing decks outTrigger: function() { this.deck.lightPadMode(); } }); - // The record button doesn't have a mapping by default, but you can add yours here - // this.recordPadModeButton = new Button({ - // ... - // }); this.samplesPadModeButton = new Button({ + pressed: false, deck: this, onShortPress: function() { + engine.setValue(this.deck.group, "loop_anchor", 1); + + updateRuntimeData({ + padsMode: { + [this.deck.group]: UseBeatloopRollInsteadOfSampler ? ActiveTabPadID.roll : ActiveTabPadID.samples + } + }); + }, + onShortRelease: function() { if (this.deck.currentPadLayer !== this.deck.padLayers.samplerPage) { switchPadLayer(this.deck, samplerOrBeatloopRollPage); - engine.setValue("[Samplers]", "show_samplers", true); + engine.setValue("[Skin]", "show_samplers", true); this.deck.currentPadLayer = this.deck.padLayers.samplerPage; } else { switchPadLayer(this.deck, defaultPadLayer); - engine.setValue("[Samplers]", "show_samplers", false); + engine.setValue("[Skin]", "show_samplers", false); this.deck.currentPadLayer = this.deck.padLayers.defaultLayer; } this.deck.lightPadMode(); + engine.setValue(this.deck.group, "loop_anchor", 0); }, + onLongRelease: function() { + engine.setValue(this.deck.group, "loop_anchor", 0); + } }); // The mute button doesn't have a mapping by default, but you can add yours here - // this.mutePadModeButton = new Button({ - // ... - // }); + this.mutePadModeButton = new Button({ + deck: this, + onShortPress: hasRuntimeDataAPI() ? function() { + updateRuntimeData({ + padsMode: { + [this.deck.group]: ActiveTabPadID.mute + } + }); + } : undefined, + // hack to switch the LED color when changing decks + outTrigger: function() { + this.deck.lightPadMode(); + } + }); this.stemsPadModeButton = new Button({ deck: this, @@ -2354,6 +3247,11 @@ class S4Mk3Deck extends Deck { } }, onShortPress: function() { + updateRuntimeData({ + padsMode: { + [this.deck.group]: ActiveTabPadID.stems + } + }); if (this.previousMoveMode === null) { this.previousMoveMode = this.deck.moveMode; this.deck.moveMode = moveModes.keyboard; @@ -2364,12 +3262,19 @@ class S4Mk3Deck extends Deck { this.deck.moveMode = this.previousMoveMode; this.previousMoveMode = null; } - if (this.deck.currentPadLayer === this.deck.padLayers.keyboard) { + let targetLayer = this.deck.padLayers.stem; + if (this.shifted) { + targetLayer = this.deck.padLayers.keyboard; + } + if (this.deck.currentPadLayer === targetLayer) { switchPadLayer(this.deck, defaultPadLayer); this.deck.currentPadLayer = this.deck.padLayers.defaultLayer; - } else if (this.deck.currentPadLayer !== this.deck.padLayers.keyboard) { - switchPadLayer(this.deck, this.deck.keyboard); - this.deck.currentPadLayer = this.deck.padLayers.keyboard; + } else if (targetLayer === this.deck.padLayers.stem) { + switchPadLayer(this.deck, stem); + this.deck.currentPadLayer = targetLayer; + } else if (targetLayer === this.deck.padLayers.keyboard) { + switchPadLayer(this.deck, keyboard); + this.deck.currentPadLayer = targetLayer; } this.deck.lightPadMode(); }, @@ -2386,43 +3291,47 @@ class S4Mk3Deck extends Deck { }); this.wheelMode = wheelModes.vinyl; - this.turntableButton = UseMotors ? new Button({ + this.turntableButton = new Button({ deck: this, - input: function(press) { - if (press) { - this.deck.reverseButton.loopModeOff(true); - this.deck.fluxButton.loopModeOff(true); - if (this.deck.wheelMode === wheelModes.motor) { - this.deck.wheelMode = wheelModes.vinyl; - engine.setValue(this.group, "scratch2_enable", false); - } else { - this.deck.wheelMode = wheelModes.motor; - const group = this.group; - } - this.outTrigger(); - } + onShortPress: function() { }, - outTrigger: function() { + onLongRelease: function() { + }, + onShortRelease: UseMotors ? function() { + this.deck.reverseButton.loopModeOff(true); + this.deck.fluxButton.loopModeOff(true); + if (this.deck.wheelMode === wheelModes.motor) { + this.deck.wheelMode = wheelModes.vinyl; + engine.setValue(this.group, "scratch2_enable", false); + } else { + this.deck.wheelMode = wheelModes.motor; + engine.setValue(this.group, "scratch2_enable", false); + const group = this.group; + engine.beginTimer(MotorWindUpMilliseconds, () => { + engine.setValue(group, "scratch2_enable", true); + }, true); + } + this.outTrigger(); + } : undefined, + outTrigger: UseMotors ? function() { const motorOn = this.deck.wheelMode === wheelModes.motor; this.send(this.color + (motorOn ? this.brightnessOn : this.brightnessOff)); const vinylModeOn = this.deck.wheelMode === wheelModes.vinyl; this.deck.jogButton.send(this.color + (vinylModeOn ? this.brightnessOn : this.brightnessOff)); - }, - }) : undefined; + } : undefined, + }); this.jogButton = new Button({ deck: this, - input: function(press) { - if (press) { - this.deck.reverseButton.loopModeOff(true); - this.deck.fluxButton.loopModeOff(true); - if (this.deck.wheelMode === wheelModes.vinyl) { - this.deck.wheelMode = wheelModes.jog; - } else { - this.deck.wheelMode = wheelModes.vinyl; - } - engine.setValue(this.group, "scratch2_enable", false); - this.outTrigger(); + onPress: function() { + this.deck.reverseButton.loopModeOff(true); + this.deck.fluxButton.loopModeOff(true); + if (this.deck.wheelMode === wheelModes.vinyl) { + this.deck.wheelMode = wheelModes.jog; + } else { + this.deck.wheelMode = wheelModes.vinyl; } + engine.setValue(this.group, "scratch2_enable", false); + this.outTrigger(); }, outTrigger: function() { const vinylOn = this.deck.wheelMode === wheelModes.vinyl; @@ -2464,59 +3373,112 @@ class S4Mk3Deck extends Deck { } }); - // The relative and absolute position inputs have the same resolution but direction - // cannot be determined reliably with the absolute position because it is easily - // possible to spin the wheel fast enough that it spins more than half a revolution - // between input reports. So there is no need to process the absolution position - // at all; the relative position is sufficient. - this.wheelRelative = new Component({ - oldValue: null, + // There are two position inputs reported in the USB data, with identical + // resolution. The first one is a cleaned up (unwrapped and sometimes corrected) + // version of the second one, which appears to be the raw sensor data and wraps + // every 2880 ticks (representing one full rotation). + // Therefore we only care about the first position input. + this.wheelPosition = new Component({ + prevData: null, deck: this, - speed: 0, - input: function(value, timestamp) { - if (this.oldValue === null) { - // This is to avoid the issue where the first time, we diff with 0, leading to the absolute value - this.oldValue = [value, timestamp, 0]; - return; + velocity: 0, + prevPitch: 0, + vFilter: this.velFilter, + input: function(inPosition, inTimestamp) { + // The input to the wheel is pulled from the HID input report #3 + // value: 16-bit integer that indicates angular position. Every step + // represents 1/8th of a degree of rotation. + // timestamp: 32-bit counter used by the hardware, so is "immune" + // from USB transport jitter + + // Since we're calculating velocity as difference in position, + // we have to init the previous value to zero + if (this.prevData === null) { + this.prevData = [inPosition, inTimestamp]; + return; // velocity is implicitly zero here on the return } - let [oldValue, oldTimestamp, speed] = this.oldValue; - if (timestamp < oldTimestamp) { - oldTimestamp -= wheelTimerMax; - } + // After the 1st iteration, proceed as normal. Save the previous data + let [prevPosition, prevTimestamp] = this.prevData; - let diff = value - oldValue; - if (diff > wheelRelativeMax / 2) { - oldValue += wheelRelativeMax; - } else if (diff < -wheelRelativeMax / 2) { - oldValue -= wheelRelativeMax; + // Unwrap the previous counter value if the new one rolls over to zero + if (inTimestamp < prevTimestamp) { + prevTimestamp -= wheelTimerMax; } - - const currentSpeed = (value - oldValue)/(timestamp - oldTimestamp); - if ((currentSpeed <= 0) === (speed <= 0)) { - speed = (speed + currentSpeed)/2; - } else { - speed = currentSpeed; + + // Unwrap over/under-runs in the position signal + let diff = inPosition - prevPosition; + if (diff > WheelPositionMax / 2) { + prevPosition += WheelPositionMax; + } else if (diff < -WheelPositionMax / 2) { + prevPosition -= WheelPositionMax; } - this.oldValue = [value, timestamp, speed]; - this.speed = wheelAbsoluteMax*speed*10; - - if (this.speed === 0 && + + // Using the unwrapped position reference, calculate 1st derivative + // (angular velocity) + // first, calculate the velocity in ticks (1/8th degree) per second + // inPosition and prevPosition are both in 1/8th degrees + // inTimestamp and prevTimestamp are both in clock ticks (10ns per) + // WheelClockFreq converts clock ticks to seconds + // example: position difference of 3 and timestamp difference of 2ms = 200000ns + // results in 1.5e-05 or 0.000015 + // multiply by 100MHz or 100000000 produces 1500 ticks per second + // (for reference, 33.3rpm is 1600 ticks per second) + const currentVelocityTicksPerSecond = WheelClockFreq * (inPosition - prevPosition)/(inTimestamp - prevTimestamp); + + // then, normalize it with reference to the target rotation speed of the platter + // regardless of how the pitch is being adjusted. In other words, the "rate_ratio" + // is not a consideration here because we are only concerned with how fast the + // platter is spinning relative to the playback rate of 1.0 + // Therefore we simply divide the ticks per second by the ticks per degree + // which eliminates the units of measure and leaves us with a ratio. + const currentVelocityNormalized = currentVelocityTicksPerSecond / BaseEncoderTicksPerDegree; + + // Input filtering: + // Using a weighted average convolution filter ie., an FIR filter, + // apply a lowpass to the velocity which is otherwise quite noisy. + this.vFilter.insert(currentVelocityNormalized); // push/pop to the circular buffer + this.velocity = this.vFilter.runFilter(); // sum of products with filter coeffs + + // Overwrite the previous position/time data with the current values + this.prevData = [inPosition, inTimestamp]; + + // At this point, all of our velocity computation is complete. + // Now, we decide what to do with this information. + + // If the measured velocity is zero + // and there is no velocity reported in the scratch2 value + // and there is no residual jog value (recall: jog is an accumulator that + // gets "consumed" by RateControl::calculateSpeed in ratecontrol.cpp) + // and we are in any WheelMode except motor, + // stop here. The velocity is irrelevant to the system state. + if (this.velocity === 0 && engine.getValue(this.group, "scratch2") === 0 && engine.getValue(this.group, "jog") === 0 && - this.deck.wheelMode !== wheelModes.motor) { + this.deck.wheelMode !== WheelModes.motor) { return; } + // Otherwise, interpret/report the velocity differently + // depending on the wheel mode switch (this.deck.wheelMode) { case wheelModes.motor: - engine.setValue(this.group, "scratch2", this.speed); + // Smoothing the output playback (when not slipping/scratching) + if (engine.getValue(this.group, "play")) { + if (this.deck.isSlipping == false) { + // apply simple smoothing filter to the velocity input when the disc is not slipping/scratching + this.velocity = this.prevPitch + (NonSlipPitchSmoothing*(this.velocity - this.prevPitch)); + this.prevPitch = this.velocity; + } + } else { + engine.setValue(this.group, "scratch2", this.velocity); + } break; case wheelModes.loopIn: { const loopStartPosition = engine.getValue(this.group, "loop_start_position"); const loopEndPosition = engine.getValue(this.group, "loop_end_position"); - const value = Math.min(loopStartPosition + (this.speed * LoopWheelMoveFactor), loopEndPosition - LoopWheelMoveFactor); + const value = Math.min(loopStartPosition + (this.velocity * LoopWheelMoveFactor), loopEndPosition - LoopWheelMoveFactor); engine.setValue( this.group, "loop_start_position", @@ -2527,7 +3489,7 @@ class S4Mk3Deck extends Deck { case wheelModes.loopOut: { const loopEndPosition = engine.getValue(this.group, "loop_end_position"); - const value = loopEndPosition + (this.speed * LoopWheelMoveFactor); + const value = loopEndPosition + (this.velocity * LoopWheelMoveFactor); engine.setValue( this.group, "loop_end_position", @@ -2537,13 +3499,13 @@ class S4Mk3Deck extends Deck { break; case wheelModes.vinyl: if (this.deck.wheelTouch.touched || engine.getValue(this.group, "scratch2") !== 0) { - engine.setValue(this.group, "scratch2", this.speed); + engine.setValue(this.group, "scratch2", this.velocity); } else { - engine.setValue(this.group, "jog", this.speed); + engine.setValue(this.group, "jog", this.velocity); } break; default: - engine.setValue(this.group, "jog", this.speed); + engine.setValue(this.group, "jog", this.velocity); } }, }); @@ -2582,32 +3544,44 @@ class S4Mk3Deck extends Deck { return; } // Emit cue haptic feedback if enabled + + // samplePos aka currentPos const samplePos = Math.round(fractionOfTrack * engine.getValue(this.group, "track_samples")); if (this.deck.wheelTouch.touched && CueHapticFeedback) { const cuePos = engine.getValue(this.group, "cue_point"); + // forward == clockwise rotation const forward = this.lastPos <= samplePos; let fired = false; + // Stage a motor instruction with forward direction and max wheel force + // FIXME: this should be migrated to use the new motor manager. Not a big deal though. const motorDeckData = new Uint8Array([ 1, 0x20, 1, MaxWheelForce & 0xff, MaxWheelForce >> 8, ]); + // if the cue position is between the current and last position, + // then fire the haptic bump + // clockwise check: last < cue < current if (forward && this.lastPos < cuePos && cuePos < samplePos) { fired = true; + // counter-clockwise check: current < cue < last } else if (!forward && cuePos < this.lastPos && samplePos <= cuePos) { - motorDeckData[1] = 0xe0; - motorDeckData[2] = 0xfe; + motorDeckData[1] = 0xe0; // set reverse direction + motorDeckData[2] = 0xfe; // set reverse direction (byte 2) fired = true; } if (fired) { + // FIXME: this should be migrated to use the new motor manager. Not a big deal though. const motorData = new Uint8Array([ 1, 0x20, 1, 0, 0, 1, 0x20, 1, 0, 0, ]); + // overwrite one or the other of the 5-byte motor instructions + // with if (this.deck === TraktorS4MK3.leftDeck) { motorData.set(motorDeckData); } else { motorData.set(motorDeckData, 5); } - controller.sendOutputReport(49, motorData.buffer, true); + controller.sendOutputReport(HIDOutputMotorsReportID, motorData.buffer, true); } } this.lastPos = samplePos; @@ -2618,6 +3592,7 @@ class S4Mk3Deck extends Deck { const fractionalRevolution = revolutions - Math.floor(revolutions); const LEDposition = fractionalRevolution * wheelAbsoluteMax; + // send commands to the wheel ring LEDs const wheelOutput = new Uint8Array(40).fill(0); wheelOutput[0] = decks[0] - 1; wheelOutput[4] = this.color + Button.prototype.brightnessOn; @@ -2641,13 +3616,16 @@ class S4Mk3Deck extends Deck { } }); + this.selectedStem = new Array(4).fill(false); + this.samplerStemSelection = null; + for (const property in this) { if (Object.prototype.hasOwnProperty.call(this, property)) { const component = this[property]; if (component instanceof Component) { Object.assign(component, io[property]); if (component.inReport === undefined) { - component.inReport = inReports[1]; + component.inReport = inReports[HIDInputButtonsReportID]; } component.outReport = outReport; if (component.group === undefined) { @@ -2670,6 +3648,17 @@ class S4Mk3Deck extends Deck { } } + hasSelectedStem() { + return this.selectedStem.some((stemSelected) => stemSelected); + } + + stemSelection() { + return [...this.selectedStem].reverse().reduce( + (acc, curr) => (curr + acc * 2), + 0, + ); + } + assignKeyboardPlayMode(group, action) { this.keyboardPlayMode = { group: group, @@ -2687,102 +3676,310 @@ class S4Mk3Deck extends Deck { this.hotcuePadModeButton.send(this.hotcuePadModeButton.color + this.hotcuePadModeButton.brightnessOff); } + const data = (hasRuntimeDataAPI() ? engine.getSharedData() : false) || {}; + // unfortunately the other pad mode buttons only have one LED color // const recordPadModeLEDOn = this.currentPadLayer === this.padLayers.hotcuePage3; - // this.recordPadModeButton.send(recordPadModeLEDOn ? 127 : 0); + this.recordPadModeButton.output(data.padsMode && data.padsMode[this.group] === ActiveTabPadID.jump); const samplesPadModeLEDOn = this.currentPadLayer === this.padLayers.samplerPage; this.samplesPadModeButton.send(samplesPadModeLEDOn ? 127 : 0); // this.mutePadModeButtonLEDOn = this.currentPadLayer === this.padLayers.samplerPage2; - // const mutedModeButton.send(mutePadModeButtonLEDOn ? 127 : 0); + this.mutePadModeButton.output(data.viewArtwork && data.viewArtwork[this.group]); if (this.keyboardPlayMode !== null) { this.stemsPadModeButton.send(LedColors.green + this.stemsPadModeButton.brightnessOn); } else { - const keyboardPadModeLEDOn = this.currentPadLayer === this.padLayers.keyboard; + const keyboardPadModeLEDOn = this.currentPadLayer === this.padLayers.keyboard || this.currentPadLayer === this.padLayers.stem; this.stemsPadModeButton.send(this.stemsPadModeButton.color + (keyboardPadModeLEDOn ? this.stemsPadModeButton.brightnessOn : this.stemsPadModeButton.brightnessOff)); } + if (!hasRuntimeDataAPI() || !data.keyboardMode) { return; } + data.keyboardMode[this.group] = this.currentPadLayer === this.padLayers.keyboard; + engine.setSharedData(data); } } class S4Mk3MotorManager { constructor(deck) { this.deck = deck; - this.userHold = 0; + // Set the Left/Right motor identifier + // NOTE: CURRENTLY THIS ASSUMES THAT THE FIRST DECK ON THE LEFT IS 1 + // AND THE FIRST DECK ON THE RIGHT IS 2 + if (this.deck.decks[0] === 1) { + this.deckMotorID = MotorBuffIDLeft; + } + else if (this.deck.decks[0] === 2) { + this.deckMotorID = MotorBuffIDRight; + } + else { + console.warn(`First deck assignment ${this.deck.decks[0]} does not match expected mapping of 1 (left deck) or 2 (right deck)`); + } + + this.motorBuffMgr = this.deck.motorBuffMgr; + this.oldValue = [0, 0]; - this.baseFactor = parseInt(110 * BaseRevolutionsPerMinute); - this.zeroSpeedForce = 1650; this.currentMaxWheelForce = MaxWheelForce; + this.prev_playbackError = 0; + this.proportionalTerm = 0; + this.integralAccumulator = 0; + this.derivativeTerm = 0; + this.outputTorquePrev = 0; + this.outputTrackingPrev = 0; + + // Motor testing variables. For development only. -ZT + this.motorTestingOnOff = false; + this.motorTestingComplete = false; + this.motorTestingTimer = Date.now(); + this.motorTestingNextInterval = S4MK3MOTORTEST_UPTIME; + this.motorTestingCurrentLevel = S4MK3MOTORTEST_STARTLVL; + + this.nominalRatePrenudge = 1.0; + this.isUpToSpeed = false; + this.isStopped = false; } tick() { - const motorData = new Uint8Array([ - 1, 0x20, 1, 0, 0, - ]); - const maxVelocity = 10; - let velocity = 0; + let outputTorque = 0; + let targetRate = 0; + let playbackError = 0; + let torqueDiff = 0; + let trackingDiff = 0; + let outputTracking = 0; + let trackingError = 0; + let trackingTarget = 0; + + // Declaring local constant used in jog mode + const maxVelocity = 10; //FIXME: hardcoded and unexplained + + // get latest velocity calculation from the input lowpass filter + const normalizedVelocity = this.deck.velFilter.getCurrentVel() + + // If this deck is currently playing, calculate motor output: + // Determine target (relative) angular velocity based on wheel mode + if (this.deck.wheelMode === wheelModes.motor) { + if (engine.getValue(this.deck.group, "play")) { + if (this.isStopped === true) { + this.isStopped = false; + engine.setValue(this.deck.group, "scratch2_enable", false); + } + // Motor calibration testing for determining real output torque. Development only. + if (S4MK3MOTORTEST_ENABLE && this.motorTestingComplete === false) { + if (Date.now() - this.motorTestingTimer > this.motorTestingNextInterval) { + console.warn(this.deckMotorID, "Motor test level: ", this.motorTestingCurrentLevel); + this.motorTestingTimer = Date.now(); + // If the output was previously OFF, turn it on + if (this.motorTestingOnOff === false) { + this.motorTestingOnOff = true; + this.motorTestingNextInterval = S4MK3MOTORTEST_UPTIME; + // If the output was previously ON, turn it off and increment + // for next time + } else { + this.motorTestingOnOff = false; + this.motorTestingNextInterval = S4MK3MOTORTEST_DOWNTIME; + if (this.motorTestingCurrentLevel > S4MK3MOTORTEST_ENDLVL) { + this.motorTestingCurrentLevel = 0; + this.motorTestingComplete = true; + } else { + this.motorTestingCurrentLevel += S4MK3MOTORTEST_STEPSIZE; + } + } + } + + if (this.motorTestingOnOff === true) { + outputTorque = this.motorTestingCurrentLevel; + } else { + outputTorque = 0; + } + // Write the calculated value to the motor output buffer + this.motorBuffMgr.setMotorOutput(this.deckMotorID, outputTorque); + return true; + } - let expectedSpeed = 0; + // targetRate is 1.0 +/- pitch adjustment. So +8% pitch means targetRate == 1.08 + targetRate = engine.getValue(this.deck.group, "rate_ratio"); - const currentSpeed = this.deck.wheelRelative.speed / baseRevolutionsPerSecond; + // if the performer is holding the REV button, spin the wheel backwards + if (engine.getValue(this.deck.group, "reverseroll")) { + targetRate = -targetRate; + } + + // First, determine the error between target vs measured + playbackError = targetRate - normalizedVelocity; + + // If we are touching the disc AND the playbackError goes beyond + // the slipping threshold, apply the slip force only + if (this.deck.wheelTouch.touched && Math.abs(playbackError) > SlipmatErrorThresh) { + this.deck.isSlipping = true; + engine.setValue(this.deck.group, "scratch2_enable", true); + } else if (this.deck.wheelTouch.touched == false && this.deck.isSlipping && Math.abs(playbackError) < SlipmatErrorThresh) { + this.deck.isSlipping = false; + engine.setValue(this.deck.group, "scratch2_enable", false); + + // If we are beyond a certain error threshold, + // suppress the error integrator---to help with gracefully restoring rotation + // speed without overshoot when adjusting with the crown. + } else if (Math.abs(playbackError) > IntegratorSuppressionErrorThresh) { + // keep the accumulator suppressed so it doesn't go crazy + this.integralAccumulator = 0; + } - if (this.deck.wheelMode === wheelModes.motor - && engine.getValue(this.deck.group, "play")) { - expectedSpeed = engine.getValue(this.deck.group, "rate_ratio"); - const normalisationFactor = 1/expectedSpeed/5; - velocity = expectedSpeed + Math.pow(-5 * (expectedSpeed / 1) * (currentSpeed - expectedSpeed), 3); + // apply slipmat friction force if slipping + if (this.deck.isSlipping) { + engine.setValue(this.deck.group, "scratch2", normalizedVelocity); + // use the slipmat error threshold as a 'dead zone' to avoid chattering when + // hand-spinning close to the nominal rotation velocity + if (playbackError > SlipmatErrorThresh) { + outputTorque = SlipFrictionForce; + } else if (playbackError < -SlipmatErrorThresh) { + outputTorque = -SlipFrictionForce; + } else { + outputTorque = 0; + } + } else { // If we aren't slipping, apply new motor controller + // PID motor controller + this.proportionalTerm = playbackError * ProportionalGain; + this.integralAccumulator += playbackError * IntegrativeGain; + this.derivativeTerm = (playbackError - this.prev_playbackError) * DerivativeGain; + outputTorque = this.proportionalTerm + this.integralAccumulator - this.derivativeTerm; + + // Difference calculation for a smoothing filter + torqueDiff = outputTorque - this.outputTorquePrev; + // and another smoothing filter, only for pitch analysis + trackingDiff = outputTorque - this.outputTrackingPrev; + + // Apply the smoothing filters + outputTorque = this.outputTorquePrev + (torqueDiff * MotorOutSmoothingFactor); + outputTracking = this.outputTrackingPrev + (trackingDiff * MotorOutSmoothingFactor); + // Compare the smoothed output to a target expected torque to determine effective output pitch in + // non-slip mode (scratch2 disabled): + // NOTE: assumes linear mapping between motor output and wheel velocity (is not 100% correct but it's close enough for now) + trackingTarget = TargetMotorOutput*engine.getValue(this.deck.group, "rate_ratio"); + trackingError = (outputTorque - trackingTarget)/trackingTarget; + + // Only apply nudge/jog if the disc has spun up to the target velocity + if (this.isUpToSpeed == true && Math.abs(trackingError) > 0.02) { //TODO: move this to a config const in header + engine.setValue(this.deck.group, "jog", -trackingError*TurnTableNudgeSensitivity); + // console.warn(outputTorque, outputTracking, trackingError); + } else if (Math.abs(trackingError) < 0.02) { //TODO: move this to a config const in header + // if we've spun all the way up to speed, only then act like it's jogging time. + this.isUpToSpeed = true; + } + } + // New torque becomes old torque + this.outputTorquePrev = outputTorque; + + // If the deck isn't playing, ensure that scratch mode is ON (for scrubbing) + // and reset the "isUpToSpeed" flag + } else { // engine.getValue(this.deck.group, "play") == false) + this.isUpToSpeed = false; + this.isStopped = true; + engine.setValue(this.deck.group, "scratch2_enable", true); + } + // In any other wheel mode, the motor only provides resistance to scrubbing/scratching } else if (this.deck.wheelMode !== wheelModes.motor) { if (TightnessFactor > 0.5) { // Super loose const reduceFactor = (Math.min(0.5, TightnessFactor - 0.5) / 0.5) * 0.7; - velocity = currentSpeed * reduceFactor; + outputTorque = normalizedVelocity * reduceFactor; } else if (TightnessFactor < 0.5) { // Super tight const reduceFactor = (2 - Math.max(0, TightnessFactor) * 4); - velocity = expectedSpeed + Math.min( + outputTorque = targetRate + Math.min( maxVelocity, Math.max( -maxVelocity, - (expectedSpeed - currentSpeed) * reduceFactor + (targetRate - normalizedVelocity) * reduceFactor ) ); } } - velocity *= this.baseFactor; - - if (velocity < 0) { - motorData[1] = 0xe0; - motorData[2] = 0xfe; - velocity = -velocity; - } else if (this.deck.wheelMode === wheelModes.motor && engine.getValue(this.deck.group, "play")) { - velocity += this.zeroSpeedForce; - } - - if (!this.isBlockedByUser() && velocity > MaxWheelForce) { - this.userHold++; - } else if (velocity < MaxWheelForce / 2 && this.userHold > 0) { - this.userHold--; - } - - if (this.isBlockedByUser()) { - engine.setValue(this.deck.group, "scratch2_enable", true); - this.currentMaxWheelForce = this.zeroSpeedForce + parseInt(this.baseFactor * expectedSpeed); - } else if (expectedSpeed && this.userHold === 0 && !this.deck.wheelTouch.touched) { - engine.setValue(this.deck.group, "scratch2_enable", false); - this.currentMaxWheelForce = MaxWheelForce; - } - - velocity = Math.min( - this.currentMaxWheelForce, - Math.floor(velocity) - ); - - motorData[3] = velocity & 0xff; - motorData[4] = velocity >> 8; - return motorData; - } - isBlockedByUser() { - return this.userHold >= 10; + // NOTES ON PHYSICAL MODELING + // Leaving this here as it may be useful to someone down the line. -ZT + + // ========================== + // SLIPMAT PHYSICAL MODELING: + // ========================== + // Calculate the outgoing motor torque using a simulated turntable/slipmat. + // + // ----------- + // Parameters: + // ----------- + // Disc/platter radius (m) and mass (kg) + // --- for a 12" disc, 0.15m and 0.15--0.2kg + // Slipmat coefficients of static and kinetic friction (no unit) + // --- not sure of exact coeff. for a given slipmat material, but I would choose + // one that can *just* handle the motor torque without slipping + // Maximum motor torque (Nm) + // --- for a T1200 this is around 0.15Nm + // + // ------- + // SUMMARY + // ------- + // There are two primary states of the simulation: STICK and SLIP. + // - The disc and platter STICK together unless the performer touches the disc AND + // alters the angular velocity of the jogwheel enough for the disc to SLIP. + // - The disc remains in the SLIP state until its angular velocity returns + // to within a margin of the target velocity, at which point it STICKs again. + // + // ------------ + // STICK state: + // ------------ + // The vinyl disc, slipmat, and platter are stuck to each other and move together. + // In this state, the jogwheel represents the entire assembly. The crown/edge of the + // jogwheel represents the crown/edge of the platter. The performer can hinder or help + // the rotation of the jogwheel, and the motor works to correct any deviation from + // the target angular velocity. + // If the performer perturbs the rotation of the jogwheel while touching the disc + // surface, the same behaviour applies UNLESS the perturbation is large enough to + // defeat the force of friction that keeps everything stuck together. In other words, + // the target and measured velocity are too far apart. At this point, the simulated + // rotation of disc and platter decouple from each other and we enter the SLIP state. + // + // ----------- + // SLIP state: + // ----------- + // The vinyl disc moves at a different rate than the motor, and only (kinetic) friction + // works to bring them back in sync. In this state, **the jogwheel only represents the + // rotation of the disc**. The platter is abstracted and we assume that it rotates at + // the target velocity. + // The torque applied to the jogwheel motor represents the force of friction, which + // pulls in the direction of the target angular velocity. This means that when the + // performer is scratching, the motor is sometimes helping, if the scratch is moving the + // disc closer to synchrony with the platter. + // Anytime the disc and platter velocity are nearly matched, the simulated platter + // regains its influence---in other words, within a certain margin the system returns + // to the STICK state, even if only for a fraction of a second. + // Finally, whether by manually bringing the disc to the target velocity or allowing + // the simulated friction force to do it, the system syncs up and re-enters the STICK + // state. We don't care if the performer is still touching the disc, as long as they + // aren't bringing it out of sync again. + // + // NOTE: + // ----- + // Missing from this simulation is the modeling of the torque transfer *from* the + // disc *to* the platter, as well as the mass/inertia of the platter. + // For example: when a turntable motor is off, spinning the record will transfer some + // torque across the slipmat and the platter will start spinning as well. However, + // adding the platter dynamics to the model will increase the computational complexity + // for what amounts to an edge case at this point. For now, the platter is massless and + // the motor is frictionless for the purposes of the slipmat physics. + // Of course, the jogwheel is *not* frictionless *nor* massless, but my hope is that + // the control system will automatically account for this in practice as it will + // naturally reach a steady-state in its attempts to regulate the jogwheel's angular + // velocity. + + // NOTE 2: + // ------- + // After extensive study and simulation of USB traffic between the S4Mk3 and Traktor, + // I have tuned the input filter, output PID controller and output smoothing filters + // to closely match the behaviour of Traktor. The tuned parameters are not yet linked + // to physical dynamics per se, as the first step is to recreate the commercial system + // before breaking down the control parameters into physical constants. + + // Write the calculated value to the motor output buffer + this.motorBuffMgr.setMotorOutput(this.deckMotorID,outputTorque); + + return true; } } @@ -2809,7 +4006,7 @@ class S4Mk3MixerColumn extends ComponentContainer { inKey: "parameter1", }); this.quickEffectKnob = new Pot({ - group: `[QuickEffectRack1_${this.group}]`, + group: quickFxChannel(this.group), inKey: "super1", }); this.volume = new Pot({ @@ -2866,9 +4063,9 @@ class S4Mk3MixerColumn extends ComponentContainer { if (component instanceof Component) { Object.assign(component, io[property]); if (component instanceof Pot) { - component.inReport = inReports[2]; + component.inReport = inReports[HIDInputPotsReportID]; } else { - component.inReport = inReports[1]; + component.inReport = inReports[HIDInputButtonsReportID]; } component.outReport = outReport; @@ -2944,18 +4141,21 @@ class S4MK3 { } this.inReports = []; - this.inReports[1] = new HIDInputReport(1); + this.inReports[HIDInputButtonsReportID] = new HIDInputReport(HIDInputButtonsReportID); // The master volume, booth volume, headphone mix, and headphone volume knobs // control the controller's audio interface in hardware, so they are not mapped. - this.inReports[2] = new HIDInputReport(2); - this.inReports[3] = new HIDInputReport(3); + this.inReports[HIDInputPotsReportID] = new HIDInputReport(HIDInputPotsReportID); + this.inReports[HIDInputWheelsReportID] = new HIDInputReport(HIDInputWheelsReportID); // There are various of other HID report which doesn't seem to have any // immediate use but it is likely that some useful settings may be found // in them such as the wheel tension. this.outReports = []; - this.outReports[128] = new HIDOutputReport(128, 94); + this.outReports[128] = new HIDOutputReport(128, 94); // FIXME: hardcoded + + // Single motor output buffer for both wheels + this.motorBuffMgr = new MotorOutputBuffMgr(); this.effectUnit1 = new S4Mk3EffectUnit(1, this.inReports, this.outReports[128], { @@ -2998,12 +4198,17 @@ class S4MK3 { // There is no consistent offset between the left and right deck, // so every single components' IO needs to be specified individually // for both decks. + // FIXME: byte offsets should be declared as constants at the head of the file + // for easy reference. Additionally, many of these offsets (for HID input reports 1 and 2) + // are incorrectly offset by 1 byte due to a preceding slice operation on the raw data. + // The slice throws away the report ID once it's no longer needed, but it makes these byte offsets + // confusing as a reference to the real spec of the comms protocol. this.leftDeck = new S4Mk3Deck( [1, 3], [DeckColors[0], DeckColors[2]], { tempoCenterLower: TempoCenterLowerLeft, tempoCenterUpper: TempoCenterUpperLeft, }, this.effectUnit1, this.mixer, - this.inReports, this.outReports[128], + this.inReports, this.outReports[128], this.motorBuffMgr, { playButton: {inByte: 4, inBit: 0, outByte: 55}, cueButton: {inByte: 4, inBit: 1, outByte: 8}, @@ -3045,8 +4250,10 @@ class S4MK3 { {inByte: 3, inBit: 0, outByte: 7}, ], tempoFader: {inByte: 12, inBit: 0, inBitLength: 16, inReport: this.inReports[2], outByte: 11}, - wheelRelative: {inByte: 11, inBit: 0, inBitLength: 16, inReport: this.inReports[3]}, - wheelAbsolute: {inByte: 15, inBit: 0, inBitLength: 16, inReport: this.inReports[3]}, + // FIXME: the wheel position here is one byte offset from its position in the raw data, + // and is hardcoded as such later on. these entries really must be harmonized for readability and robustness. ZT + wheelPosition: {inByte: 11, inBit: 0, inBitLength: 16, inReport: this.inReports[HIDInputWheelsReportID]}, + wheelAbsolute: {inByte: 15, inBit: 0, inBitLength: 16, inReport: this.inReports[HIDInputWheelsReportID]}, wheelTouch: {inByte: 16, inBit: 4}, } ); @@ -3056,7 +4263,7 @@ class S4MK3 { tempoCenterLower: TempoCenterLowerRight, tempoCenterUpper: TempoCenterUpperRight, }, this.effectUnit2, this.mixer, - this.inReports, this.outReports[128], + this.inReports, this.outReports[128], this.motorBuffMgr, { playButton: {inByte: 12, inBit: 0, outByte: 66}, cueButton: {inByte: 14, inBit: 5, outByte: 31}, @@ -3098,8 +4305,10 @@ class S4MK3 { {inByte: 13, inBit: 0, outByte: 30}, ], tempoFader: {inByte: 10, inBit: 0, inBitLength: 16, inReport: this.inReports[2], outByte: 34}, - wheelRelative: {inByte: 39, inBit: 0, inBitLength: 16, inReport: this.inReports[3]}, - wheelAbsolute: {inByte: 43, inBit: 0, inBitLength: 16, inReport: this.inReports[3]}, + // FIXME: the relative wheel value here is one byte offset from its position in the raw data, + // and is hardcoded as such later on. these entries really must be harmonized for readability and robustness. ZT + wheelPosition: {inByte: 39, inBit: 0, inBitLength: 16, inReport: this.inReports[HIDInputWheelsReportID]}, + wheelAbsolute: {inByte: 43, inBit: 0, inBitLength: 16, inReport: this.inReports[HIDInputWheelsReportID]}, wheelTouch: {inByte: 16, inBit: 5}, } ); @@ -3140,25 +4349,43 @@ class S4MK3 { // There are more bytes in the report which seem like they should be for the main // mix meters, but setting those bytes does not do anything, except for lighting // the clip lights on the main mix meters. - controller.sendOutputReport(129, deckMeters.buffer); + controller.sendOutputReport(HIDOutputVUMeterReportID, deckMeters.buffer); }); if (UseMotors) { this.leftMotor = new S4Mk3MotorManager(this.leftDeck); this.rightMotor = new S4Mk3MotorManager(this.rightDeck); - engine.beginTimer(1, this.motorCallback.bind(this)); + // Previously, we were requesting a timer + // interval of 2ms to match sampling rate of wheel sensors, but + // max value was capped in controllerscriptinterfacelegacy.cpp + // normally capped at 20ms but can remove this cap and get 2ms. + // However, this is not consistent across all systems; some can + // only manage 15ms which is not a proper solution. + // Therefore this has been moved to the input handler so that it + // operates as fast as the inputs are received (ideally) + // engine.beginTimer(2, this.motorCallback.bind(this)); + // QT can't guarantee that any given system + // will maintain a sub-20ms timer period. See: + // https://doc.qt.io/qt-5/qobject.html#startTimer } } motorCallback() { - var motorData = new Uint8Array(10); - motorData.set(this.leftMotor.tick()); - motorData.set(this.rightMotor.tick(), 5); - controller.sendOutputReport(49, motorData.buffer, true); + this.leftMotor.tick(); + this.rightMotor.tick(); + controller.sendOutputReport(HIDOutputMotorsReportID, this.motorBuffMgr.getBuff(), true); } incomingData(data) { + // The first byte of the HID report is the reportID const reportId = data[0]; - if (reportId in this.inReports && reportId !== 3) { + if (reportId in this.inReports && reportId !== HIDInputWheelsReportID) { + // Slicing out the first data point is actively harmful to code legibility later on. + // FIXME: Should pass the full data buffer to the input handler. This is the slice + // operation that causes byte offsets to appear incorrect in the code this.inReports[reportId].handleInput(data.buffer.slice(1)); - } else if (reportId === 3) { + } else if (reportId === HIDInputWheelsReportID) { + // FIXME: input report # 3 comes the most frequently, so it should + // be at the start of the conditional block for optimization: + // saves a multi-condition check every time an input comes in. + // The 32 bit unsigned ints at bytes 8 and 36 always have exactly the same value, // so only process one of them. This must be processed before the wheel positions. const oldWheelTimer = wheelTimer; @@ -3172,8 +4399,16 @@ class S4MK3 { if (wheelTimerDelta < 0) { wheelTimerDelta += wheelTimerMax; } - this.leftDeck.wheelRelative.input(view.getUint32(12, true), view.getUint32(8, true)); - this.rightDeck.wheelRelative.input(view.getUint32(40, true), view.getUint32(36, true)); + + // FIXME: the byte offsets below don't match with the ones in the deck definitions + // the offsets here are the correct ones with reference to the entire HID report + this.leftDeck.wheelPosition.input(view.getUint32(12, true), view.getUint32(8, true)); + this.rightDeck.wheelPosition.input(view.getUint32(40, true), view.getUint32(36, true)); + + // Finally, triggering motor output anytime input report #3 is processed. + // This should make the motor output timing more reliable across different platforms + // rather than relying on QTimer. + this.motorCallback(); } else { console.warn(`Unsupported HID repord with ID ${reportId}. Contains: ${data}`); } @@ -3187,6 +4422,14 @@ class S4MK3 { wheelLEDinitReport[0] = 1; controller.sendOutputReport(48, wheelLEDinitReport.buffer); + // Reset the motor's torque to a nil value + const motorData = new Uint8Array([ + 1, 0x20, 1, 0, 0, + 1, 0x20, 1, 0, 0, + + ]); + controller.sendOutputReport(49, motorData.buffer); + // Init wheel timer data wheelTimer = null; wheelTimerDelta = 0; @@ -3195,13 +4438,75 @@ class S4MK3 { for (const repordId of [0x01, 0x02]) { this.inReports[repordId].handleInput(controller.getInputReport(repordId)); } + + updateRuntimeData({ + group: { + "leftdeck": "[Channel1]", + "rightdeck": "[Channel2]", + }, + shift: { + "leftdeck": false, + "rightdeck": false, + }, + scrollingWavefom: { + "[Channel1]": false, + "[Channel2]": false, + "[Channel3]": false, + "[Channel4]": false, + }, + deckColor: { + "[Channel1]": Object.keys(LedColors).indexOf(Object.keys(LedColors).find(key => LedColors[key] === DeckColors[0])) - 1, + "[Channel2]": Object.keys(LedColors).indexOf(Object.keys(LedColors).find(key => LedColors[key] === DeckColors[1])) - 1, + "[Channel3]": Object.keys(LedColors).indexOf(Object.keys(LedColors).find(key => LedColors[key] === DeckColors[2])) - 1, + "[Channel4]": Object.keys(LedColors).indexOf(Object.keys(LedColors).find(key => LedColors[key] === DeckColors[3])) - 1, + }, + rollpadSize: BeatLoopRolls, + beatjumpSize: BeatJumps, + selectedQuickFX: null, + selectedHotcue: { + "[Channel1]": null, + "[Channel2]": null, + "[Channel3]": null, + "[Channel4]": null, + }, + selectedStems: { + "[Channel1]": [0, 0, 0, 0], + "[Channel2]": [0, 0, 0, 0], + "[Channel3]": [0, 0, 0, 0], + "[Channel4]": [0, 0, 0, 0], + }, + viewArtwork: { + "[Channel1]": false, + "[Channel2]": false, + "[Channel3]": false, + "[Channel4]": false, + }, + keyboardMode: { + "[Channel1]": false, + "[Channel2]": false, + "[Channel3]": false, + "[Channel4]": false, + }, + displayBeatloopSize: { + "[Channel1]": false, + "[Channel2]": false, + "[Channel3]": false, + "[Channel4]": false, + }, + padsMode: { + "[Channel1]": 0, + "[Channel2]": 0, + "[Channel3]": 0, + "[Channel4]": 0, + }, + }); } shutdown() { // button LEDs controller.sendOutputReport(128, new Uint8Array(94).fill(0).buffer); // meter LEDs - controller.sendOutputReport(129, new Uint8Array(78).fill(0).buffer); + controller.sendOutputReport(HIDOutputVUMeterReportID, new Uint8Array(78).fill(0).buffer); const wheelOutput = new Uint8Array(40).fill(0); // left wheel LEDs diff --git a/res/controllers/engine-api.d.ts b/res/controllers/engine-api.d.ts index 332ad882d5df..8808161848c2 100644 --- a/res/controllers/engine-api.d.ts +++ b/res/controllers/engine-api.d.ts @@ -1,3 +1,6 @@ +declare interface QtSlot void> { + connect(callback: F): void +} /** ScriptConnectionJSProxy */ @@ -31,10 +34,96 @@ declare interface ScriptConnection { readonly isConnected: boolean; } +/** JavascriptPlayerProxy */ + +declare interface Player { + /** Track's artist or empty string if no track is loaded */ + readonly artist: string + /** Track's title or empty string if no track is loaded */ + readonly title: string + /** Track's album or empty string if no track is loaded */ + readonly album: string + /** Track's album artist or empty string if no track is loaded */ + readonly albumArtist: string + /** Track's genre or empty string if no track is loaded */ + readonly genre: string + /** Track's composer or empty string if no track is loaded */ + readonly composer: string + /** Track's grouping or empty string if no track is loaded */ + readonly grouping: string + /** Track's year of release or empty string if no track is loaded */ + readonly year: string + /** Track's number or empty string if no track is loaded */ + readonly trackNumber: string + /** Total number of tracks in track's album or empty string if no track is loaded */ + readonly trackTotal: string + + /** Emitted when the track is unloaded from the player. */ + trackUnloaded: QtSlot<() => void> + + /** + * Emitted with the new track's artist when a new track is loaded + * to the player or when the current track's metadata change. + */ + artistChanged: QtSlot<(newArtist: string) => void> + /** + * Emitted with the new track title when a new track is loaded + * to the player or when the current track's metadata change. + */ + titleChanged: QtSlot<(newTitle: string) => void> + /** + * Emitted with the new track album when a new track is loaded + * to the player or when the current track's metadata change. + */ + albumChanged: QtSlot<(newAlbum: string) => void> + /** + * Emitted with the new track album artist when a new track is loaded + * to the player or when the current track's metadata change. + */ + albumArtistChanged: QtSlot<(newAlbumArtist: string) => void> + /** + * Emitted with the new track genre when a new track is loaded + * to the player or when the current track's metadata change. + */ + genreChanged: QtSlot<(newGenre: string) => void> + /** + * Emitted with the new track's composer when a new track is loaded + * to the player or when the current track's metadata change. + */ + composerChanged: QtSlot<(newComposer: string) => void> + /** + * Emitted with the new track's grouping when a new track is loaded + * to the player or when the current track's metadata change. + */ + groupingChanged: QtSlot<(newGrouping: string) => void> + /** + * Emitted with the new track year of release when a new track is loaded + * to the player or when the current track's metadata change. + */ + yearChanged: QtSlot<(newYear: string) => void> + /** + * Emitted with the new track number when a new track is loaded + * to the player or when the current track's metadata change. + */ + trackNumberChanged: QtSlot<(newTrackNumber: string) => void> + /** + * Emitted with the new number of track in track's album when a new track + * is loaded to the player or when the current track's metadata change. + */ + trackTotalChanged: QtSlot<(newTrackTotal: string) => void> +} /** ControllerScriptInterfaceLegacy */ declare namespace engine { + /** + * Obtain the player associated with this deck. + * @param group The midi group for this deck; e.g. '[Channel1]' for deck 1. + * @returns The player providing track information and signals, or undefined + * if not player associated with this group was found. + */ + function getPlayer(group: string): Player | undefined + type SettingValue = string | number | boolean; /** * Gets the value of a controller setting diff --git a/res/keyboard/cs_CZ.kbd.cfg b/res/keyboard/cs_CZ.kbd.cfg index d12ee77db1bc..d84ede8a4b6b 100644 --- a/res/keyboard/cs_CZ.kbd.cfg +++ b/res/keyboard/cs_CZ.kbd.cfg @@ -141,6 +141,8 @@ vinylcontrol_cueing Ctrl+Alt+U FileMenu_LoadDeck1 Ctrl+o FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q +LibraryMenu_SearchInCurrentView Ctrl+f +LibraryMenu_SearchInAllTracks Ctrl+Shift+F LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N ViewMenu_ShowSkinSettings Ctrl+1 diff --git a/res/keyboard/da_DK.kbd.cfg b/res/keyboard/da_DK.kbd.cfg index 9255c56f1bf8..9bfcbd54e86a 100644 --- a/res/keyboard/da_DK.kbd.cfg +++ b/res/keyboard/da_DK.kbd.cfg @@ -141,6 +141,8 @@ vinylcontrol_cueing Ctrl+Alt+U FileMenu_LoadDeck1 Ctrl+o FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q +LibraryMenu_SearchInCurrentView Ctrl+f +LibraryMenu_SearchInAllTracks Ctrl+Shift+F LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N ViewMenu_ShowSkinSettings Ctrl+1 diff --git a/res/keyboard/de_CH.kbd.cfg b/res/keyboard/de_CH.kbd.cfg index b4da055bdfe1..d080b43be88a 100644 --- a/res/keyboard/de_CH.kbd.cfg +++ b/res/keyboard/de_CH.kbd.cfg @@ -141,6 +141,8 @@ vinylcontrol_cueing Ctrl+Alt+U FileMenu_LoadDeck1 Ctrl+o FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q +LibraryMenu_SearchInCurrentView Ctrl+f +LibraryMenu_SearchInAllTracks Ctrl+Shift+F LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N ViewMenu_ShowSkinSettings Ctrl+1 diff --git a/res/keyboard/de_DE.kbd.cfg b/res/keyboard/de_DE.kbd.cfg index b912eef63e6b..aa5a976d4c79 100644 --- a/res/keyboard/de_DE.kbd.cfg +++ b/res/keyboard/de_DE.kbd.cfg @@ -141,6 +141,8 @@ vinylcontrol_cueing Ctrl+Alt+U FileMenu_LoadDeck1 Ctrl+o FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q +LibraryMenu_SearchInCurrentView Ctrl+f +LibraryMenu_SearchInAllTracks Ctrl+Shift+F LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N ViewMenu_ShowSkinSettings Ctrl+1 diff --git a/res/keyboard/el_GR.kbd.cfg b/res/keyboard/el_GR.kbd.cfg index 4674298283f4..3571c934a130 100644 --- a/res/keyboard/el_GR.kbd.cfg +++ b/res/keyboard/el_GR.kbd.cfg @@ -145,6 +145,8 @@ vinylcontrol_cueing Ctrl+Alt+Θ FileMenu_LoadDeck1 Ctrl+o FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q +LibraryMenu_SearchInCurrentView Ctrl+f +LibraryMenu_SearchInAllTracks Ctrl+Shift+F LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N ViewMenu_ShowSkinSettings Ctrl+1 diff --git a/res/keyboard/en_US.kbd.cfg b/res/keyboard/en_US.kbd.cfg index 3b8050bfe722..d14ab1b04797 100644 --- a/res/keyboard/en_US.kbd.cfg +++ b/res/keyboard/en_US.kbd.cfg @@ -141,6 +141,8 @@ vinylcontrol_cueing Ctrl+Alt+U FileMenu_LoadDeck1 Ctrl+o FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q +LibraryMenu_SearchInCurrentView Ctrl+f +LibraryMenu_SearchInAllTracks Ctrl+Shift+F LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N ViewMenu_ShowSkinSettings Ctrl+1 diff --git a/res/keyboard/es_ES.kbd.cfg b/res/keyboard/es_ES.kbd.cfg index cfe492491ec9..dc13861bd505 100644 --- a/res/keyboard/es_ES.kbd.cfg +++ b/res/keyboard/es_ES.kbd.cfg @@ -141,6 +141,8 @@ vinylcontrol_cueing Ctrl+Alt+U FileMenu_LoadDeck1 Ctrl+o FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q +LibraryMenu_SearchInCurrentView Ctrl+f +LibraryMenu_SearchInAllTracks Ctrl+Shift+F LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N ViewMenu_ShowSkinSettings Ctrl+1 diff --git a/res/keyboard/fi_FI.kbd.cfg b/res/keyboard/fi_FI.kbd.cfg index 9ec9721d1382..c8c6fc6bed2d 100644 --- a/res/keyboard/fi_FI.kbd.cfg +++ b/res/keyboard/fi_FI.kbd.cfg @@ -141,6 +141,8 @@ vinylcontrol_cueing Ctrl+Alt+U FileMenu_LoadDeck1 Ctrl+o FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q +LibraryMenu_SearchInCurrentView Ctrl+f +LibraryMenu_SearchInAllTracks Ctrl+Shift+F LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N ViewMenu_ShowSkinSettings Ctrl+1 diff --git a/res/keyboard/fr_CH.kbd.cfg b/res/keyboard/fr_CH.kbd.cfg index ed65a7de8a4a..5f3cbfd398a4 100644 --- a/res/keyboard/fr_CH.kbd.cfg +++ b/res/keyboard/fr_CH.kbd.cfg @@ -141,6 +141,8 @@ vinylcontrol_cueing Ctrl+Alt+U FileMenu_LoadDeck1 Ctrl+o FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q +LibraryMenu_SearchInCurrentView Ctrl+f +LibraryMenu_SearchInAllTracks Ctrl+Shift+F LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N ViewMenu_ShowSkinSettings Ctrl+1 diff --git a/res/keyboard/fr_FR.kbd.cfg b/res/keyboard/fr_FR.kbd.cfg index 7c1f50d2077c..dd16215ff720 100644 --- a/res/keyboard/fr_FR.kbd.cfg +++ b/res/keyboard/fr_FR.kbd.cfg @@ -141,6 +141,8 @@ vinylcontrol_cueing Ctrl+Alt+U FileMenu_LoadDeck1 Ctrl+o FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q +LibraryMenu_SearchInCurrentView Ctrl+f +LibraryMenu_SearchInAllTracks Ctrl+Shift+F LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N ViewMenu_ShowSkinSettings Ctrl+1 diff --git a/res/keyboard/it_IT.kbd.cfg b/res/keyboard/it_IT.kbd.cfg index 46063ceb6c3e..3df1bda30e19 100644 --- a/res/keyboard/it_IT.kbd.cfg +++ b/res/keyboard/it_IT.kbd.cfg @@ -141,6 +141,8 @@ vinylcontrol_cueing Ctrl+Alt+U FileMenu_LoadDeck1 Ctrl+o FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q +LibraryMenu_SearchInCurrentView Ctrl+f +LibraryMenu_SearchInAllTracks Ctrl+Shift+F LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N ViewMenu_ShowSkinSettings Ctrl+1 diff --git a/res/keyboard/ru_RU.kbd.cfg b/res/keyboard/ru_RU.kbd.cfg index f506f422c515..38263d70fa35 100644 --- a/res/keyboard/ru_RU.kbd.cfg +++ b/res/keyboard/ru_RU.kbd.cfg @@ -141,6 +141,8 @@ vinylcontrol_cueing Ctrl+Alt+Г FileMenu_LoadDeck1 Ctrl+o FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q +LibraryMenu_SearchInCurrentView Ctrl+f +LibraryMenu_SearchInAllTracks Ctrl+Shift+F LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N ViewMenu_ShowSkinSettings Ctrl+1 diff --git a/res/linux/org.mixxx.Mixxx.metainfo.xml b/res/linux/org.mixxx.Mixxx.metainfo.xml index 205e71f0cdd1..c9bc5b0239e4 100644 --- a/res/linux/org.mixxx.Mixxx.metainfo.xml +++ b/res/linux/org.mixxx.Mixxx.metainfo.xml @@ -96,6 +96,10 @@ Do not edit it manually. --> + + + +

diff --git a/res/qml/Button.qml b/res/qml/Button.qml index c01dd33c48d1..d9143853d4d7 100644 --- a/res/qml/Button.qml +++ b/res/qml/Button.qml @@ -6,116 +6,152 @@ import "Theme" AbstractButton { id: root - property color normalColor: Theme.buttonNormalColor required property color activeColor - property color pressedColor: activeColor property bool highlight: false + property color normalColor: Theme.buttonNormalColor + property color pressedColor: activeColor - implicitWidth: 52 implicitHeight: 26 + implicitWidth: 52 + + background: Item { + anchors.fill: parent + + Rectangle { + id: backgroundImage + anchors.fill: parent + color: Theme.darkGray2 + radius: 0 + } + InnerShadow { + id: bottomInnerEffect + anchors.fill: parent + color: "transparent" + horizontalOffset: -1 + radius: 8 + samples: 16 + source: backgroundImage + spread: 0.3 + verticalOffset: -1 + } + InnerShadow { + id: topInnerEffect + anchors.fill: parent + color: "transparent" + horizontalOffset: 1 + radius: 8 + samples: 16 + source: bottomInnerEffect + spread: 0.3 + verticalOffset: 1 + } + DropShadow { + id: dropEffect + anchors.fill: parent + color: Theme.darkGray + horizontalOffset: 0 + radius: 4.0 + source: topInnerEffect + verticalOffset: 0 + } + } + contentItem: Item { + anchors.fill: parent + + Glow { + id: labelGlow + anchors.fill: parent + color: label.color + radius: 1 + source: label + spread: 0.1 + } + Label { + id: label + anchors.fill: parent + color: root.normalColor + font.bold: true + font.capitalization: Font.AllUppercase + font.family: Theme.fontFamily + font.pixelSize: Theme.buttonFontPixelSize + horizontalAlignment: Text.AlignHCenter + text: root.text + verticalAlignment: Text.AlignVCenter + visible: root.text != null + } + Image { + id: image + anchors.centerIn: parent + asynchronous: true + fillMode: Image.PreserveAspectFit + height: icon.height + source: icon.source + visible: false + width: icon.width + } + ColorOverlay { + anchors.fill: image + antialiasing: true + color: root.normalColor + source: image + visible: icon.source != null + } + } states: [ State { name: "pressed" when: root.pressed PropertyChanges { + color: root.checked ? Theme.accentColor : Theme.darkGray3 target: backgroundImage - source: Theme.imgButtonPressed } - PropertyChanges { - target: label color: root.pressedColor + target: label } - PropertyChanges { target: labelGlow visible: true } - }, State { name: "active" when: (root.highlight || root.checked) && !root.pressed PropertyChanges { + color: Theme.accentColor target: backgroundImage - source: Theme.imgButton } - PropertyChanges { - target: label color: root.activeColor + target: label } - PropertyChanges { target: labelGlow visible: true } - + PropertyChanges { + color: Qt.darker(Theme.accentColor, 3) + target: bottomInnerEffect + } + PropertyChanges { + color: Qt.darker(Theme.accentColor, 3) + target: topInnerEffect + } }, State { name: "inactive" when: !root.checked && !root.highlight && !root.pressed PropertyChanges { - target: backgroundImage - source: Theme.imgButton - } - - PropertyChanges { - target: label color: root.normalColor + target: label } - PropertyChanges { target: labelGlow visible: false } } ] - - background: BorderImage { - id: backgroundImage - - anchors.fill: parent - horizontalTileMode: BorderImage.Stretch - verticalTileMode: BorderImage.Stretch - source: Theme.imgButton - - border { - top: 10 - left: 10 - right: 10 - bottom: 10 - } - } - - contentItem: Item { - anchors.fill: parent - - Glow { - id: labelGlow - - anchors.fill: parent - radius: 5 - spread: 0.1 - color: label.color - source: label - } - - Label { - id: label - - anchors.fill: parent - text: root.text - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.family: Theme.fontFamily - font.capitalization: Font.AllUppercase - font.bold: true - font.pixelSize: Theme.buttonFontPixelSize - color: root.normalColor - } - } } diff --git a/res/qml/Mixxx/Controls/WaveformOverview.qml b/res/qml/Mixxx/Controls/WaveformOverview.qml index 1c217986451a..0ed78fea1562 100644 --- a/res/qml/Mixxx/Controls/WaveformOverview.qml +++ b/res/qml/Mixxx/Controls/WaveformOverview.qml @@ -6,8 +6,9 @@ Mixxx.WaveformOverview { id: root required property string group + readonly property var player: Mixxx.PlayerManager.getPlayer(root.group) - player: Mixxx.PlayerManager.getPlayer(root.group) + track: player.currentTrack Mixxx.ControlProxy { id: trackLoadedControl diff --git a/res/qml/Settings.qml b/res/qml/Settings.qml new file mode 100644 index 000000000000..8a9d39eb9a96 --- /dev/null +++ b/res/qml/Settings.qml @@ -0,0 +1,276 @@ +import "." as Skin +import Mixxx 1.0 as Mixxx +import QtQuick 2.12 +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Shapes +import Qt5Compat.GraphicalEffects +import "Theme" + +Popup { + id: root + + property int activeCategoryIndex: 0 + property list sections: ["SoundHardware", "Library", "Controller", "Interface", "MixerEffect", "AutoDJ", "Broadcast", "Recording", "Analyzer", "StatsPerformance"] + + readonly property var manager: managerItem + + background: Rectangle { + anchors.fill: parent + color: Theme.darkGray2 + opacity: parent.radius < 0 ? Math.max(0.1, 1 + parent.radius / 8) : 1 + radius: 8 + } + contentItem: Item { + anchors.centerIn: parent + height: parent.height - 40 + width: parent.width - 40 + + RowLayout { + anchors.fill: parent + spacing: 0 + + Rectangle { + Layout.fillHeight: true + Layout.preferredWidth: 280 + border.color: Theme.darkGray3 + border.width: 6 + color: Theme.darkGray + + ColumnLayout { + anchors.fill: parent + anchors.margins: 6 + spacing: 0 + + Rectangle { + id: searchSetting + + property bool active: false + property alias input: searchInput + + Layout.fillWidth: true + color: Theme.midGray + height: 30 + + Text { + id: searchInputPlaceholder + anchors.verticalCenter: parent.verticalCenter + color: Theme.white + text: 'Search...' + visible: !parent.active + } + TextInput { + id: searchInput + anchors.verticalCenter: parent.verticalCenter + visible: parent.active + width: parent.width + + onActiveFocusChanged: { + parent.active = activeFocus; + } + onTextEdited: { + root.manager.search(text); + } + } + TapHandler { + onTapped: { + parent.active = true; + searchInput.forceActiveFocus(); + } + } + } + ListView { + id: categoryList + Layout.fillHeight: true + Layout.fillWidth: true + clip: true + focus: true + model: sectionProperties + visible: !searchSetting.active + + delegate: Rectangle { + required property int index + required property var label + + color: ListView.isCurrentItem ? Theme.darkGray3 : Theme.darkGray2 + height: 38 + width: ListView.view.width + + Image { + id: handleImage + anchors.left: parent.left + anchors.leftMargin: 8 + anchors.verticalCenter: parent.verticalCenter + fillMode: Image.PreserveAspectFit + height: 24 + source: "images/gear.svg" + visible: false + } + ColorOverlay { + anchors.fill: handleImage + antialiasing: true + color: parent.ListView.isCurrentItem ? Theme.accentColor : Theme.midGray + source: handleImage + } + Text { + anchors.left: handleImage.right + anchors.leftMargin: 8 + anchors.verticalCenter: parent.verticalCenter + color: Theme.white + font.bold: parent.ListView.isCurrentItem + text: label + } + TapHandler { + onTapped: { + categoryList.currentIndex = index; + } + } + } + } + ListView { + id: settingResultList + Layout.fillHeight: true + Layout.fillWidth: true + clip: true + focus: true + model: root.manager.model + visible: searchSetting.active + + delegate: Rectangle { + required property var display + required property int index + required property var toolTip + required property var whatsThis + + color: Theme.darkGray2 + height: 40 + width: ListView.view.width + + ColumnLayout { + anchors.fill: parent + anchors.margins: 4 + + Text { + Layout.fillWidth: true + Layout.preferredHeight: implicitHeight + color: Theme.white + text: searchSetting.input.text ? display.replace(searchSetting.input.text, `${searchSetting.input.text}`) : display + textFormat: Text.RichText + } + Text { + Layout.fillWidth: true + Layout.preferredHeight: implicitHeight + color: Theme.midGray + font.pixelSize: 10 + text: searchSetting.input.text ? whatsThis.replace(searchSetting.input.text, `${searchSetting.input.text}`) : whatsThis + textFormat: Text.RichText + } + } + TapHandler { + onTapped: { + for (let setting of toolTip) { + setting.activated(); + } + parent.forceActiveFocus(); + } + } + } + } + } + } + ColumnLayout { + Layout.fillHeight: true + Layout.fillWidth: true + + Text { + Layout.alignment: Qt.AlignHCenter + Layout.preferredHeight: 36 + color: Theme.white + font.pixelSize: 16 + font.weight: Font.DemiBold + text: "Settings" + } + Rectangle { + id: tabBar + + readonly property var categoryItem: categoriesLoader.itemAt(categoryList.currentIndex) ? categoriesLoader.itemAt(categoryList.currentIndex).item : null + readonly property int selectedIndex: categoryItem && categoryItem.selectedIndex !== undefined ? categoryItem.selectedIndex : 0 + readonly property var tabs: categoryItem ? categoryItem.tabs : [] + + Layout.fillWidth: true + Layout.preferredHeight: 30 + color: Theme.darkGray3 + visible: tabs?.length > 0 + + RowLayout { + anchors.fill: parent + + Repeater { + model: tabBar.tabs + + Skin.Button { + required property int index + required property string modelData + + Layout.alignment: Qt.AlignHCenter + Layout.preferredHeight: 22 + Layout.preferredWidth: parent.width / (tabBar.tabs.length + 2) + activeColor: Theme.white + checked: tabBar.selectedIndex == index + text: modelData + + onPressed: { + categoriesLoader.itemAt(categoryList.currentIndex).item.selectedIndex = index; + } + } + } + } + } + + Mixxx.SettingParameterManager { + id: managerItem + Layout.fillHeight: true + Layout.fillWidth: true + Layout.leftMargin: 20 + + Repeater { + id: categoriesLoader + model: root.sections + + Loader { + id: category + + required property int index + required property var modelData + + anchors.fill: parent + source: `Settings/${modelData}.qml` + visible: categoryList.currentIndex == index + + // asynchronous: true // Unsupported + onLoaded: { + for (let i = sectionProperties.count; i < index; i++) + sectionProperties.append({}); + sectionProperties.set(index, { + "label": category.item.label + }); + } + + Connections { + function onActivated() { + categoryList.currentIndex = index; + } + + target: category.item + } + } + } + } + } + } + } + + ListModel { + id: sectionProperties + } +} diff --git a/res/qml/Settings/Analyzer.qml b/res/qml/Settings/Analyzer.qml new file mode 100644 index 000000000000..f56980a576e9 --- /dev/null +++ b/res/qml/Settings/Analyzer.qml @@ -0,0 +1,16 @@ +import QtQuick +import Mixxx 1.0 as Mixxx + +Category { + label: "Analyzer" + + Mixxx.SettingParameter { + label: "A grey square" + + Rectangle { + color: 'grey' + height: 20 + width: 20 + } + } +} diff --git a/res/qml/Settings/AutoDJ.qml b/res/qml/Settings/AutoDJ.qml new file mode 100644 index 000000000000..a8dd46caea76 --- /dev/null +++ b/res/qml/Settings/AutoDJ.qml @@ -0,0 +1,16 @@ +import QtQuick +import Mixxx 1.0 as Mixxx + +Category { + label: "AutoDJ" + + Mixxx.SettingParameter { + label: "A black square" + + Rectangle { + color: 'black' + height: 20 + width: 20 + } + } +} diff --git a/res/qml/Settings/Broadcast.qml b/res/qml/Settings/Broadcast.qml new file mode 100644 index 000000000000..d8150886807e --- /dev/null +++ b/res/qml/Settings/Broadcast.qml @@ -0,0 +1,16 @@ +import QtQuick +import Mixxx 1.0 as Mixxx + +Category { + label: "Broadcast" + + Mixxx.SettingParameter { + label: "A yellow square" + + Rectangle { + color: 'yellow' + height: 20 + width: 20 + } + } +} diff --git a/res/qml/Settings/Category.qml b/res/qml/Settings/Category.qml new file mode 100644 index 000000000000..16d232cb12a6 --- /dev/null +++ b/res/qml/Settings/Category.qml @@ -0,0 +1,9 @@ +import QtQuick +import Mixxx 1.0 as Mixxx + +Mixxx.SettingGroup { + id: root + + property int selectedIndex: 0 + property list tabs: [] +} diff --git a/res/qml/Settings/Controller.qml b/res/qml/Settings/Controller.qml new file mode 100644 index 000000000000..1023da91d3b6 --- /dev/null +++ b/res/qml/Settings/Controller.qml @@ -0,0 +1,16 @@ +import QtQuick +import Mixxx 1.0 as Mixxx + +Category { + label: "Controllers" + + Mixxx.SettingParameter { + label: "A orange square" + + Rectangle { + color: 'orange' + height: 20 + width: 20 + } + } +} diff --git a/res/qml/Settings/Interface.qml b/res/qml/Settings/Interface.qml new file mode 100644 index 000000000000..848da7fe71bb --- /dev/null +++ b/res/qml/Settings/Interface.qml @@ -0,0 +1,18 @@ +import QtQuick +import Mixxx 1.0 as Mixxx + +Category { + tabs: ["theme & colour", "waveform", "decks"] + + label: "Interface" + + Mixxx.SettingParameter { + label: "A pink square" + + Rectangle { + color: 'pink' + height: 20 + width: 20 + } + } +} diff --git a/res/qml/Settings/Library.qml b/res/qml/Settings/Library.qml new file mode 100644 index 000000000000..1ca75978c25e --- /dev/null +++ b/res/qml/Settings/Library.qml @@ -0,0 +1,16 @@ +import QtQuick +import Mixxx 1.0 as Mixxx + +Category { + label: "Library" + + Mixxx.SettingParameter { + label: "A blue square" + + Rectangle { + color: 'blue' + height: 20 + width: 20 + } + } +} diff --git a/res/qml/Settings/MixerEffect.qml b/res/qml/Settings/MixerEffect.qml new file mode 100644 index 000000000000..3be92fc7c17f --- /dev/null +++ b/res/qml/Settings/MixerEffect.qml @@ -0,0 +1,16 @@ +import QtQuick +import Mixxx 1.0 as Mixxx + +Category { + label: "Mixer & Effects" + + Mixxx.SettingParameter { + label: "A green square" + + Rectangle { + color: 'green' + height: 20 + width: 20 + } + } +} diff --git a/res/qml/Settings/Recording.qml b/res/qml/Settings/Recording.qml new file mode 100644 index 000000000000..16ac87057aae --- /dev/null +++ b/res/qml/Settings/Recording.qml @@ -0,0 +1,16 @@ +import QtQuick +import Mixxx 1.0 as Mixxx + +Category { + label: "Recording" + + Mixxx.SettingParameter { + label: "A red square" + + Rectangle { + color: 'red' + height: 20 + width: 20 + } + } +} diff --git a/res/qml/Settings/SoundHardware.qml b/res/qml/Settings/SoundHardware.qml new file mode 100644 index 000000000000..b1b6f70c9de2 --- /dev/null +++ b/res/qml/Settings/SoundHardware.qml @@ -0,0 +1,64 @@ +import QtQuick +import Mixxx 1.0 as Mixxx + +Category { + id: root + + label: "Sound hardware" + tabs: ["engine", "delays", "stats"] + + Mixxx.SettingGroup { + label: "Engine" + visible: root.selectedIndex == 0 + + onActivated: { + root.selectedIndex = 0; + } + + Mixxx.SettingParameter { + label: "A cyan square" + + Rectangle { + color: 'cyan' + height: 20 + width: 20 + } + } + } + Mixxx.SettingGroup { + label: "Delays" + visible: root.selectedIndex == 1 + + onActivated: { + root.selectedIndex = 1; + } + + Mixxx.SettingParameter { + label: "A magenta square" + + Rectangle { + color: 'magenta' + height: 20 + width: 20 + } + } + } + Mixxx.SettingGroup { + label: "Stats" + visible: root.selectedIndex == 2 + + onActivated: { + root.selectedIndex = 2; + } + + Mixxx.SettingParameter { + label: "A white square" + + Rectangle { + color: 'white' + height: 20 + width: 20 + } + } + } +} diff --git a/res/qml/Settings/StatsPerformance.qml b/res/qml/Settings/StatsPerformance.qml new file mode 100644 index 000000000000..a61b68730339 --- /dev/null +++ b/res/qml/Settings/StatsPerformance.qml @@ -0,0 +1,16 @@ +import QtQuick +import Mixxx 1.0 as Mixxx + +Category { + label: "Stats & Performance" + + Mixxx.SettingParameter { + label: "A grey square" + + Rectangle { + color: 'grey' + height: 20 + width: 20 + } + } +} diff --git a/res/qml/Theme/Theme.qml b/res/qml/Theme/Theme.qml index ca1b4d498e51..375911fcd0e8 100644 --- a/res/qml/Theme/Theme.qml +++ b/res/qml/Theme/Theme.qml @@ -2,65 +2,68 @@ import QtQuick 2.12 pragma Singleton QtObject { - property color white: "#e3d7fb" - property color yellow: "#fca001" - property color red: "#ea2a4e" + property color accentColor: "#3a60be" + property color backgroundColor: "#1e1e20" property color blue: "#01dcfc" - property color green: "#85c85b" - property color lightGray: "#747474" - property color lightGray2: "#b0b0b0" - property color midGray: "#696969" - property color darkGray: "#0f0f0f" - property color darkGray2: "#2e2e2e" - property color eqHighColor: white - property color eqMidColor: white - property color eqLowColor: white - property color eqFxColor: red - property color effectColor: yellow - property color effectUnitColor: red property color bpmSliderBarColor: green - property color volumeSliderBarColor: blue - property color gainKnobColor: blue - property color samplerColor: blue - property color crossfaderOrientationColor: lightGray + property color buttonNormalColor: midGray property color crossfaderBarColor: red - property color toolbarBackgroundColor: darkGray2 - property color pflActiveButtonColor: blue - property color backgroundColor: "#1e1e20" + property color crossfaderOrientationColor: lightGray + property color darkGray: "#0f0f0f" + property color darkGray2: "#2e2e2e" + property color darkGray3: "#3F3F3F" property color deckActiveColor: green property color deckBackgroundColor: darkGray - property color knobBackgroundColor: "#262626" property color deckLineColor: darkGray2 property color deckTextColor: lightGray2 + property color effectColor: yellow + property color effectUnitColor: red property color embeddedBackgroundColor: "#a0000000" - property color buttonNormalColor: midGray + property color eqFxColor: red + property color eqHighColor: white + property color eqLowColor: white + property color eqMidColor: white + property color gainKnobColor: blue + property color green: "#85c85b" + property color knobBackgroundColor: "#262626" + property color lightGray: "#747474" + property color lightGray2: "#b0b0b0" + property color midGray: "#696969" + property color pflActiveButtonColor: blue + property color red: "#ea2a4e" + property color samplerColor: blue property color textColor: lightGray2 property color toolbarActiveColor: white - property color waveformPrerollColor: midGray - property color waveformPostrollColor: midGray + property color toolbarBackgroundColor: darkGray2 + property color volumeSliderBarColor: blue + property color warningColor: "#7D3B3B" property color waveformBeatColor: lightGray property color waveformCursorColor: white property color waveformMarkerDefault: '#ff7a01' - property color waveformMarkerLabel: Qt.rgba(255, 255, 255, 0.8) property color waveformMarkerIntroOutroColor: '#2c5c9a' + property color waveformMarkerLabel: Qt.rgba(255, 255, 255, 0.8) property color waveformMarkerLoopColor: '#00b400' property color waveformMarkerLoopColorDisabled: '#FFFFFF' - property string fontFamily: "Open Sans" - property int textFontPixelSize: 14 + property color waveformPostrollColor: midGray + property color waveformPrerollColor: midGray + property color white: "#D9D9D9" + property color yellow: "#fca001" property int buttonFontPixelSize: 10 + property int textFontPixelSize: 14 + property string fontFamily: "Open Sans" + property string imgBpmSliderBackground: "images/slider_bpm.svg" property string imgButton: "images/button.svg" property string imgButtonPressed: "images/button_pressed.svg" - property string imgSliderHandle: "images/slider_handle.svg" - property string imgBpmSliderBackground: "images/slider_bpm.svg" - property string imgVolumeSliderBackground: "images/slider_volume.svg" - property string imgCrossfaderHandle: "images/slider_handle_crossfader.svg" property string imgCrossfaderBackground: "images/slider_crossfader.svg" - property string imgMicDuckingSliderHandle: "images/slider_handle_micducking.svg" - property string imgMicDuckingSlider: "images/slider_micducking.svg" - property string imgPopupBackground: imgButton + property string imgCrossfaderHandle: "images/slider_handle_crossfader.svg" property string imgKnob: "images/knob.svg" - property string imgKnobShadow: "images/knob_shadow.svg" property string imgKnobMini: "images/miniknob.svg" property string imgKnobMiniShadow: "images/miniknob_shadow.svg" + property string imgKnobShadow: "images/knob_shadow.svg" + property string imgMicDuckingSlider: "images/slider_micducking.svg" + property string imgMicDuckingSliderHandle: "images/slider_handle_micducking.svg" + property string imgPopupBackground: imgButton property string imgSectionBackground: "images/section.svg" + property string imgSliderHandle: "images/slider_handle.svg" + property string imgVolumeSliderBackground: "images/slider_volume.svg" } diff --git a/res/qml/WaveformDisplay.qml b/res/qml/WaveformDisplay.qml index bc3b74c59f68..7267b22edf0e 100644 --- a/res/qml/WaveformDisplay.qml +++ b/res/qml/WaveformDisplay.qml @@ -10,6 +10,8 @@ Item { required property string group property bool splitStemTracks: false + readonly property string zoomGroup: Mixxx.Config.waveformZoomSynchronization() ? "[Channel1]" : group + enum MouseStatus { Normal, Bending, @@ -22,6 +24,13 @@ Item { zoom: zoomControl.value backgroundColor: "#5e000000" + Behavior on zoom { + SmoothedAnimation { + duration: 500 + velocity: -1 + } + } + Mixxx.WaveformRendererEndOfTrack { color: '#ff8872' endOfTrackWarningTime: 30 @@ -62,11 +71,11 @@ Item { } } - Mixxx.WaveformRendererRGB { + Mixxx.WaveformRendererFiltered { axesColor: '#a1a1a1a1' - lowColor: '#ff2154d7' - midColor: '#cfb26606' - highColor: '#e5029c5c' + lowColor: '#2154D7' + midColor: '#97632D' + highColor: '#D5C2A2' gainAll: 1.0 gainLow: 1.0 @@ -84,8 +93,8 @@ Item { } Mixxx.WaveformRendererMark { - playMarkerColor: 'cyan' - playMarkerBackground: 'orange' + playMarkerColor: '#D9D9D9' + playMarkerBackground: '#D9D9D9' defaultMark: Mixxx.WaveformMark { align: "bottom|right" color: "#00d9ff" @@ -180,8 +189,14 @@ Item { Mixxx.ControlProxy { id: zoomControl - group: root.group + group: root.zoomGroup key: "waveform_zoom" + + Component.onCompleted: { + if (group == root.group) { + value = Mixxx.Config.waveformDefaultZoom() + } + } } MouseArea { @@ -243,10 +258,10 @@ Item { mouseStatus = WaveformDisplay.MouseStatus.Normal; } - onWheel: { - if (wheel.angleDelta.y < 0 && zoomControl.value > 1) { + onWheel: (mouse) => { + if (mouse.angleDelta.y < 0 && zoomControl.value > 1) { zoomControl.value -= 1; - } else if (wheel.angleDelta.y > 0 && zoomControl.value < 10.0) { + } else if (mouse.angleDelta.y > 0 && zoomControl.value < 10.0) { zoomControl.value += 1; } } diff --git a/res/qml/WaveformRow.qml b/res/qml/WaveformRow.qml deleted file mode 100644 index be129fc42bfa..000000000000 --- a/res/qml/WaveformRow.qml +++ /dev/null @@ -1,380 +0,0 @@ -import "." as Skin -import Mixxx 1.0 as Mixxx -import QtQuick 2.14 -import QtQuick.Shapes 1.12 -import "Theme" - -Item { - id: root - - enum MouseStatus { - Normal, - Bending, - Scratching - } - - property string group // required - property var deckPlayer: Mixxx.PlayerManager.getPlayer(group) - - Item { - id: waveformContainer - - property real duration: samplesControl.value / sampleRateControl.value - - anchors.fill: parent - clip: true - - Mixxx.ControlProxy { - id: samplesControl - - group: root.group - key: "track_samples" - } - - Mixxx.ControlProxy { - id: sampleRateControl - - group: root.group - key: "track_samplerate" - } - - Mixxx.ControlProxy { - id: playPositionControl - - group: root.group - key: "playposition" - } - - Mixxx.ControlProxy { - id: rateRatioControl - - group: root.group - key: "rate_ratio" - } - - Mixxx.ControlProxy { - id: zoomControl - - group: root.group - key: "waveform_zoom" - } - - Mixxx.ControlProxy { - id: introStartPosition - - group: root.group - key: "intro_start_position" - } - - Mixxx.ControlProxy { - id: introEndPosition - - group: root.group - key: "intro_end_position" - } - - Mixxx.ControlProxy { - id: outroStartPosition - - group: root.group - key: "outro_start_position" - } - - Mixxx.ControlProxy { - id: outroEndPosition - - group: root.group - key: "outro_end_position" - } - - Mixxx.ControlProxy { - id: loopStartPosition - - group: root.group - key: "loop_start_position" - } - - Mixxx.ControlProxy { - id: loopEndPosition - - group: root.group - key: "loop_end_position" - } - - Mixxx.ControlProxy { - id: loopEnabled - - group: root.group - key: "loop_enabled" - } - - Mixxx.ControlProxy { - id: mainCuePosition - - group: root.group - key: "cue_point" - } - - Item { - id: waveform - - property real effectiveZoomFactor: (1 / rateRatioControl.value) * (100 / zoomControl.value) - - width: waveformContainer.duration * effectiveZoomFactor - height: parent.height - x: playMarker.screenPosition * waveformContainer.width - playPositionControl.value * width - visible: root.deckPlayer.isLoaded - - WaveformShader { - group: root.group - anchors.fill: parent - } - - Shape { - id: preroll - - property real triangleHeight: waveform.height - property real triangleWidth: 0.25 * waveform.effectiveZoomFactor - property int numTriangles: Math.ceil(width / triangleWidth) - - anchors.top: waveform.top - anchors.right: waveform.left - width: Math.max(0, waveform.x) - height: waveform.height - - ShapePath { - strokeColor: Theme.waveformPrerollColor - strokeWidth: 1 - fillColor: "transparent" - - PathMultiline { - paths: { - let p = []; - for (let i = 0; i < preroll.numTriangles; i++) { - p.push([ - Qt.point(preroll.width - i * preroll.triangleWidth, preroll.triangleHeight / 2), - Qt.point(preroll.width - (i + 1) * preroll.triangleWidth, 0), - Qt.point(preroll.width - (i + 1) * preroll.triangleWidth, preroll.triangleHeight), - Qt.point(preroll.width - i * preroll.triangleWidth, preroll.triangleHeight / 2), - ]); - } - return p; - } - } - } - } - - Shape { - id: postroll - - property real triangleHeight: waveform.height - property real triangleWidth: 0.25 * waveform.effectiveZoomFactor - property int numTriangles: Math.ceil(width / triangleWidth) - - anchors.top: waveform.top - anchors.left: waveform.right - width: waveformContainer.width / 2 - height: waveform.height - - ShapePath { - strokeColor: Theme.waveformPostrollColor - strokeWidth: 1 - fillColor: "transparent" - - PathMultiline { - paths: { - let p = []; - for (let i = 0; i < postroll.numTriangles; i++) { - p.push([ - Qt.point(i * postroll.triangleWidth, postroll.triangleHeight / 2), - Qt.point((i + 1) * postroll.triangleWidth, 0), - Qt.point((i + 1) * postroll.triangleWidth, postroll.triangleHeight), - Qt.point(i * postroll.triangleWidth, postroll.triangleHeight / 2), - ]); - } - return p; - } - } - } - } - - Repeater { - model: root.deckPlayer.beatsModel - - Rectangle { - property real alpha: 0.9 // TODO: Make this configurable (i.e., "[Waveform],beatGridAlpha" config option) - - width: 1 - height: waveform.height - x: (framePosition * 2 / samplesControl.value) * waveform.width - color: Theme.waveformBeatColor - } - } - - Skin.WaveformIntroOutro { - id: intro - - visible: introStartPosition.value != -1 || introEndPosition.value != -1 - - height: waveform.height - x: ((introStartPosition.value != -1 ? introStartPosition.value : introEndPosition.value) / samplesControl.value) * waveform.width - width: introEndPosition.value == -1 ? 0 : ((introEndPosition.value - introStartPosition.value) / samplesControl.value) * waveform.width - } - - Skin.WaveformIntroOutro { - id: outro - - visible: outroStartPosition.value != -1 || outroEndPosition.value != -1 - isIntro: false - - height: waveform.height - x: ((outroStartPosition.value != -1 ? outroStartPosition.value : outroEndPosition.value) / samplesControl.value) * waveform.width - width: outroEndPosition.value == -1 || outroStartPosition.value == -1 ? 0 : ((outroEndPosition.value - outroStartPosition.value) / samplesControl.value) * waveform.width - } - - Skin.WaveformLoop { - id: loop - - visible: loopStartPosition.value != -1 && loopEndPosition.value != -1 - - height: waveform.height - x: (loopStartPosition.value / samplesControl.value) * waveform.width - width: ((loopEndPosition.value - loopStartPosition.value) / samplesControl.value) * waveform.width - enabled: loopEnabled.value - } - - Repeater { - model: root.deckPlayer.hotcuesModel - - Item { - id: cue - - required property int startPosition - required property int endPosition - required property string label - required property bool isLoop - required property int hotcueNumber - - Skin.WaveformHotcue { - group: root.group - hotcueNumber: cue.hotcueNumber + 1 - label: cue.label - isLoop: cue.isLoop - - x: (startPosition * 2 / samplesControl.value) * waveform.width - width: cue.isLoop ? ((endPosition - startPosition) * 2 / samplesControl.value) * waveform.width : null - height: waveform.height - } - } - } - - Skin.WaveformCue { - id: maincue - - height: waveform.height - x: (mainCuePosition.value / samplesControl.value) * waveform.width - } - } - } - - Shape { - id: playMarkerShape - - anchors.fill: parent - - ShapePath { - id: playMarker - - property real screenPosition: 0.5 - - startX: playMarkerShape.width * playMarker.screenPosition - startY: 0 - strokeColor: Theme.waveformCursorColor - strokeWidth: 1 - - PathLine { - id: marker - - x: playMarkerShape.width * playMarker.screenPosition - y: playMarkerShape.height - } - } - } - - Mixxx.ControlProxy { - id: scratchPositionEnableControl - - group: root.group - key: "scratch_position_enable" - } - - Mixxx.ControlProxy { - id: scratchPositionControl - - group: root.group - key: "scratch_position" - } - - Mixxx.ControlProxy { - id: wheelControl - - group: root.group - key: "wheel" - } - - MouseArea { - property int mouseStatus: WaveformRow.MouseStatus.Normal - property point mouseAnchor: Qt.point(0, 0) - - anchors.fill: parent - acceptedButtons: Qt.LeftButton | Qt.RightButton - onPressed: { - mouseAnchor = Qt.point(mouse.x, mouse.y); - if (mouse.button == Qt.LeftButton) { - if (mouseStatus == WaveformRow.MouseStatus.Bending) - wheelControl.parameter = 0.5; - - mouseStatus = WaveformRow.MouseStatus.Scratching; - scratchPositionEnableControl.value = 1; - // TODO: Calculate position properly - scratchPositionControl.value = -mouse.x * waveform.effectiveZoomFactor * 2; - console.log(mouse.x); - } else { - if (mouseStatus == WaveformRow.MouseStatus.Scratching) - scratchPositionEnableControl.value = 0; - - wheelControl.parameter = 0.5; - mouseStatus = WaveformRow.MouseStatus.Bending; - } - } - onPositionChanged: { - switch (mouseStatus) { - case WaveformRow.MouseStatus.Bending: { - const diff = mouse.x - mouseAnchor.x; - // Start at the middle of [0.0, 1.0], and emit values based on how far - // the mouse has traveled horizontally. Note, for legacy (MIDI) reasons, - // this is tuned to 127. - const v = 0.5 + (diff / 1270); - // clamp to [0.0, 1.0] - wheelControl.parameter = Mixxx.MathUtils.clamp(v, 0, 1); - break; - }; - case WaveformRow.MouseStatus.Scratching: - // TODO: Calculate position properly - scratchPositionControl.value = -mouse.x * waveform.effectiveZoomFactor * 2; - break; - } - } - onReleased: { - switch (mouseStatus) { - case WaveformRow.MouseStatus.Bending: - wheelControl.parameter = 0.5; - break; - case WaveformRow.MouseStatus.Scratching: - scratchPositionEnableControl.value = 0; - break; - } - mouseStatus = WaveformRow.MouseStatus.Normal; - } - } -} diff --git a/res/qml/WaveformShader.qml b/res/qml/WaveformShader.qml deleted file mode 100644 index 33de4acde0cf..000000000000 --- a/res/qml/WaveformShader.qml +++ /dev/null @@ -1,88 +0,0 @@ -import Mixxx 1.0 as Mixxx -import QtQuick 2.12 - -ShaderEffect { - id: root - - property string group // required - property var deckPlayer: Mixxx.PlayerManager.getPlayer(group) - property size framebufferSize: Qt.size(width, height) - property int waveformLength: root.deckPlayer.waveformLength - property int textureSize: root.deckPlayer.waveformTextureSize - property int textureStride: root.deckPlayer.waveformTextureStride - property real firstVisualIndex: 1 - property real lastVisualIndex: root.deckPlayer.waveformLength / 2 - property color axesColor: "#FFFFFF" - property color highColor: "#0000FF" - property color midColor: "#00FF00" - property color lowColor: "#FF0000" - property real highGain: filterWaveformEnableControl.value ? (filterHighKillControl.value ? 0 : filterHighControl.value) : 1 - property real midGain: filterWaveformEnableControl.value ? (filterMidKillControl.value ? 0 : filterMidControl.value) : 1 - property real lowGain: filterWaveformEnableControl.value ? (filterLowKillControl.value ? 0 : filterLowControl.value) : 1 - property real allGain: pregainControl.value - property Image waveformTexture - - fragmentShader: "qrc:/shaders/rgbsignal_qml.frag.qsb" - - Mixxx.ControlProxy { - id: pregainControl - - group: root.group - key: "pregain" - } - - Mixxx.ControlProxy { - id: filterWaveformEnableControl - - group: root.group - key: "filterWaveformEnable" - } - - Mixxx.ControlProxy { - id: filterHighControl - - group: "[EqualizerRack1_" + root.group + "_Effect1]" - key: "parameter3" - } - - Mixxx.ControlProxy { - id: filterHighKillControl - - group: "[EqualizerRack1_" + root.group + "_Effect1]" - key: "button_parameter3" - } - - Mixxx.ControlProxy { - id: filterMidControl - - group: "[EqualizerRack1_" + root.group + "_Effect1]" - key: "parameter2" - } - - Mixxx.ControlProxy { - id: filterMidKillControl - - group: "[EqualizerRack1_" + root.group + "_Effect1]" - key: "button_parameter2" - } - - Mixxx.ControlProxy { - id: filterLowControl - - group: "[EqualizerRack1_" + root.group + "_Effect1]" - key: "parameter1" - } - - Mixxx.ControlProxy { - id: filterLowKillControl - - group: "[EqualizerRack1_" + root.group + "_Effect1]" - key: "button_parameter1" - } - - waveformTexture: Image { - visible: false - layer.enabled: false - source: root.deckPlayer.waveformTexture - } -} diff --git a/res/qml/images/gear.svg b/res/qml/images/gear.svg new file mode 100644 index 000000000000..c4f6cebc1aaa --- /dev/null +++ b/res/qml/images/gear.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + diff --git a/res/qml/main.qml b/res/qml/main.qml index ced85a02856e..f79470440c41 100644 --- a/res/qml/main.qml +++ b/res/qml/main.qml @@ -1,86 +1,79 @@ import "." as Skin import Mixxx 1.0 as Mixxx import QtQuick 2.12 -import QtQuick.Controls 2.12 +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects import "Theme" ApplicationWindow { id: root + property alias maximizeLibrary: maximizeLibraryButton.checked property alias show4decks: show4DecksButton.checked property alias showEffects: showEffectsButton.checked property alias showSamplers: showSamplersButton.checked - property alias maximizeLibrary: maximizeLibraryButton.checked - width: 1920 - height: 1080 color: Theme.backgroundColor + height: 1080 visible: true + width: 1920 Column { + id: content anchors.fill: parent + move: Transition { + NumberAnimation { + duration: 150 + properties: "x,y" + } + } + Rectangle { id: toolbar - - width: parent.width - height: 36 color: Theme.toolbarBackgroundColor + height: 36 radius: 1 + width: parent.width - Row { - padding: 5 - spacing: 5 + RowLayout { + anchors.fill: parent Skin.Button { id: show4DecksButton - - text: "4 Decks" activeColor: Theme.white checkable: true + text: "4 Decks" } - Skin.Button { id: maximizeLibraryButton - - text: "Library" activeColor: Theme.white checkable: true + text: "Library" } - Skin.Button { id: showEffectsButton - - text: "Effects" activeColor: Theme.white checkable: true + text: "Effects" } - Skin.Button { id: showSamplersButton - - text: "Sampler" activeColor: Theme.white checkable: true + text: "Sampler" } - - Skin.Button { - id: showPreferencesButton - - text: "Prefs" - activeColor: Theme.white - onClicked: { - Mixxx.PreferencesDialog.show(); - } + Item { + Layout.fillWidth: true } - Skin.Button { id: showDevToolsButton - - text: "Develop" activeColor: Theme.white checkable: true checked: devToolsWindow.visible + text: "Develop" + onClicked: { if (devToolsWindow.visible) devToolsWindow.close(); @@ -90,132 +83,152 @@ ApplicationWindow { DeveloperToolsWindow { id: devToolsWindow - - width: 640 height: 480 + width: 640 + } + } + Skin.Button { + id: showPreferencesButton + activeColor: Theme.white + checked: settingsPopup.opened + icon.height: 16 + icon.source: "images/gear.svg" + icon.width: 16 + implicitWidth: implicitHeight + + onClicked: { + if (!settingsPopup.opened) { + settingsPopup.open(); + } + } + onPressAndHold: { + Mixxx.PreferencesDialog.show(); } } } } - Skin.WaveformDisplay { id: deck3waveform - group: "[Channel3]" - width: root.width height: 120 visible: root.show4decks && !root.maximizeLibrary + width: root.width - FadeBehavior on visible { + FadeBehavior on visible { fadeTarget: deck3waveform } } - Skin.WaveformDisplay { id: deck1waveform - group: "[Channel1]" - width: root.width height: 120 visible: !root.maximizeLibrary + width: root.width - FadeBehavior on visible { + FadeBehavior on visible { fadeTarget: deck1waveform } } - Skin.WaveformDisplay { id: deck2waveform - group: "[Channel2]" - width: root.width height: 120 visible: !root.maximizeLibrary + width: root.width - FadeBehavior on visible { + FadeBehavior on visible { fadeTarget: deck2waveform } } - Skin.WaveformDisplay { id: deck4waveform - group: "[Channel4]" - width: root.width height: 120 visible: root.show4decks && !root.maximizeLibrary + width: root.width - FadeBehavior on visible { + FadeBehavior on visible { fadeTarget: deck4waveform } } - Skin.DeckRow { id: decks12 - leftDeckGroup: "[Channel1]" + minimized: root.maximizeLibrary rightDeckGroup: "[Channel2]" width: parent.width - minimized: root.maximizeLibrary } - Skin.CrossfaderRow { id: crossfader - crossfaderWidth: decks12.mixer.width - width: parent.width visible: !root.maximizeLibrary + width: parent.width - Skin.FadeBehavior on visible { + Skin.FadeBehavior on visible { fadeTarget: crossfader } } - Skin.DeckRow { id: decks34 - leftDeckGroup: "[Channel3]" - rightDeckGroup: "[Channel4]" - width: parent.width minimized: root.maximizeLibrary + rightDeckGroup: "[Channel4]" visible: root.show4decks + width: parent.width - Skin.FadeBehavior on visible { + Skin.FadeBehavior on visible { fadeTarget: decks34 } } - Skin.SamplerRow { id: samplers - - width: parent.width visible: root.showSamplers + width: parent.width - Skin.FadeBehavior on visible { + Skin.FadeBehavior on visible { fadeTarget: samplers } } - Skin.EffectRow { id: effects - - width: parent.width visible: root.showEffects + width: parent.width - Skin.FadeBehavior on visible { + Skin.FadeBehavior on visible { fadeTarget: effects } } - Skin.Library { - width: parent.width height: parent.height - y + width: parent.width } - - move: Transition { - NumberAnimation { - properties: "x,y" - duration: 150 + } + Skin.Settings { + id: settingsPopup + height: Math.max(840, parent.height * 0.7) + modal: true + width: Math.max(1400, parent.width * 0.8) + x: Math.round((parent.width - width) / 2) + y: Math.round((parent.height - height) / 2) + + Overlay.modal: Rectangle { + id: overlayModal + property real radius: 12 + + readonly property bool hasHardwareAcceleration: Mixxx.Config.useAcceleration() + + anchors.fill: parent + color: Qt.alpha('#00000010', hasHardwareAcceleration ? 1.0 : 0.6) + + Repeater { + model: hasHardwareAcceleration ? 1 : 0 + GaussianBlur { + anchors.fill: overlayModal + deviation: 4 + radius: Math.max(0, overlayModal.radius) + samples: 16 + source: content + } } } } diff --git a/src/controllers/scripting/controllerscriptenginebase.cpp b/src/controllers/scripting/controllerscriptenginebase.cpp index 075ef6f5a8a9..2137803be3e2 100644 --- a/src/controllers/scripting/controllerscriptenginebase.cpp +++ b/src/controllers/scripting/controllerscriptenginebase.cpp @@ -30,12 +30,17 @@ ControllerScriptEngineBase::ControllerScriptEngineBase( qRegisterMetaType("QMessageBox::StandardButton"); } -#ifdef MIXXX_USE_QML +void ControllerScriptEngineBase::registerPlayerManager( + std::shared_ptr pPlayerManager) { + ControllerScriptEngineBase::s_pPlayerManager = pPlayerManager; +} + void ControllerScriptEngineBase::registerTrackCollectionManager( std::shared_ptr pTrackCollectionManager) { s_pTrackCollectionManager = std::move(pTrackCollectionManager); } +#ifdef MIXXX_USE_QML void ControllerScriptEngineBase::handleQMLErrors(const QList& qmlErrors) { for (const QQmlError& error : std::as_const(qmlErrors)) { showQMLExceptionDialog(error, m_bErrorsAreFatal); @@ -116,6 +121,24 @@ void ControllerScriptEngineBase::reload() { initialize(); } +QObject* ControllerScriptEngineBase::getPlayer(const QString& group) { + VERIFY_OR_DEBUG_ASSERT(s_pPlayerManager != nullptr) { + qCritical() << "Uninitialized PlayerManager"; + return nullptr; + } + auto* const player = s_pPlayerManager->getPlayer(group); + if (!player) { + qWarning() << "PlayerManagerProxy failed to find player for group" << group; + return nullptr; + } + + // Don't set a parent here, so that the QML engine deletes the object when + // the corresponding JS object is garbage collected. + JavascriptPlayerProxy* pPlayerProxy = new JavascriptPlayerProxy(player, nullptr); + QJSEngine::setObjectOwnership(pPlayerProxy, QJSEngine::JavaScriptOwnership); + return pPlayerProxy; +} + bool ControllerScriptEngineBase::executeFunction( QJSValue* pFunctionObject, const QJSValueList& args) { // This function is called from outside the controller engine, so we can't diff --git a/src/controllers/scripting/controllerscriptenginebase.h b/src/controllers/scripting/controllerscriptenginebase.h index 2129184b641b..05bdaba7a92f 100644 --- a/src/controllers/scripting/controllerscriptenginebase.h +++ b/src/controllers/scripting/controllerscriptenginebase.h @@ -7,6 +7,8 @@ #include #include +#include "javascriptplayerproxy.h" +#include "mixer/playermanager.h" #include "util/runtimeloggingcategory.h" #ifdef MIXXX_USE_QML #include "controllers/controllerenginethreadcontrol.h" @@ -14,9 +16,7 @@ class Controller; class QJSEngine; -#ifdef MIXXX_USE_QML class TrackCollectionManager; -#endif /// ControllerScriptEngineBase manages the JavaScript engine for controller scripts. /// ControllerScriptModuleEngine implements the current system using JS modules. @@ -32,6 +32,8 @@ class ControllerScriptEngineBase : public QObject { bool executeFunction(QJSValue* pFunctionObject, const QJSValueList& arguments = {}); + QObject* getPlayer(const QString& group); + /// Shows a UI dialog notifying of a script evaluation error. /// Precondition: QJSValue.isError() == true void showScriptExceptionDialog(const QJSValue& evaluationResult, bool bFatal = false); @@ -53,10 +55,11 @@ class ControllerScriptEngineBase : public QObject { return m_bTesting; } -#ifdef MIXXX_USE_QML + static void registerPlayerManager(std::shared_ptr pPlayerManager); + static void registerTrackCollectionManager( std::shared_ptr pTrackCollectionManager); -#endif + signals: void beforeShutdown(); @@ -91,10 +94,11 @@ class ControllerScriptEngineBase : public QObject { #endif bool m_bTesting; -#ifdef MIXXX_USE_QML private: + static inline std::shared_ptr s_pPlayerManager; static inline std::shared_ptr s_pTrackCollectionManager; +#ifdef MIXXX_USE_QML protected: /// Pause the GUI main thread. Pause is required by rendering /// thread (https://doc.qt.io/qt-6/qquickrendercontrol.html#sync). This diff --git a/src/controllers/scripting/javascriptplayerproxy.cpp b/src/controllers/scripting/javascriptplayerproxy.cpp new file mode 100644 index 000000000000..7a81146d1500 --- /dev/null +++ b/src/controllers/scripting/javascriptplayerproxy.cpp @@ -0,0 +1,119 @@ +#include "javascriptplayerproxy.h" + +#include "moc_javascriptplayerproxy.cpp" + +JavascriptPlayerProxy::JavascriptPlayerProxy(BaseTrackPlayer* pTrackPlayer, QObject* parent) + : QObject(parent), + m_pTrackPlayer(pTrackPlayer) { + if (m_pTrackPlayer && m_pTrackPlayer->getLoadedTrack()) { + slotTrackLoaded(pTrackPlayer->getLoadedTrack()); + } + + connect(m_pTrackPlayer, + &BaseTrackPlayer::loadingTrack, + this, + &JavascriptPlayerProxy::slotLoadingTrack); + connect(m_pTrackPlayer, + &BaseTrackPlayer::newTrackLoaded, + this, + &JavascriptPlayerProxy::slotTrackLoaded); + connect(m_pTrackPlayer, + &BaseTrackPlayer::playerEmpty, + this, + [this]() { + disconnectTrack(); + emit trackUnloaded(); + }); +} + +void JavascriptPlayerProxy::slotTrackLoaded(TrackPointer pTrack) { + m_pCurrentTrack = pTrack; + if (pTrack == nullptr) { + emit trackUnloaded(); + return; + } + + connect(pTrack.get(), + &Track::artistChanged, + this, + &JavascriptPlayerProxy::artistChanged); + connect(pTrack.get(), + &Track::titleChanged, + this, + &JavascriptPlayerProxy::titleChanged); + connect(pTrack.get(), + &Track::albumChanged, + this, + &JavascriptPlayerProxy::albumChanged); + connect(pTrack.get(), + &Track::albumArtistChanged, + this, + &JavascriptPlayerProxy::albumArtistChanged); + connect(pTrack.get(), + &Track::genreChanged, + this, + &JavascriptPlayerProxy::genreChanged); + connect(pTrack.get(), + &Track::composerChanged, + this, + &JavascriptPlayerProxy::composerChanged); + connect(pTrack.get(), + &Track::groupingChanged, + this, + &JavascriptPlayerProxy::groupingChanged); + connect(pTrack.get(), + &Track::yearChanged, + this, + &JavascriptPlayerProxy::yearChanged); + connect(pTrack.get(), + &Track::trackNumberChanged, + this, + &JavascriptPlayerProxy::trackNumberChanged); + connect(pTrack.get(), + &Track::trackTotalChanged, + this, + &JavascriptPlayerProxy::trackTotalChanged); + + emit artistChanged(m_pCurrentTrack->getArtist()); + emit titleChanged(m_pCurrentTrack->getTitle()); + emit albumChanged(m_pCurrentTrack->getAlbum()); + emit albumArtistChanged(m_pCurrentTrack->getAlbumArtist()); + emit genreChanged(m_pCurrentTrack->getGenre()); + emit composerChanged(m_pCurrentTrack->getComposer()); + emit groupingChanged(m_pCurrentTrack->getGrouping()); + emit yearChanged(m_pCurrentTrack->getYear()); + emit trackNumberChanged(m_pCurrentTrack->getTrackNumber()); + emit trackTotalChanged(m_pCurrentTrack->getTrackTotal()); +} + +void JavascriptPlayerProxy::slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack) { + VERIFY_OR_DEBUG_ASSERT(pOldTrack == m_pCurrentTrack) { + qWarning() << "Javascript Player proxy was expected to contain " + << pOldTrack.get() << "as active track but got" + << m_pCurrentTrack.get(); + } + + if (pNewTrack == m_pCurrentTrack) { + return; + } + + disconnectTrack(); + m_pCurrentTrack = pNewTrack; +} + +void JavascriptPlayerProxy::disconnectTrack() { + if (m_pCurrentTrack != nullptr) { + m_pCurrentTrack->disconnect(this); + } +} + +PROPERTY_IMPL_GETTER(JavascriptPlayerProxy, QString, artist, getArtist) +PROPERTY_IMPL_GETTER(JavascriptPlayerProxy, QString, title, getTitle) +PROPERTY_IMPL_GETTER(JavascriptPlayerProxy, QString, album, getAlbum) +PROPERTY_IMPL_GETTER(JavascriptPlayerProxy, QString, albumArtist, getAlbumArtist) +PROPERTY_IMPL_GETTER(JavascriptPlayerProxy, QString, genre, getGenre) +PROPERTY_IMPL_GETTER(JavascriptPlayerProxy, QString, composer, getComposer) +PROPERTY_IMPL_GETTER(JavascriptPlayerProxy, QString, grouping, getGrouping) +PROPERTY_IMPL_GETTER(JavascriptPlayerProxy, QString, year, getYear) +PROPERTY_IMPL_GETTER(JavascriptPlayerProxy, QString, trackNumber, getTrackNumber) +PROPERTY_IMPL_GETTER(JavascriptPlayerProxy, QString, trackTotal, getTrackTotal) diff --git a/src/controllers/scripting/javascriptplayerproxy.h b/src/controllers/scripting/javascriptplayerproxy.h new file mode 100644 index 000000000000..da2d04260d13 --- /dev/null +++ b/src/controllers/scripting/javascriptplayerproxy.h @@ -0,0 +1,62 @@ +#pragma once + +#include "mixer/basetrackplayer.h" +#include "track/track.h" + +#define PROPERTY_IMPL_GETTER(NAMESPACE, TYPE, NAME, GETTER) \ + TYPE NAMESPACE::GETTER() const { \ + const TrackPointer pTrack = m_pCurrentTrack; \ + if (pTrack == nullptr) { \ + return TYPE(); \ + } \ + return pTrack->GETTER(); \ + } + +class JavascriptPlayerProxy : public QObject { + Q_OBJECT + Q_PROPERTY(QString artist READ getArtist NOTIFY artistChanged) + Q_PROPERTY(QString title READ getTitle NOTIFY titleChanged) + Q_PROPERTY(QString album READ getAlbum NOTIFY albumChanged) + Q_PROPERTY(QString albumArtist READ getAlbumArtist NOTIFY albumArtistChanged) + Q_PROPERTY(QString genre READ getGenre STORED false NOTIFY genreChanged) + Q_PROPERTY(QString composer READ getComposer NOTIFY composerChanged) + Q_PROPERTY(QString grouping READ getGrouping NOTIFY groupingChanged) + Q_PROPERTY(QString year READ getYear NOTIFY yearChanged) + Q_PROPERTY(QString trackNumber READ getTrackNumber NOTIFY trackNumberChanged) + Q_PROPERTY(QString trackTotal READ getTrackTotal NOTIFY trackTotalChanged) + public: + explicit JavascriptPlayerProxy(BaseTrackPlayer* pTrackPlayer, QObject* parent); + + QString getTitle() const; + QString getArtist() const; + QString getAlbum() const; + QString getAlbumArtist() const; + QString getGenre() const; + QString getComposer() const; + QString getGrouping() const; + QString getYear() const; + QString getTrackNumber() const; + QString getTrackTotal() const; + + public slots: + void slotTrackLoaded(TrackPointer pTrack); + void slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack); + + signals: + void trackUnloaded(); + void albumChanged(QString newAlbum); + void titleChanged(QString newTitle); + void artistChanged(QString newArtist); + void albumArtistChanged(QString newAlbumArtist); + void genreChanged(QString newGenre); + void composerChanged(QString newComposer); + void groupingChanged(QString grouping); + void yearChanged(QString newYear); + void trackNumberChanged(QString newTrackNumber); + void trackTotalChanged(QString newTrackTotal); + + protected: + void disconnectTrack(); + QPointer m_pTrackPlayer; + TrackPointer m_pCurrentTrack; +}; diff --git a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp index 753dfe3e70a8..899e9988e914 100644 --- a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp +++ b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp @@ -136,6 +136,10 @@ QJSValue ControllerScriptInterfaceLegacy::getSetting(const QString& name) { } } +QObject* ControllerScriptInterfaceLegacy::getPlayer(const QString& group) { + return m_pScriptEngineLegacy->getPlayer(group); +} + double ControllerScriptInterfaceLegacy::getValue(const QString& group, const QString& name) { ControlObjectScript* coScript = getControlObjectScript(group, name); if (coScript == nullptr) { diff --git a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h index 3585c3692df4..809d01cbd8e8 100644 --- a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h +++ b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h @@ -56,6 +56,7 @@ class ControllerScriptInterfaceLegacy : public QObject { virtual ~ControllerScriptInterfaceLegacy(); Q_INVOKABLE QJSValue getSetting(const QString& name); + Q_INVOKABLE QObject* getPlayer(const QString& group); Q_INVOKABLE double getValue(const QString& group, const QString& name); Q_INVOKABLE void setValue(const QString& group, const QString& name, double newValue); Q_INVOKABLE double getParameter(const QString& group, const QString& name); diff --git a/src/coreservices.cpp b/src/coreservices.cpp index fd8bd8e44fe9..aa7b311ec6f9 100644 --- a/src/coreservices.cpp +++ b/src/coreservices.cpp @@ -14,6 +14,7 @@ #include "control/controlindicatortimer.h" #include "controllers/controllermanager.h" #include "controllers/keyboard/keyboardeventfilter.h" +#include "controllers/scripting/controllerscriptenginebase.h" #include "database/mixxxdb.h" #include "effects/effectsmanager.h" #include "engine/enginemixer.h" @@ -40,15 +41,10 @@ #include #include -#include "controllers/scripting/controllerscriptenginebase.h" #include "qml/qmlconfigproxy.h" -#include "qml/qmlcontrolproxy.h" -#include "qml/qmldlgpreferencesproxy.h" -#include "qml/qmleffectslotproxy.h" #include "qml/qmleffectsmanagerproxy.h" #include "qml/qmllibraryproxy.h" #include "qml/qmlplayermanagerproxy.h" -#include "qml/qmlplayerproxy.h" #endif #include "soundio/soundmanager.h" #include "sources/soundsourceproxy.h" @@ -67,7 +63,7 @@ #include "util/sandbox.h" #endif -#ifdef Q_OS_LINUX +#if defined(Q_OS_LINUX) && defined(__X11__) #include #endif @@ -122,7 +118,7 @@ Bool __xErrorHandler(Display* display, XErrorEvent* event, xError* error) { #endif -#if defined(Q_OS_LINUX) +#if defined(Q_OS_LINUX) && defined(__X11__) QLocale localeFromXkbSymbol(const QString& xkbLayout) { // This maps XKB layouts to locales of keyboard mappings that are shipped with Mixxx static const QMap xkbToLocaleMap = { @@ -272,7 +268,7 @@ QString getCurrentXkbLayoutName() { // to "ibus engine". QGuiApplication::inputMethod() does not work with GNOME and XFCE // https://bugreports.qt.io/browse/QTBUG-137302 inline QLocale inputLocale() { -#if defined(Q_OS_LINUX) +#if defined(Q_OS_LINUX) && defined(__X11__) QString layoutName = getCurrentXkbLayoutName(); if (!layoutName.isEmpty()) { qDebug() << "Keyboard Layout from XKB:" << layoutName; @@ -726,6 +722,8 @@ void CoreServices::initialize(QApplication* pApp) { m_isInitialized = true; + ControllerScriptEngineBase::registerPlayerManager(getPlayerManager()); + #ifdef MIXXX_USE_QML initializeQMLSingletons(); } @@ -867,6 +865,7 @@ void CoreServices::finalize() { mixxx::qml::QmlLibraryProxy::registerLibrary(nullptr); ControllerScriptEngineBase::registerTrackCollectionManager(nullptr); + ControllerScriptEngineBase::registerPlayerManager(nullptr); #endif // Stop all pending library operations diff --git a/src/engine/controls/loopingcontrol.cpp b/src/engine/controls/loopingcontrol.cpp index cb1c7436e3c4..f5a099790c70 100644 --- a/src/engine/controls/loopingcontrol.cpp +++ b/src/engine/controls/loopingcontrol.cpp @@ -1309,6 +1309,15 @@ void LoopingControl::slotBeatLoopDeactivate(BeatLoopingControl* pBeatLoopControl void LoopingControl::slotBeatLoopDeactivateRoll(BeatLoopingControl* pBeatLoopControl) { pBeatLoopControl->deactivate(); + + if (!m_bLoopRollActive) { + // beatloop_activate was pressed while rolling and slotBeatLoopToggle() + // did already reset roll status (m_activeLoopRolls, m_bLoopRollActive) + // and EngineBuffer quit slip mode (but didn't seek). + // So nothing to do here, just leave the adopted loop active. + return; + } + const double size = pBeatLoopControl->getSize(); // clang-tidy wants auto to be auto* because QStack inherits from QVector // and QVector::iterator is a pointer type in Qt5, but QStack inherits @@ -1325,18 +1334,15 @@ void LoopingControl::slotBeatLoopDeactivateRoll(BeatLoopingControl* pBeatLoopCon // Make sure slip mode is not turned off if it was turned on // by something that was not a rolling beatloop. - if (m_bLoopRollActive && m_activeLoopRolls.empty()) { + if (m_activeLoopRolls.empty()) { setLoopingEnabled(false); m_pSlipEnabled->set(0); m_bLoopRollActive = false; - } - - // Return to the previous beatlooproll if necessary. - // Else previous regular beatloop if no rolling loops are active. - if (!m_activeLoopRolls.empty()) { - slotBeatLoop(m_activeLoopRolls.top(), m_bLoopRollActive, true); - } else { restoreLoopInfo(); + } else { + // Return to the previous beatlooproll if necessary. + // Else previous regular beatloop if no rolling loops are active. + slotBeatLoop(m_activeLoopRolls.top(), m_bLoopRollActive, true); } } @@ -1694,14 +1700,25 @@ void LoopingControl::slotBeatLoopSizeChangeRequest(double beats) { } void LoopingControl::slotBeatLoopToggle(double pressed) { - if (pressed > 0) { - if (m_bLoopingEnabled) { + if (pressed <= 0) { + return; + } + + if (m_bLoopingEnabled) { + // If we're in a rolling loop, quit slip mode and adopt it as regular loop. + // Use case is to have a looproll button pressed, then press loop_activate + // and nothing should happen when releasing the looproll button. + if (m_bLoopRollActive) { + m_bLoopRollActive = false; + m_activeLoopRolls.clear(); + getEngineBuffer()->slipQuitAndAdopt(); + } else { // Deactivate the loop if we're already looping setLoopingEnabled(false); - } else { - // Create a loop at current position - slotBeatLoop(m_pCOBeatLoopSize->get()); } + } else { + // Create a loop at current position + slotBeatLoop(m_pCOBeatLoopSize->get()); } } @@ -1723,6 +1740,14 @@ void LoopingControl::slotBeatLoopRollActivate(double pressed) { m_bLoopRollActive = true; } } else { + if (!m_bLoopRollActive) { + // beatloop_activate was pressed while rolling and slotBeatLoopToggle() + // did already reset roll status (m_activeLoopRolls, m_bLoopRollActive) + // and EngineBuffer quit slip mode (but didn't seek). + // So nothing to do here, just leave the adopted loop active. + return; + } + setLoopingEnabled(false); // Make sure slip mode is not turned off if it was turned on // by something that was not a rolling beatloop. diff --git a/src/engine/enginebuffer.cpp b/src/engine/enginebuffer.cpp index 50f31f0e614c..2ed902b39b25 100644 --- a/src/engine/enginebuffer.cpp +++ b/src/engine/enginebuffer.cpp @@ -90,6 +90,7 @@ EngineBuffer::EngineBuffer(const QString& group, m_iSeekPhaseQueued(0), m_iEnableSyncQueued(SYNC_REQUEST_NONE), m_iSyncModeQueued(static_cast(SyncMode::Invalid)), + m_slipQuitAndAdopt(0), m_bPlayAfterLoading(false), m_channelCount(mixxx::kEngineChannelOutputCount), m_pCrossfadeBuffer(SampleUtil::alloc( @@ -870,6 +871,11 @@ void EngineBuffer::slotKeylockEngineChanged(double dIndex) { } } +void EngineBuffer::slipQuitAndAdopt() { + m_slipQuitAndAdopt.storeRelease(1); + m_pSlipButton->set(0); +} + void EngineBuffer::processTrackLocked( CSAMPLE* pOutput, const std::size_t bufferSize, mixxx::audio::SampleRate sampleRate) { ScopedTimer t(QStringLiteral("EngineBuffer::process_pauselock")); @@ -1263,8 +1269,12 @@ void EngineBuffer::processSlip(std::size_t bufferSize) { m_slipPos = m_playPos; m_dSlipRate = m_rate_old; } else { - // TODO(owen) assuming that looping will get canceled properly - seekExact(m_slipPos.toNearestFrameBoundary()); + // If m_slipQuitAndAdopt is 1 we've already quit slip mode + // but we don't seek in that case. + if (m_slipQuitAndAdopt.fetchAndStoreAcquire(0) == 0) { + // TODO(owen) assuming that looping will get canceled properly + seekExact(m_slipPos.toNearestFrameBoundary()); + } m_slipPos = mixxx::audio::kStartFramePos; } } diff --git a/src/engine/enginebuffer.h b/src/engine/enginebuffer.h index 9d4480609a15..046b2ff8dec5 100644 --- a/src/engine/enginebuffer.h +++ b/src/engine/enginebuffer.h @@ -236,6 +236,8 @@ class EngineBuffer : public EngineObject { void verifyPlay(); + void slipQuitAndAdopt(); + public slots: void slotControlPlayRequest(double); void slotControlPlayFromStart(double); @@ -466,6 +468,7 @@ class EngineBuffer : public EngineObject { ControlValueAtomic m_queuedSeek; bool m_previousBufferSeek = false; + QAtomicInt m_slipQuitAndAdopt; /// Indicates that no seek is queued static constexpr QueuedSeek kNoQueuedSeek = {mixxx::audio::kInvalidFramePos, SEEK_NONE}; /// indicates a clone seek on a bosition from another deck diff --git a/src/engine/sidechain/enginerecord.cpp b/src/engine/sidechain/enginerecord.cpp index a60e982258ba..5eac865eb0fa 100644 --- a/src/engine/sidechain/enginerecord.cpp +++ b/src/engine/sidechain/enginerecord.cpp @@ -18,7 +18,8 @@ EngineRecord::EngineRecord(UserSettingsPointer pConfig) m_recordedDuration(0), m_iMetaDataLife(0), m_cueTrack(0), - m_bCueIsEnabled(false) { + m_bCueIsEnabled(false), + m_bCueUsesFileAnnotation(false) { m_pRecReady = new ControlProxy(RECORDING_PREF_KEY, "status", this); m_sampleRate = mixxx::audio::SampleRate::fromDouble(m_sampleRateControl.get()); } @@ -35,7 +36,10 @@ int EngineRecord::updateFromPreferences() { m_baAuthor = m_pConfig->getValueString(ConfigKey(RECORDING_PREF_KEY, "Author")); m_baAlbum = m_pConfig->getValueString(ConfigKey(RECORDING_PREF_KEY, "Album")); m_cueFileName = m_pConfig->getValueString(ConfigKey(RECORDING_PREF_KEY, "CuePath")); - m_bCueIsEnabled = m_pConfig->getValueString(ConfigKey(RECORDING_PREF_KEY, "CueEnabled")).toInt(); + m_bCueIsEnabled = m_pConfig->getValue( + ConfigKey(RECORDING_PREF_KEY, QStringLiteral("CueEnabled"))); + m_bCueUsesFileAnnotation = m_pConfig->getValue( + ConfigKey(RECORDING_PREF_KEY, QStringLiteral("cue_file_annotation_enabled"))); m_sampleRate = mixxx::audio::SampleRate::fromDouble(m_sampleRateControl.get()); // Delete m_pEncoder if it has been initialized (with maybe) different bitrate. @@ -239,16 +243,22 @@ void EngineRecord::writeCueLine() { ((m_frames / (m_sampleRate / 75))) % 75); - m_cueFile.write(QString(" TRACK %1 AUDIO\n") - .arg((double)m_cueTrack, 2, 'f', 0, '0') - .toUtf8()); + m_cueFile.write(QStringLiteral(" TRACK %1 AUDIO\n") + .arg((double)m_cueTrack, 2, 'f', 0, '0') + .toUtf8()); - m_cueFile.write(QString(" TITLE \"%1\"\n") - .arg(m_pCurrentTrack->getTitle()) - .toUtf8()); - m_cueFile.write(QString(" PERFORMER \"%1\"\n") - .arg(m_pCurrentTrack->getArtist()) - .toUtf8()); + m_cueFile.write(QStringLiteral(" TITLE \"%1\"\n") + .arg(m_pCurrentTrack->getTitle()) + .toUtf8()); + m_cueFile.write(QStringLiteral(" PERFORMER \"%1\"\n") + .arg(m_pCurrentTrack->getArtist()) + .toUtf8()); + + if (m_bCueUsesFileAnnotation) { + m_cueFile.write(QStringLiteral(" FILE \"%1\"\n") + .arg(m_pCurrentTrack->getLocation()) + .toUtf8()); + } // Woefully inaccurate (at the seconds level anyways). // We'd need a signal fired state tracker diff --git a/src/engine/sidechain/enginerecord.h b/src/engine/sidechain/enginerecord.h index 53e110cd854f..2c0e8b6c903e 100644 --- a/src/engine/sidechain/enginerecord.h +++ b/src/engine/sidechain/enginerecord.h @@ -85,4 +85,5 @@ class EngineRecord : public QObject, public EncoderCallback, public SideChainWor QString m_cueFileName; quint64 m_cueTrack; bool m_bCueIsEnabled; + bool m_bCueUsesFileAnnotation; }; diff --git a/src/library/library.cpp b/src/library/library.cpp index e8161bf9dc62..fb4c5e58c495 100644 --- a/src/library/library.cpp +++ b/src/library/library.cpp @@ -745,13 +745,27 @@ void Library::setEditMetadataSelectedClick(bool enabled) { emit setSelectedClick(enabled); } +void Library::slotSearchInCurrentView() { + m_pLibraryControl->setLibraryFocus(FocusWidget::Searchbar, Qt::ShortcutFocusReason); +} + +void Library::slotSearchInAllTracks() { + searchTracksInCollection(); +} + +void Library::searchTracksInCollection() { + VERIFY_OR_DEBUG_ASSERT(m_pMixxxLibraryFeature) { + return; + } + m_pMixxxLibraryFeature->selectAndActivate(); + m_pLibraryControl->setLibraryFocus(FocusWidget::Searchbar, Qt::ShortcutFocusReason); +} + void Library::searchTracksInCollection(const QString& query) { VERIFY_OR_DEBUG_ASSERT(m_pMixxxLibraryFeature) { return; } m_pMixxxLibraryFeature->searchAndActivate(query); - emit switchToView(m_sTrackViewName); - m_pSidebarModel->activateDefaultSelection(); } #ifdef __ENGINEPRIME__ diff --git a/src/library/library.h b/src/library/library.h index 2598498da97a..0b1a9140068d 100644 --- a/src/library/library.h +++ b/src/library/library.h @@ -98,6 +98,10 @@ class Library: public QObject { void setRowHeight(int rowHeight); void setEditMetadataSelectedClick(bool enable); + /// Switches to the internal track collection view + /// and focuses the search box. + void searchTracksInCollection(); + /// Triggers a new search in the internal track collection /// and shows the results by switching the view. void searchTracksInCollection(const QString& query); @@ -126,6 +130,8 @@ class Library: public QObject { void slotRefreshLibraryModels(); void slotCreatePlaylist(); void slotCreateCrate(); + void slotSearchInCurrentView(); + void slotSearchInAllTracks(); void onSkinLoadFinished(); void slotSaveCurrentViewState() const; void slotRestoreCurrentViewState() const; diff --git a/src/library/librarycontrol.cpp b/src/library/librarycontrol.cpp index 6f0f9d70e95d..8fa7b60cae3b 100644 --- a/src/library/librarycontrol.cpp +++ b/src/library/librarycontrol.cpp @@ -956,15 +956,11 @@ FocusWidget LibraryControl::getFocusedWidget() { } } -void LibraryControl::setLibraryFocus(FocusWidget newFocusWidget) { - if (!QApplication::focusWindow()) { - qInfo() << "No Mixxx window, popup or menu has focus." - << "Don't attempt to focus a specific widget."; - return; - } - - // ignore no-op - if (newFocusWidget == m_focusedWidget) { +void LibraryControl::setLibraryFocus(FocusWidget newFocusWidget, Qt::FocusReason focusReason) { + // The search box wants to do special handling when the Ctrl+f is used + // while it is already focused. Non-shortcut cases should still be a + // no-op when a control is already focused. + if (newFocusWidget == m_focusedWidget && focusReason != Qt::ShortcutFocusReason) { return; } @@ -973,13 +969,13 @@ void LibraryControl::setLibraryFocus(FocusWidget newFocusWidget) { VERIFY_OR_DEBUG_ASSERT(m_pSearchbox) { return; } - m_pSearchbox->setFocus(); + m_pSearchbox->setFocus(focusReason); return; case FocusWidget::Sidebar: VERIFY_OR_DEBUG_ASSERT(m_pSidebarWidget) { return; } - m_pSidebarWidget->setFocus(); + m_pSidebarWidget->setFocus(focusReason); return; case FocusWidget::TracksTable: VERIFY_OR_DEBUG_ASSERT(m_pLibraryWidget) { diff --git a/src/library/librarycontrol.h b/src/library/librarycontrol.h index 2511cb0db3a6..ecf6e1370bc9 100644 --- a/src/library/librarycontrol.h +++ b/src/library/librarycontrol.h @@ -58,8 +58,9 @@ class LibraryControl : public QObject { void bindLibraryWidget(WLibrary* pLibrary, KeyboardEventFilter* pKeyboard); void bindSidebarWidget(WLibrarySidebar* pLibrarySidebar); void bindSearchboxWidget(WSearchLineEdit* pSearchbox); - // Give the keyboard focus to one of the library widgets - void setLibraryFocus(FocusWidget newFocusWidget); + /// Give the keyboard focus to one of the library widgets + void setLibraryFocus(FocusWidget newFocusWidget, + Qt::FocusReason focusReason = Qt::OtherFocusReason); FocusWidget getFocusedWidget(); signals: diff --git a/src/library/libraryfeature.cpp b/src/library/libraryfeature.cpp index 51ad6ae3fac6..7c88fbf93bdc 100644 --- a/src/library/libraryfeature.cpp +++ b/src/library/libraryfeature.cpp @@ -32,6 +32,17 @@ LibraryFeature::LibraryFeature( } } +void LibraryFeature::selectAndActivate(const QModelIndex& index) { + if (index.isValid()) { + emit featureSelect(this, index); + activateChild(index); + } else { + // calling featureSelect with invalid index will select the root item + emit featureSelect(this, QModelIndex()); + activate(); + } +} + QStringList LibraryFeature::getPlaylistFiles(QFileDialog::FileMode mode) const { QString lastPlaylistDirectory = m_pConfig->getValue( ConfigKey("[Library]", "LastImportExportPlaylistDirectory"), diff --git a/src/library/libraryfeature.h b/src/library/libraryfeature.h index 9a91e748ddda..3c0a204cc436 100644 --- a/src/library/libraryfeature.h +++ b/src/library/libraryfeature.h @@ -106,6 +106,10 @@ class LibraryFeature : public QObject { const UserSettingsPointer m_pConfig; public slots: + /// Pretend that the user has clicked on a tree item belonging + /// to this LibraryFeature by updating both the library view + /// and the sidebar selection. + void selectAndActivate(const QModelIndex& index = QModelIndex()); // called when you single click on the root item virtual void activate() = 0; // called when you single click on a child item, e.g., a concrete playlist or crate diff --git a/src/library/mixxxlibraryfeature.cpp b/src/library/mixxxlibraryfeature.cpp index 838f0fd0480c..519f80f86790 100644 --- a/src/library/mixxxlibraryfeature.cpp +++ b/src/library/mixxxlibraryfeature.cpp @@ -173,7 +173,7 @@ void MixxxLibraryFeature::searchAndActivate(const QString& query) { return; } m_pLibraryTableModel->search(query); - activate(); + selectAndActivate(); } #ifdef __ENGINEPRIME__ diff --git a/src/library/trackset/playlistfeature.cpp b/src/library/trackset/playlistfeature.cpp index 0f41d70383e8..17eab0ea2d11 100644 --- a/src/library/trackset/playlistfeature.cpp +++ b/src/library/trackset/playlistfeature.cpp @@ -372,8 +372,7 @@ void PlaylistFeature::slotPlaylistTableChanged(int playlistId) { // Else (root item was selected or for some reason no index could be created) // there's nothing to do: either no child was selected earlier, or the root // was selected and will remain selected after the child model was rebuilt. - activateChild(newIndex); - emit featureSelect(this, newIndex); + selectAndActivate(newIndex); } } diff --git a/src/library/trackset/setlogfeature.cpp b/src/library/trackset/setlogfeature.cpp index 093ff3703eb3..1f3582ad8f63 100644 --- a/src/library/trackset/setlogfeature.cpp +++ b/src/library/trackset/setlogfeature.cpp @@ -712,13 +712,8 @@ void SetlogFeature::slotPlaylistTableChanged(int playlistId) { newIndex = m_pSidebarModel->index(selectedYearIndexRow - 1, 0); } } - if (newIndex.isValid()) { - emit featureSelect(this, newIndex); - activateChild(newIndex); - } else if (rootWasSelected) { - // calling featureSelect with invalid index will select the root item - emit featureSelect(this, newIndex); - activate(); // to reload the new current playlist + if (newIndex.isValid() || rootWasSelected) { + selectAndActivate(newIndex); } } diff --git a/src/mixxxmainwindow.cpp b/src/mixxxmainwindow.cpp index 28fc6d2b7853..e35b53ab5910 100644 --- a/src/mixxxmainwindow.cpp +++ b/src/mixxxmainwindow.cpp @@ -965,6 +965,16 @@ void MixxxMainWindow::connectMenuBar() { } if (m_pCoreServices->getLibrary()) { + connect(m_pMenuBar, + &WMainMenuBar::searchInCurrentView, + m_pCoreServices->getLibrary().get(), + &Library::slotSearchInCurrentView, + Qt::UniqueConnection); + connect(m_pMenuBar, + &WMainMenuBar::searchInAllTracks, + m_pCoreServices->getLibrary().get(), + &Library::slotSearchInAllTracks, + Qt::UniqueConnection); connect(m_pMenuBar, &WMainMenuBar::createCrate, m_pCoreServices->getLibrary().get(), diff --git a/src/preferences/dialog/dlgprefinterface.cpp b/src/preferences/dialog/dlgprefinterface.cpp index 7cdc67e86a19..f4e0c7841d70 100644 --- a/src/preferences/dialog/dlgprefinterface.cpp +++ b/src/preferences/dialog/dlgprefinterface.cpp @@ -36,6 +36,7 @@ const QString kResizableSkinKey = QStringLiteral("ResizableSkin"); const QString kLocaleKey = QStringLiteral("Locale"); const QString kTooltipsKey = QStringLiteral("Tooltips"); const QString kMultiSamplingKey = QStringLiteral("multi_sampling"); +const QString kForceHardwareAccelerationKey = QStringLiteral("force_hardware_acceleration"); const QString kHideMenuBarKey = QStringLiteral("hide_menubar"); // TODO move these to a common *_defs.h file, some are also used by e.g. MixxxMainWindow @@ -206,6 +207,9 @@ DlgPrefInterface::DlgPrefInterface( m_multiSampling = m_pConfig->getValue( ConfigKey(kPreferencesGroup, kMultiSamplingKey), mixxx::preferences::MultiSamplingMode::Four); + m_forceHardwareAcceleration = m_pConfig->getValue( + ConfigKey(kPreferencesGroup, kForceHardwareAccelerationKey), + false); int multiSamplingIndex = multiSamplingComboBox->findData( QVariant::fromValue((m_multiSampling))); if (multiSamplingIndex != -1) { @@ -215,14 +219,17 @@ DlgPrefInterface::DlgPrefInterface( m_pConfig->setValue(ConfigKey(kPreferencesGroup, kMultiSamplingKey), mixxx::preferences::MultiSamplingMode::Disabled); } + checkBoxForceHardwareAcceleration->setChecked(m_forceHardwareAcceleration); } else #endif { #ifdef MIXXX_USE_QML m_multiSampling = mixxx::preferences::MultiSamplingMode::Disabled; + m_forceHardwareAcceleration = false; #endif multiSamplingLabel->hide(); multiSamplingComboBox->hide(); + checkBoxForceHardwareAcceleration->hide(); } // Tooltip configuration @@ -358,6 +365,8 @@ void DlgPrefInterface::slotResetToDefaults() { multiSamplingComboBox->setCurrentIndex( multiSamplingComboBox->findData(QVariant::fromValue( mixxx::preferences::MultiSamplingMode::Four))); // 4x MSAA + checkBoxForceHardwareAcceleration->setChecked( + false); #endif #ifdef Q_OS_IOS @@ -489,11 +498,20 @@ void DlgPrefInterface::slotApply() { .value(); m_pConfig->setValue( ConfigKey(kPreferencesGroup, kMultiSamplingKey), multiSampling); + bool forceHardwareAcceleration = checkBoxForceHardwareAcceleration->isChecked(); + if (m_pConfig->exists( + ConfigKey(kPreferencesGroup, kForceHardwareAccelerationKey)) || + forceHardwareAcceleration) { + m_pConfig->setValue( + ConfigKey(kPreferencesGroup, kForceHardwareAccelerationKey), + forceHardwareAcceleration); + } #endif if (locale != m_localeOnUpdate || scaleFactor != m_dScaleFactor #ifdef MIXXX_USE_QML - || multiSampling != m_multiSampling + || multiSampling != m_multiSampling || + forceHardwareAcceleration != m_forceHardwareAcceleration #endif ) { notifyRebootNecessary(); @@ -502,6 +520,7 @@ void DlgPrefInterface::slotApply() { m_dScaleFactor = scaleFactor; #ifdef MIXXX_USE_QML m_multiSampling = multiSampling; + m_forceHardwareAcceleration = forceHardwareAcceleration; #endif } diff --git a/src/preferences/dialog/dlgprefinterface.h b/src/preferences/dialog/dlgprefinterface.h index b2c7fcb0d6c5..1a990b25d1b6 100644 --- a/src/preferences/dialog/dlgprefinterface.h +++ b/src/preferences/dialog/dlgprefinterface.h @@ -70,6 +70,7 @@ class DlgPrefInterface : public DlgPreferencePage, public Ui::DlgPrefControlsDlg QString m_colorSchemeOnUpdate; QString m_localeOnUpdate; mixxx::preferences::MultiSamplingMode m_multiSampling; + bool m_forceHardwareAcceleration; mixxx::preferences::Tooltips m_tooltipMode; double m_dScaleFactor; double m_minScaleFactor; diff --git a/src/preferences/dialog/dlgprefinterfacedlg.ui b/src/preferences/dialog/dlgprefinterfacedlg.ui index 2b7def9894f4..21fb27b4aa90 100644 --- a/src/preferences/dialog/dlgprefinterfacedlg.ui +++ b/src/preferences/dialog/dlgprefinterfacedlg.ui @@ -287,16 +287,26 @@ - + Multi-Sampling - + + + + + Force 3D acceleration + + + If checked, Mixxx will always assume 3D acceleration is available. This may lead to pour performance if only CP-backed rendering is available.. + + + @@ -327,6 +337,7 @@ radioButtonTooltipsLibrary radioButtonTooltipsLibraryAndSkin multiSamplingComboBox + checkBoxForceHardwareAcceleration diff --git a/src/preferences/dialog/dlgprefrecord.cpp b/src/preferences/dialog/dlgprefrecord.cpp index 0ae6b19be8e7..02885df84315 100644 --- a/src/preferences/dialog/dlgprefrecord.cpp +++ b/src/preferences/dialog/dlgprefrecord.cpp @@ -12,6 +12,7 @@ namespace { constexpr bool kDefaultCueEnabled = true; +constexpr bool kDefaultCueFileAnnotationEnabled = false; } // anonymous namespace DlgPrefRecord::DlgPrefRecord(QWidget* parent, UserSettingsPointer pConfig) @@ -76,6 +77,10 @@ DlgPrefRecord::DlgPrefRecord(QWidget* parent, UserSettingsPointer pConfig) CheckBoxRecordCueFile->setChecked(m_pConfig->getValue( ConfigKey(RECORDING_PREF_KEY, "CueEnabled"), kDefaultCueEnabled)); + CheckBoxUseCueFileAnnotation->setChecked(m_pConfig->getValue( + ConfigKey(RECORDING_PREF_KEY, "cue_file_annotation_enabled"), + kDefaultCueFileAnnotationEnabled)); + // Setting split comboBoxSplitting->addItem(SPLIT_650MB); comboBoxSplitting->addItem(SPLIT_700MB); @@ -119,6 +124,15 @@ DlgPrefRecord::DlgPrefRecord(QWidget* parent, UserSettingsPointer pConfig) &QAbstractSlider::sliderReleased, this, &DlgPrefRecord::slotSliderCompression); + + connect(CheckBoxRecordCueFile, +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) + &QCheckBox::checkStateChanged, +#else + &QCheckBox::stateChanged, +#endif + this, + &DlgPrefRecord::slotToggleCueEnabled); } DlgPrefRecord::~DlgPrefRecord() { @@ -144,6 +158,7 @@ void DlgPrefRecord::slotApply() { saveMetaData(); saveEncoding(); saveUseCueFile(); + saveUseCueFileAnnotation(); saveSplitSize(); } @@ -175,10 +190,12 @@ void DlgPrefRecord::slotUpdate() { loadMetaData(); - // Setting miscellaneous + // Setting miscellaneous CheckBoxRecordCueFile->setChecked(m_pConfig->getValue( ConfigKey(RECORDING_PREF_KEY, "CueEnabled"), kDefaultCueEnabled)); + slotToggleCueEnabled(); + QString fileSizeStr = m_pConfig->getValueString(ConfigKey(RECORDING_PREF_KEY, "FileSize")); int index = comboBoxSplitting->findText(fileSizeStr); if (index >= 0) { @@ -201,7 +218,12 @@ void DlgPrefRecord::slotResetToDefaults() { // 4GB splitting is the default comboBoxSplitting->setCurrentIndex(4); + + // Sets 'Create a CUE file' checkbox value CheckBoxRecordCueFile->setChecked(kDefaultCueEnabled); + + // Sets 'Enable File Annotation in CUE file' checkbox value + CheckBoxUseCueFileAnnotation->setChecked(kDefaultCueFileAnnotationEnabled); } void DlgPrefRecord::slotBrowseRecordingsDir() { @@ -429,11 +451,23 @@ void DlgPrefRecord::saveEncoding() { } } +// Set 'Enable File Annotation in CUE file' checkbox value depending on 'Create +// a CUE file' checkbox value +void DlgPrefRecord::slotToggleCueEnabled() { + CheckBoxUseCueFileAnnotation->setEnabled(CheckBoxRecordCueFile + ->isChecked()); +} + void DlgPrefRecord::saveUseCueFile() { m_pConfig->set(ConfigKey(RECORDING_PREF_KEY, "CueEnabled"), ConfigValue(CheckBoxRecordCueFile->isChecked())); } +void DlgPrefRecord::saveUseCueFileAnnotation() { + m_pConfig->set(ConfigKey(RECORDING_PREF_KEY, "cue_file_annotation_enabled"), + ConfigValue(CheckBoxUseCueFileAnnotation->isChecked())); +} + void DlgPrefRecord::saveSplitSize() { m_pConfig->set(ConfigKey(RECORDING_PREF_KEY, "FileSize"), ConfigValue(comboBoxSplitting->currentText())); diff --git a/src/preferences/dialog/dlgprefrecord.h b/src/preferences/dialog/dlgprefrecord.h index 17c47a1ea573..19d287c43c12 100644 --- a/src/preferences/dialog/dlgprefrecord.h +++ b/src/preferences/dialog/dlgprefrecord.h @@ -32,6 +32,9 @@ class DlgPrefRecord : public DlgPreferencePage, public Ui::DlgPrefRecordDlg { void slotSliderCompression(); void slotGroupChanged(); + private slots: + void slotToggleCueEnabled(); + signals: void apply(const QString &); @@ -44,6 +47,7 @@ class DlgPrefRecord : public DlgPreferencePage, public Ui::DlgPrefRecordDlg { void saveMetaData(); void saveEncoding(); void saveUseCueFile(); + void saveUseCueFileAnnotation(); void saveSplitSize(); // Pointer to config object diff --git a/src/preferences/dialog/dlgprefrecorddlg.ui b/src/preferences/dialog/dlgprefrecorddlg.ui index 01a3808af0d9..cffe8673c217 100644 --- a/src/preferences/dialog/dlgprefrecorddlg.ui +++ b/src/preferences/dialog/dlgprefrecorddlg.ui @@ -119,6 +119,20 @@ + + + + + This will include the filepath for each track in the CUE file. +This option makes the CUE file less portable and can reveal personal +information from filepaths (i.e. username) + + + Enable File Annotation in CUE file + + + + @@ -386,6 +400,7 @@ PushButtonBrowseRecordings comboBoxSplitting CheckBoxRecordCueFile + CheckBoxUseCueFileAnnotation SliderCompression SliderQuality LineEditTitle diff --git a/src/preferences/dialog/dlgprefwaveform.cpp b/src/preferences/dialog/dlgprefwaveform.cpp index 723b3ad17dd6..d8c7bd139750 100644 --- a/src/preferences/dialog/dlgprefwaveform.cpp +++ b/src/preferences/dialog/dlgprefwaveform.cpp @@ -237,10 +237,10 @@ DlgPrefWaveform::~DlgPrefWaveform() { } void DlgPrefWaveform::slotSetWaveformOptions( - allshader::WaveformRendererSignalBase::Option option, bool enabled) { - allshader::WaveformRendererSignalBase::Options currentOption = m_pConfig->getValue( + WaveformRendererSignalBase::Option option, bool enabled) { + WaveformRendererSignalBase::Options currentOption = m_pConfig->getValue( ConfigKey("[Waveform]", "waveform_options"), - allshader::WaveformRendererSignalBase::Option::None); + WaveformRendererSignalBase::Option::None); m_pConfig->setValue(ConfigKey("[Waveform]", "waveform_options"), enabled ? currentOption | option @@ -280,9 +280,9 @@ void DlgPrefWaveform::slotUpdate() { bool useWaveform = factory->getType() != WaveformWidgetType::Empty; useWaveformCheckBox->setChecked(useWaveform); - allshader::WaveformRendererSignalBase::Options currentOptions = m_pConfig->getValue( + WaveformRendererSignalBase::Options currentOptions = m_pConfig->getValue( ConfigKey("[Waveform]", "waveform_options"), - allshader::WaveformRendererSignalBase::Option::None); + WaveformRendererSignalBase::Option::None); WaveformWidgetBackend backend = m_pConfig->getValue( ConfigKey("[Waveform]", "use_hardware_acceleration"), factory->preferredBackend()); @@ -364,11 +364,11 @@ void DlgPrefWaveform::slotResetToDefaults() { updateWaveformAcceleration(WaveformWidgetFactory::defaultType(), defaultBackend); updateWaveformTypeOptions(true, defaultBackend, - allshader::WaveformRendererSignalBase::Option::None); + WaveformRendererSignalBase::Option::None); // Restore waveform backend and option setting instantly m_pConfig->setValue(ConfigKey("[Waveform]", "waveform_options"), - allshader::WaveformRendererSignalBase::Option::None); + WaveformRendererSignalBase::Option::None); m_pConfig->setValue(ConfigKey("[Waveform]", "use_hardware_acceleration"), defaultBackend); factory->setWidgetTypeFromHandle( @@ -436,9 +436,9 @@ void DlgPrefWaveform::slotSetWaveformType(int index) { useAccelerationCheckBox->setChecked(backend != WaveformWidgetBackend::None); - allshader::WaveformRendererSignalBase::Options currentOptions = m_pConfig->getValue( + WaveformRendererSignalBase::Options currentOptions = m_pConfig->getValue( ConfigKey("[Waveform]", "waveform_options"), - allshader::WaveformRendererSignalBase::Option::None); + WaveformRendererSignalBase::Option::None); updateWaveformAcceleration(type, backend); updateWaveformTypeOptions(true, backend, currentOptions); updateEnableUntilMark(); @@ -475,9 +475,9 @@ void DlgPrefWaveform::slotSetWaveformAcceleration(bool checked) { auto type = static_cast(waveformTypeComboBox->currentData().toInt()); auto* factory = WaveformWidgetFactory::instance(); factory->setWidgetTypeFromHandle(factory->findHandleIndexFromType(type), true); - allshader::WaveformRendererSignalBase::Options currentOptions = m_pConfig->getValue( + WaveformRendererSignalBase::Options currentOptions = m_pConfig->getValue( ConfigKey("[Waveform]", "waveform_options"), - allshader::WaveformRendererSignalBase::Option::None); + WaveformRendererSignalBase::Option::None); updateWaveformTypeOptions(true, backend, currentOptions); updateEnableUntilMark(); } @@ -511,14 +511,14 @@ void DlgPrefWaveform::updateWaveformAcceleration( void DlgPrefWaveform::updateWaveformTypeOptions(bool useWaveform, WaveformWidgetBackend backend, - allshader::WaveformRendererSignalBase::Options currentOptions) { + WaveformRendererSignalBase::Options currentOptions) { splitLeftRightCheckBox->blockSignals(true); highDetailCheckBox->blockSignals(true); #ifdef MIXXX_USE_QOPENGL WaveformWidgetFactory* factory = WaveformWidgetFactory::instance(); - allshader::WaveformRendererSignalBase::Options supportedOption = - allshader::WaveformRendererSignalBase::Option::None; + WaveformRendererSignalBase::Options supportedOption = + WaveformRendererSignalBase::Option::None; auto type = static_cast(waveformTypeComboBox->currentData().toInt()); int handleIdx = factory->findHandleIndexFromType(type); @@ -529,15 +529,15 @@ void DlgPrefWaveform::updateWaveformTypeOptions(bool useWaveform, splitLeftRightCheckBox->setEnabled(useWaveform && supportedOption & - allshader::WaveformRendererSignalBase::Option::SplitStereoSignal); + WaveformRendererSignalBase::Option::SplitStereoSignal); highDetailCheckBox->setEnabled(useWaveform && supportedOption & - allshader::WaveformRendererSignalBase::Option::HighDetail); + WaveformRendererSignalBase::Option::HighDetail); splitLeftRightCheckBox->setChecked(splitLeftRightCheckBox->isEnabled() && currentOptions & - allshader::WaveformRendererSignalBase::Option::SplitStereoSignal); + WaveformRendererSignalBase::Option::SplitStereoSignal); highDetailCheckBox->setChecked(highDetailCheckBox->isEnabled() && - currentOptions & allshader::WaveformRendererSignalBase::Option::HighDetail); + currentOptions & WaveformRendererSignalBase::Option::HighDetail); #else splitLeftRightCheckBox->setVisible(false); highDetailCheckBox->setVisible(false); diff --git a/src/preferences/dialog/dlgprefwaveform.h b/src/preferences/dialog/dlgprefwaveform.h index 9e020ca71467..240f378c0c79 100644 --- a/src/preferences/dialog/dlgprefwaveform.h +++ b/src/preferences/dialog/dlgprefwaveform.h @@ -35,14 +35,14 @@ class DlgPrefWaveform : public DlgPreferencePage, public Ui::DlgPrefWaveformDlg void slotSetWaveformEnabled(bool checked); void slotSetWaveformAcceleration(bool checked); #ifdef MIXXX_USE_QOPENGL - void slotSetWaveformOptions(allshader::WaveformRendererSignalBase::Option option, bool enabled); + void slotSetWaveformOptions(WaveformRendererSignalBase::Option option, bool enabled); void slotSetWaveformOptionSplitStereoSignal(bool checked) { - slotSetWaveformOptions(allshader::WaveformRendererSignalBase::Option:: + slotSetWaveformOptions(WaveformRendererSignalBase::Option:: SplitStereoSignal, checked); } void slotSetWaveformOptionHighDetail(bool checked) { - slotSetWaveformOptions(allshader::WaveformRendererSignalBase::Option::HighDetail, checked); + slotSetWaveformOptions(WaveformRendererSignalBase::Option::HighDetail, checked); } #endif void slotSetWaveformOverviewType(); @@ -74,7 +74,7 @@ class DlgPrefWaveform : public DlgPreferencePage, public Ui::DlgPrefWaveformDlg void updateEnableUntilMark(); void updateWaveformTypeOptions(bool useWaveform, WaveformWidgetBackend backend, - allshader::WaveformRendererSignalBase::Options currentOption); + WaveformRendererSignalBase::Options currentOption); void updateWaveformAcceleration( WaveformWidgetType::Type type, WaveformWidgetBackend backend); void updateWaveformGeneralOptionsEnabled(); diff --git a/src/preferences/upgrade.cpp b/src/preferences/upgrade.cpp index 1d7704a6da47..f4641a5961a7 100644 --- a/src/preferences/upgrade.cpp +++ b/src/preferences/upgrade.cpp @@ -37,7 +37,7 @@ namespace { // mapping to proactively move users to the new all-shader waveform types std::tuple + WaveformRendererSignalBase::Options> upgradeToAllShaders(int unsafeWaveformType, int unsafeWaveformBackend, int unsafeWaveformOption) { @@ -45,10 +45,10 @@ upgradeToAllShaders(int unsafeWaveformType, using WWT = WaveformWidgetType; if (static_cast(WaveformWidgetBackend::AllShader) == unsafeWaveformBackend) { - allshader::WaveformRendererSignalBase::Options waveformOption = - static_cast( + WaveformRendererSignalBase::Options waveformOption = + static_cast( unsafeWaveformOption) & - allshader::WaveformRendererSignalBase::Option::AllOptionsCombined; + WaveformRendererSignalBase::Option::AllOptionsCombined; switch (unsafeWaveformType) { case WWT::Simple: case WWT::Filtered: @@ -67,8 +67,8 @@ upgradeToAllShaders(int unsafeWaveformType, } // Reset the options - allshader::WaveformRendererSignalBase::Options waveformOption = - allshader::WaveformRendererSignalBase::Option::None; + WaveformRendererSignalBase::Options waveformOption = + WaveformRendererSignalBase::Option::None; WaveformWidgetType::Type waveformType = static_cast(unsafeWaveformType); WaveformWidgetBackend waveformBackend = WaveformWidgetBackend::AllShader; @@ -97,7 +97,7 @@ upgradeToAllShaders(int unsafeWaveformType, // Filtered waveforms case WWT::Filtered: // GLSLFilteredWaveform case 22: // AllShaderTexturedFiltered - waveformOption = allshader::WaveformRendererSignalBase::Option::HighDetail; + waveformOption = WaveformRendererSignalBase::Option::HighDetail; [[fallthrough]]; case 2: // SoftwareWaveform case 4: // QtWaveform @@ -116,7 +116,7 @@ upgradeToAllShaders(int unsafeWaveformType, // Stacked waveform case 24: // AllShaderTexturedStacked case WWT::Stacked: // GLSLRGBStackedWaveform - waveformOption = allshader::WaveformRendererSignalBase::Option::HighDetail; + waveformOption = WaveformRendererSignalBase::Option::HighDetail; [[fallthrough]]; case 26: // AllShaderRGBStackedWaveform waveformType = WaveformWidgetType::Stacked; @@ -127,8 +127,8 @@ upgradeToAllShaders(int unsafeWaveformType, case 23: // AllShaderTexturedRGB case 12: // GLSLRGBWaveform waveformOption = unsafeWaveformType == 18 - ? allshader::WaveformRendererSignalBase::Option::SplitStereoSignal - : allshader::WaveformRendererSignalBase::Option::HighDetail; + ? WaveformRendererSignalBase::Option::SplitStereoSignal + : WaveformRendererSignalBase::Option::HighDetail; [[fallthrough]]; default: waveformType = WaveformWidgetFactory::defaultType(); diff --git a/src/qml/qmlconfigproxy.cpp b/src/qml/qmlconfigproxy.cpp index 091d4e348293..4d3aba4f5e87 100644 --- a/src/qml/qmlconfigproxy.cpp +++ b/src/qml/qmlconfigproxy.cpp @@ -15,6 +15,13 @@ QVariantList paletteToQColorList(const ColorPalette& palette) { const QString kPreferencesGroup = QStringLiteral("[Preferences]"); const QString kMultiSamplingKey = QStringLiteral("multi_sampling"); +const QString k3DHardwareAccelerationKey = QStringLiteral("force_hardware_acceleration"); + +const QString kWaveformGroup = QStringLiteral("[Waveform]"); +const QString kWaveformZoomSynchronizationKey = QStringLiteral("ZoomSynchronization"); +const QString kWaveformDefaultZoomKey = QStringLiteral("DefaultZoom"); +const bool kWaveformZoomSynchronizationDefault = true; +const double kWaveformDefaultZoomDefault = 3.0; } // namespace @@ -42,6 +49,27 @@ int QmlConfigProxy::getMultiSamplingLevel() { mixxx::preferences::MultiSamplingMode::Disabled)); } +bool QmlConfigProxy::useAcceleration() { + if (!m_pConfig->exists( + ConfigKey(kPreferencesGroup, k3DHardwareAccelerationKey))) { + // TODO: detect whether QML currently run with 3D acceleration. QSGRendererInterface? + return false; + } + return m_pConfig->getValue( + ConfigKey(kPreferencesGroup, k3DHardwareAccelerationKey)); +} + +bool QmlConfigProxy::waveformZoomSynchronization() { + return m_pConfig->getValue( + ConfigKey(kWaveformGroup, kWaveformZoomSynchronizationKey), + kWaveformZoomSynchronizationDefault); +} +double QmlConfigProxy::waveformDefaultZoom() { + return m_pConfig->getValue( + ConfigKey(kWaveformGroup, kWaveformDefaultZoomKey), + kWaveformDefaultZoomDefault); +} + // static QmlConfigProxy* QmlConfigProxy::create(QQmlEngine* pQmlEngine, QJSEngine* pJsEngine) { // The implementation of this method is mostly taken from the code example diff --git a/src/qml/qmlconfigproxy.h b/src/qml/qmlconfigproxy.h index a0e107276d9a..d5a03bc7040e 100644 --- a/src/qml/qmlconfigproxy.h +++ b/src/qml/qmlconfigproxy.h @@ -23,6 +23,11 @@ class QmlConfigProxy : public QObject { Q_INVOKABLE QVariantList getHotcueColorPalette(); Q_INVOKABLE QVariantList getTrackColorPalette(); Q_INVOKABLE int getMultiSamplingLevel(); + Q_INVOKABLE bool useAcceleration(); + + // Waveform settings + Q_INVOKABLE bool waveformZoomSynchronization(); + Q_INVOKABLE double waveformDefaultZoom(); static QmlConfigProxy* create(QQmlEngine* pQmlEngine, QJSEngine* pJsEngine); static inline void registerUserSettings(UserSettingsPointer pConfig) { diff --git a/src/qml/qmlplayermanagerproxy.cpp b/src/qml/qmlplayermanagerproxy.cpp index 2544b34f3ad5..cc2d1b3a8e97 100644 --- a/src/qml/qmlplayermanagerproxy.cpp +++ b/src/qml/qmlplayermanagerproxy.cpp @@ -5,6 +5,7 @@ #include "mixer/playermanager.h" #include "moc_qmlplayermanagerproxy.cpp" #include "qml/qmlplayerproxy.h" +#include "track/track_decl.h" namespace mixxx { namespace qml { @@ -31,6 +32,20 @@ QmlPlayerProxy* QmlPlayerManagerProxy::getPlayer(const QString& group) { [this, group](const QString& trackLocation, bool play) { loadLocationToPlayer(trackLocation, group, play); }); + connect(pPlayerProxy, + &QmlPlayerProxy::loadTrackRequested, + this, + [this, group](TrackPointer track, +#ifdef __STEM__ + mixxx::StemChannelSelection stemSelection, +#endif + bool play) { + loadTrackToPlayer(track, group, +#ifdef __STEM__ + stemSelection, +#endif + play); + }); connect(pPlayerProxy, &QmlPlayerProxy::cloneFromGroup, this, @@ -59,6 +74,19 @@ void QmlPlayerManagerProxy::loadLocationToPlayer( m_pPlayerManager->slotLoadLocationToPlayer(location, group, play); } +void QmlPlayerManagerProxy::loadTrackToPlayer(TrackPointer track, + const QString& group, +#ifdef __STEM__ + mixxx::StemChannelSelection stemSelection, +#endif + bool play) { + m_pPlayerManager->slotLoadTrackToPlayer(track, group, +#ifdef __STEM__ + stemSelection, +#endif + play); +} + // static QmlPlayerManagerProxy* QmlPlayerManagerProxy::create(QQmlEngine* pQmlEngine, QJSEngine* pJsEngine) { // The implementation of this method is mostly taken from the code example diff --git a/src/qml/qmlplayermanagerproxy.h b/src/qml/qmlplayermanagerproxy.h index 1cf650b7ceb8..3f23ad664867 100644 --- a/src/qml/qmlplayermanagerproxy.h +++ b/src/qml/qmlplayermanagerproxy.h @@ -24,6 +24,12 @@ class QmlPlayerManagerProxy : public QObject { const QUrl& locationUrl, bool play = false); Q_INVOKABLE void loadLocationToPlayer( const QString& location, const QString& group, bool play = false); + Q_INVOKABLE void loadTrackToPlayer(TrackPointer track, + const QString& group, +#ifdef __STEM__ + mixxx::StemChannelSelection stemSelection, +#endif + bool play); static QmlPlayerManagerProxy* create(QQmlEngine* pQmlEngine, QJSEngine* pJsEngine); static void registerPlayerManager(std::shared_ptr pPlayerManager) { diff --git a/src/qml/qmlplayerproxy.cpp b/src/qml/qmlplayerproxy.cpp index d3b6056fda6b..ba5b58ea2b6d 100644 --- a/src/qml/qmlplayerproxy.cpp +++ b/src/qml/qmlplayerproxy.cpp @@ -1,42 +1,19 @@ #include "qml/qmlplayerproxy.h" #include +#include #include "mixer/basetrackplayer.h" #include "moc_qmlplayerproxy.cpp" -#include "qml/asyncimageprovider.h" - -#define PROPERTY_IMPL_GETTER(TYPE, NAME, GETTER) \ - TYPE QmlPlayerProxy::GETTER() const { \ - const TrackPointer pTrack = m_pCurrentTrack; \ - if (pTrack == nullptr) { \ - return TYPE(); \ - } \ - return pTrack->GETTER(); \ - } - -#define PROPERTY_IMPL(TYPE, NAME, GETTER, SETTER) \ - PROPERTY_IMPL_GETTER(TYPE, NAME, GETTER) \ - void QmlPlayerProxy::SETTER(const TYPE& value) { \ - const TrackPointer pTrack = m_pCurrentTrack; \ - if (pTrack != nullptr) { \ - pTrack->SETTER(value); \ - } \ - } +#include "qmltrackproxy.h" +#include "track/track.h" namespace mixxx { namespace qml { QmlPlayerProxy::QmlPlayerProxy(BaseTrackPlayer* pTrackPlayer, QObject* parent) : QObject(parent), - m_pTrackPlayer(pTrackPlayer), - m_pBeatsModel(new QmlBeatsModel(this)), - m_pHotcuesModel(new QmlCuesModel(this)) -#ifdef __STEM__ - , - m_pStemsModel(std::make_unique(this)) -#endif -{ + m_pTrackPlayer(pTrackPlayer) { connect(m_pTrackPlayer, &BaseTrackPlayer::loadingTrack, this, @@ -46,15 +23,25 @@ QmlPlayerProxy::QmlPlayerProxy(BaseTrackPlayer* pTrackPlayer, QObject* parent) this, &QmlPlayerProxy::slotTrackLoaded); connect(m_pTrackPlayer, - &BaseTrackPlayer::playerEmpty, + &BaseTrackPlayer::trackUnloaded, this, - &QmlPlayerProxy::trackUnloaded); - connect(this, &QmlPlayerProxy::trackChanged, this, &QmlPlayerProxy::slotTrackChanged); + &QmlPlayerProxy::slotTrackUnloaded); if (m_pTrackPlayer && m_pTrackPlayer->getLoadedTrack()) { slotTrackLoaded(pTrackPlayer->getLoadedTrack()); } } +void QmlPlayerProxy::loadTrack(QmlTrackProxy* track, bool play) { + if (track == nullptr || track->internal() == nullptr) { + return; + } + emit loadTrackRequested(track->internal(), +#ifdef __STEM__ + mixxx::StemChannel::All, +#endif + play); +} + void QmlPlayerProxy::loadTrackFromLocation(const QString& trackLocation, bool play) { emit loadTrackFromLocationRequested(trackLocation, play); } @@ -69,323 +56,47 @@ void QmlPlayerProxy::loadTrackFromLocationUrl(const QUrl& trackLocationUrl, bool void QmlPlayerProxy::slotTrackLoaded(TrackPointer pTrack) { m_pCurrentTrack = pTrack; - if (pTrack != nullptr) { - connect(pTrack.get(), - &Track::artistChanged, - this, - &QmlPlayerProxy::artistChanged); - connect(pTrack.get(), - &Track::titleChanged, - this, - &QmlPlayerProxy::titleChanged); - connect(pTrack.get(), - &Track::albumChanged, - this, - &QmlPlayerProxy::albumChanged); - connect(pTrack.get(), - &Track::albumArtistChanged, - this, - &QmlPlayerProxy::albumArtistChanged); - connect(pTrack.get(), - &Track::genreChanged, - this, - &QmlPlayerProxy::genreChanged); - connect(pTrack.get(), - &Track::composerChanged, - this, - &QmlPlayerProxy::composerChanged); - connect(pTrack.get(), - &Track::groupingChanged, - this, - &QmlPlayerProxy::groupingChanged); - connect(pTrack.get(), - &Track::yearChanged, - this, - &QmlPlayerProxy::yearChanged); - connect(pTrack.get(), - &Track::trackNumberChanged, - this, - &QmlPlayerProxy::trackNumberChanged); - connect(pTrack.get(), - &Track::trackTotalChanged, - this, - &QmlPlayerProxy::trackTotalChanged); - connect(pTrack.get(), - &Track::commentChanged, - this, - &QmlPlayerProxy::commentChanged); - connect(pTrack.get(), - &Track::keyChanged, - this, - &QmlPlayerProxy::keyTextChanged); - connect(pTrack.get(), - &Track::colorUpdated, - this, - &QmlPlayerProxy::colorChanged); - connect(pTrack.get(), - &Track::waveformUpdated, - this, - &QmlPlayerProxy::slotWaveformChanged); - connect(pTrack.get(), - &Track::beatsUpdated, - this, - &QmlPlayerProxy::slotBeatsChanged); - connect(pTrack.get(), - &Track::cuesUpdated, - this, - &QmlPlayerProxy::slotHotcuesChanged); -#ifdef __STEM__ - connect(pTrack.get(), - &Track::stemsUpdated, - this, - &QmlPlayerProxy::slotStemsChanged); -#endif - slotBeatsChanged(); - slotHotcuesChanged(); -#ifdef __STEM__ - slotStemsChanged(); -#endif - slotWaveformChanged(); - } emit trackChanged(); emit trackLoaded(); } -void QmlPlayerProxy::slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack) { +void QmlPlayerProxy::slotTrackUnloaded(TrackPointer pOldTrack) { VERIFY_OR_DEBUG_ASSERT(pOldTrack == m_pCurrentTrack) { qWarning() << "QML Player proxy was expected to contain " << pOldTrack.get() << "as active track but got" << m_pCurrentTrack.get(); } - - if (pNewTrack.get() == m_pCurrentTrack.get()) { - emit trackLoading(); - return; - } - - const TrackPointer pTrack = m_pCurrentTrack; - if (pTrack != nullptr) { - disconnect(pTrack.get(), nullptr, this, nullptr); + if (m_pCurrentTrack != nullptr) { + disconnect(m_pCurrentTrack.get(), nullptr, this, nullptr); } m_pCurrentTrack.reset(); - m_pCurrentTrack = pNewTrack; - m_waveformTexture = QImage(); emit trackChanged(); - emit trackLoading(); -} - -void QmlPlayerProxy::slotTrackChanged() { - emit artistChanged(); - emit titleChanged(); - emit albumChanged(); - emit albumArtistChanged(); - emit genreChanged(); - emit composerChanged(); - emit groupingChanged(); - emit yearChanged(); - emit trackNumberChanged(); - emit trackTotalChanged(); - emit commentChanged(); - emit keyTextChanged(); - emit colorChanged(); - emit coverArtUrlChanged(); - emit trackLocationUrlChanged(); -#ifdef __STEM__ - emit stemsChanged(); -#endif - - emit waveformLengthChanged(); - emit waveformTextureChanged(); - emit waveformTextureSizeChanged(); - emit waveformTextureStrideChanged(); + emit trackUnloaded(); } -void QmlPlayerProxy::slotWaveformChanged() { - emit waveformLengthChanged(); - emit waveformTextureSizeChanged(); - emit waveformTextureStrideChanged(); - - const TrackPointer pTrack = m_pCurrentTrack; - if (!pTrack) { - return; - } - const ConstWaveformPointer pWaveform = - pTrack->getWaveform(); - if (!pWaveform) { - return; - } - const int textureWidth = pWaveform->getTextureStride(); - const int textureHeight = pWaveform->getTextureSize() / pWaveform->getTextureStride(); - - const WaveformData* data = pWaveform->data(); - // Make a copy of the waveform data, stripping the stems portion. Note that the datasize is - // different from the texture size -- we want the full texture size so the upload works. See - // m_data in waveform/waveform.h. - m_waveformData.resize(pWaveform->getTextureSize()); - for (int i = 0; i < pWaveform->getDataSize(); i++) { - m_waveformData[i] = data[i].filtered; - } - - m_waveformTexture = - QImage(reinterpret_cast(m_waveformData.data()), - textureWidth, - textureHeight, - QImage::Format_RGBA8888); - DEBUG_ASSERT(!m_waveformTexture.isNull()); - emit waveformTextureChanged(); -} - -void QmlPlayerProxy::slotBeatsChanged() { - VERIFY_OR_DEBUG_ASSERT(m_pBeatsModel != nullptr) { - return; - } - - const TrackPointer pTrack = m_pCurrentTrack; - if (pTrack) { - const auto trackEndPosition = mixxx::audio::FramePos{ - pTrack->getDuration() * pTrack->getSampleRate()}; - const auto pBeats = pTrack->getBeats(); - m_pBeatsModel->setBeats(pBeats, trackEndPosition); - } else { - m_pBeatsModel->setBeats(nullptr, audio::kStartFramePos); - } +QmlTrackProxy* QmlPlayerProxy::currentTrack() { + auto* pTrack = new QmlTrackProxy(m_pCurrentTrack, this); + QQmlEngine::setObjectOwnership(pTrack, QQmlEngine::JavaScriptOwnership); + return pTrack; } -#ifdef __STEM__ -void QmlPlayerProxy::slotStemsChanged() { - VERIFY_OR_DEBUG_ASSERT(m_pStemsModel != nullptr) { - return; - } - - const TrackPointer pTrack = m_pCurrentTrack; - if (pTrack) { - m_pStemsModel->setStems(pTrack->getStemInfo()); - emit stemsChanged(); - } -} -#endif - -void QmlPlayerProxy::slotHotcuesChanged() { - VERIFY_OR_DEBUG_ASSERT(m_pHotcuesModel != nullptr) { +void QmlPlayerProxy::slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack) { + if (pNewTrack.get() == m_pCurrentTrack.get()) { + emit trackLoading(); return; } - QList hotcues; - - const TrackPointer pTrack = m_pCurrentTrack; - if (pTrack) { - const auto& cuePoints = pTrack->getCuePoints(); - for (const auto& cuePoint : cuePoints) { - if (cuePoint->getHotCue() == Cue::kNoHotCue) - continue; - hotcues.append(cuePoint); - } + if (m_pCurrentTrack != nullptr) { + disconnect(m_pCurrentTrack.get(), nullptr, this, nullptr); } - m_pHotcuesModel->setCues(hotcues); - emit cuesChanged(); -} - -int QmlPlayerProxy::getWaveformLength() const { - const TrackPointer pTrack = m_pCurrentTrack; - if (pTrack) { - const ConstWaveformPointer pWaveform = pTrack->getWaveform(); - if (pWaveform) { - return pWaveform->getDataSize(); - } - } - return 0; -} - -QString QmlPlayerProxy::getWaveformTexture() const { - if (m_waveformTexture.isNull()) { - return QString(); - } - QByteArray byteArray; - QBuffer buffer(&byteArray); - buffer.open(QIODevice::WriteOnly); - m_waveformTexture.save(&buffer, "png"); - - QString imageData = QString::fromLatin1(byteArray.toBase64().data()); - if (imageData.isEmpty()) { - return QString(); - } - - return QStringLiteral("data:image/png;base64,") + imageData; -} - -int QmlPlayerProxy::getWaveformTextureSize() const { - const TrackPointer pTrack = m_pCurrentTrack; - if (pTrack) { - const ConstWaveformPointer pWaveform = pTrack->getWaveform(); - if (pWaveform) { - return pWaveform->getTextureSize(); - } - } - return 0; -} - -int QmlPlayerProxy::getWaveformTextureStride() const { - const TrackPointer pTrack = m_pCurrentTrack; - if (pTrack) { - const ConstWaveformPointer pWaveform = pTrack->getWaveform(); - if (pWaveform) { - return pWaveform->getTextureStride(); - } - } - return 0; + m_pCurrentTrack = pNewTrack; + emit trackChanged(); + emit trackLoading(); } bool QmlPlayerProxy::isLoaded() const { return m_pCurrentTrack != nullptr; } -PROPERTY_IMPL(QString, artist, getArtist, setArtist) -PROPERTY_IMPL(QString, title, getTitle, setTitle) -PROPERTY_IMPL(QString, album, getAlbum, setAlbum) -PROPERTY_IMPL(QString, albumArtist, getAlbumArtist, setAlbumArtist) -PROPERTY_IMPL_GETTER(QString, genre, getGenre) -PROPERTY_IMPL(QString, composer, getComposer, setComposer) -PROPERTY_IMPL(QString, grouping, getGrouping, setGrouping) -PROPERTY_IMPL(QString, year, getYear, setYear) -PROPERTY_IMPL(QString, trackNumber, getTrackNumber, setTrackNumber) -PROPERTY_IMPL(QString, trackTotal, getTrackTotal, setTrackTotal) -PROPERTY_IMPL(QString, comment, getComment, setComment) -PROPERTY_IMPL(QString, keyText, getKeyText, setKeyText) - -QColor QmlPlayerProxy::getColor() const { - const TrackPointer pTrack = m_pCurrentTrack; - if (pTrack == nullptr) { - return QColor(); - } - return RgbColor::toQColor(pTrack->getColor()); -} - -void QmlPlayerProxy::setColor(const QColor& value) { - const TrackPointer pTrack = m_pTrackPlayer->getLoadedTrack(); - if (pTrack != nullptr) { - std::optional color = RgbColor::fromQColor(value); - pTrack->setColor(color); - } -} - -QUrl QmlPlayerProxy::getCoverArtUrl() const { - const TrackPointer pTrack = m_pCurrentTrack; - if (pTrack == nullptr) { - return QUrl(); - } - - const CoverInfo coverInfo = pTrack->getCoverInfoWithLocation(); - return AsyncImageProvider::trackLocationToCoverArtUrl(coverInfo.trackLocation); -} - -QUrl QmlPlayerProxy::getTrackLocationUrl() const { - const TrackPointer pTrack = m_pCurrentTrack; - if (pTrack == nullptr) { - return QUrl(); - } - - return QUrl::fromLocalFile(pTrack->getLocation()); -} - } // namespace qml } // namespace mixxx diff --git a/src/qml/qmlplayerproxy.h b/src/qml/qmlplayerproxy.h index 54f42bb9c9a3..538639bdb0fa 100644 --- a/src/qml/qmlplayerproxy.h +++ b/src/qml/qmlplayerproxy.h @@ -7,115 +7,36 @@ #include #include "mixer/basetrackplayer.h" -#include "qml/qmlbeatsmodel.h" -#include "qml/qmlcuesmodel.h" -#include "qml/qmlstemsmodel.h" -#include "track/cueinfo.h" -#include "track/track.h" -#include "waveform/waveform.h" +#include "qmltrackproxy.h" +#include "track/track_decl.h" namespace mixxx { namespace qml { class QmlPlayerProxy : public QObject { Q_OBJECT + Q_PROPERTY(QmlTrackProxy* currentTrack READ currentTrack NOTIFY trackChanged) Q_PROPERTY(bool isLoaded READ isLoaded NOTIFY trackChanged) - Q_PROPERTY(QString artist READ getArtist WRITE setArtist NOTIFY artistChanged) - Q_PROPERTY(QString title READ getTitle WRITE setTitle NOTIFY titleChanged) - Q_PROPERTY(QString album READ getAlbum WRITE setAlbum NOTIFY albumChanged) - Q_PROPERTY(QString albumArtist READ getAlbumArtist WRITE setAlbumArtist - NOTIFY albumArtistChanged) - Q_PROPERTY(QString genre READ getGenre STORED false NOTIFY genreChanged) - Q_PROPERTY(QString composer READ getComposer WRITE setComposer NOTIFY composerChanged) - Q_PROPERTY(QString grouping READ getGrouping WRITE setGrouping NOTIFY groupingChanged) - Q_PROPERTY(QString year READ getYear WRITE setYear NOTIFY yearChanged) - Q_PROPERTY(QString trackNumber READ getTrackNumber WRITE setTrackNumber - NOTIFY trackNumberChanged) - Q_PROPERTY(QString trackTotal READ getTrackTotal WRITE setTrackTotal NOTIFY trackTotalChanged) - Q_PROPERTY(QString comment READ getComment WRITE setComment NOTIFY commentChanged) - Q_PROPERTY(QString keyText READ getKeyText WRITE setKeyText NOTIFY keyTextChanged) - Q_PROPERTY(QColor color READ getColor WRITE setColor NOTIFY colorChanged) - Q_PROPERTY(QUrl coverArtUrl READ getCoverArtUrl NOTIFY coverArtUrlChanged) - Q_PROPERTY(QUrl trackLocationUrl READ getTrackLocationUrl NOTIFY trackLocationUrlChanged) QML_NAMED_ELEMENT(Player) QML_UNCREATABLE("Only accessible via Mixxx.PlayerManager.getPlayer(group)") - Q_PROPERTY(int waveformLength READ getWaveformLength NOTIFY waveformLengthChanged) - Q_PROPERTY(QString waveformTexture READ getWaveformTexture NOTIFY waveformTextureChanged) - Q_PROPERTY(int waveformTextureSize READ getWaveformTextureSize NOTIFY - waveformTextureSizeChanged) - Q_PROPERTY(int waveformTextureStride READ getWaveformTextureStride NOTIFY - waveformTextureStrideChanged) - - Q_PROPERTY(mixxx::qml::QmlBeatsModel* beatsModel MEMBER m_pBeatsModel CONSTANT); - Q_PROPERTY(mixxx::qml::QmlCuesModel* hotcuesModel MEMBER m_pHotcuesModel CONSTANT); -#ifdef __STEM__ - Q_PROPERTY(mixxx::qml::QmlStemsModel* stemsModel READ getStemsModel CONSTANT); -#endif - public: explicit QmlPlayerProxy(BaseTrackPlayer* pTrackPlayer, QObject* parent = nullptr); bool isLoaded() const; - QString getTrack() const; - QString getTitle() const; - QString getArtist() const; - QString getAlbum() const; - QString getAlbumArtist() const; - QString getGenre() const; - QString getComposer() const; - QString getGrouping() const; - QString getYear() const; - QString getTrackNumber() const; - QString getTrackTotal() const; - QString getComment() const; - QString getKeyText() const; - QColor getColor() const; - QUrl getCoverArtUrl() const; - QUrl getTrackLocationUrl() const; - - int getWaveformLength() const; - QString getWaveformTexture() const; - int getWaveformTextureSize() const; - int getWaveformTextureStride() const; - /// Needed for interacting with the raw track player object. BaseTrackPlayer* internalTrackPlayer() const { return m_pTrackPlayer; } + Q_INVOKABLE void loadTrack(mixxx::qml::QmlTrackProxy* track, bool play = false); Q_INVOKABLE void loadTrackFromLocation(const QString& trackLocation, bool play = false); Q_INVOKABLE void loadTrackFromLocationUrl(const QUrl& trackLocationUrl, bool play = false); -#ifdef __STEM__ - QmlStemsModel* getStemsModel() const { - return m_pStemsModel.get(); - } -#endif - public slots: void slotTrackLoaded(TrackPointer pTrack); + void slotTrackUnloaded(TrackPointer pOldTrack); void slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack); - void slotTrackChanged(); - void slotWaveformChanged(); - void slotBeatsChanged(); - void slotHotcuesChanged(); -#ifdef __STEM__ - void slotStemsChanged(); -#endif - - void setArtist(const QString& artist); - void setTitle(const QString& title); - void setAlbum(const QString& album); - void setAlbumArtist(const QString& albumArtist); - void setComposer(const QString& composer); - void setGrouping(const QString& grouping); - void setYear(const QString& year); - void setTrackNumber(const QString& trackNumber); - void setTrackTotal(const QString& trackTotal); - void setComment(const QString& comment); - void setKeyText(const QString& keyText); - void setColor(const QColor& color); signals: void trackLoading(); @@ -124,43 +45,18 @@ class QmlPlayerProxy : public QObject { void trackChanged(); void cloneFromGroup(const QString& group); - void albumChanged(); - void titleChanged(); - void artistChanged(); - void albumArtistChanged(); - void genreChanged(); - void composerChanged(); - void groupingChanged(); - void yearChanged(); - void trackNumberChanged(); - void trackTotalChanged(); - void commentChanged(); - void keyTextChanged(); - void colorChanged(); - void coverArtUrlChanged(); - void trackLocationUrlChanged(); - void cuesChanged(); + void loadTrackFromLocationRequested(const QString& trackLocation, bool play); + void loadTrackRequested(TrackPointer track, #ifdef __STEM__ - void stemsChanged(); + mixxx::StemChannelSelection stemSelection, #endif - - void loadTrackFromLocationRequested(const QString& trackLocation, bool play); - - void waveformLengthChanged(); - void waveformTextureChanged(); - void waveformTextureSizeChanged(); - void waveformTextureStrideChanged(); + bool play); private: - std::vector m_waveformData; - QImage m_waveformTexture; + QmlTrackProxy* currentTrack(); + QPointer m_pTrackPlayer; TrackPointer m_pCurrentTrack; - QmlBeatsModel* m_pBeatsModel; - QmlCuesModel* m_pHotcuesModel; -#ifdef __STEM__ - std::unique_ptr m_pStemsModel; -#endif }; } // namespace qml diff --git a/src/qml/qmlsettingparameter.cpp b/src/qml/qmlsettingparameter.cpp new file mode 100644 index 000000000000..a951c179df33 --- /dev/null +++ b/src/qml/qmlsettingparameter.cpp @@ -0,0 +1,74 @@ +#include "qml/qmlsettingparameter.h" + +#include +#include + +#include "moc_qmlsettingparameter.cpp" +#include "util/assert.h" + +namespace mixxx { +namespace qml { + +QmlSettingGroup::QmlSettingGroup(QQuickItem* parent) + : QQuickItem(parent) { +} +QmlSettingParameter::QmlSettingParameter(QQuickItem* parent) + : QmlSettingGroup(parent) { +} + +void QmlSettingParameter::componentComplete() { + QmlSettingGroup::componentComplete(); + QList pathItems; + auto* pParent = parentItem(); + while (pParent != nullptr) { + auto* pManager = qobject_cast(pParent); + if (pManager) { + pManager->registerSettingParamater(this, pathItems); + return; + } + auto* pGroup = qobject_cast(pParent); + if (pGroup) { + pathItems.prepend(pGroup); + } + pParent = pParent->parentItem(); + } + DEBUG_ASSERT(!"Couldn't find manager!"); +} + +QmlSettingParameterManager::QmlSettingParameterManager(QQuickItem* parent) + : QQuickItem(parent), + m_model(this) { + m_model.setSourceModel(&m_sourceModel); + m_model.setFilterKeyColumn(1); + m_model.setFilterCaseSensitivity(Qt::CaseInsensitive); +} +QmlSettingParameterManager::~QmlSettingParameterManager() { + // Manually deleting children so they can complete deregistration + qDeleteAll(childItems()); +} + +void QmlSettingParameterManager::registerSettingParamater( + QmlSettingParameter* pParameter, QList pathItems) { + QStringList path; + for (const auto* pItem : pathItems) { + path.append(pItem->label()); + } + pathItems.append(pParameter); + + auto* pItem = new QStandardItem(pParameter->label()); + pItem->setData(path.join(" > "), Qt::WhatsThisRole); + pItem->setData(QVariant::fromValue(pathItems), Qt::ToolTipRole); + m_sourceModel.appendRow(QList{pItem, + new QStandardItem(pParameter->label() + path.join(" > "))}); + auto rowIndex = m_sourceModel.index(m_sourceModel.rowCount() - 1, 0); + connect(pParameter, &QObject::destroyed, this, [this, rowIndex](QObject*) { + m_sourceModel.removeRow(rowIndex.row()); + }); +} + +void QmlSettingParameterManager::search(const QString& criteria) { + m_model.setFilterFixedString(criteria); +} + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmlsettingparameter.h b/src/qml/qmlsettingparameter.h new file mode 100644 index 000000000000..fc67c6b26772 --- /dev/null +++ b/src/qml/qmlsettingparameter.h @@ -0,0 +1,67 @@ +#pragma once +#include +#include +#include +#include + +namespace mixxx { +namespace qml { + +class QmlSettingGroup : public QQuickItem { + Q_OBJECT + Q_PROPERTY(QString label MEMBER m_label FINAL) + QML_NAMED_ELEMENT(SettingGroup) + public: + explicit QmlSettingGroup(QQuickItem* parent = nullptr); + + const QString& label() const { + return m_label; + } + signals: + Q_INVOKABLE void activated(); + + private: + QString m_label; +}; + +class QmlSettingParameterManager; +class QmlSettingParameter : public QmlSettingGroup { + Q_OBJECT + Q_PROPERTY(QStringList keywords MEMBER m_keywords FINAL) + QML_NAMED_ELEMENT(SettingParameter) + Q_INTERFACES(QQmlParserStatus) + public: + explicit QmlSettingParameter(QQuickItem* parent = nullptr); + + void componentComplete() override; + + const QStringList& keywords() const { + return m_keywords; + } + + private: + QStringList m_keywords; +}; + +class QmlSettingParameterManager : public QQuickItem { + Q_OBJECT + Q_PROPERTY(QSortFilterProxyModel* model READ model CONSTANT) + QML_NAMED_ELEMENT(SettingParameterManager) + public: + explicit QmlSettingParameterManager(QQuickItem* parent = nullptr); + ~QmlSettingParameterManager(); + + void registerSettingParamater(QmlSettingParameter* parameter, QList); + Q_INVOKABLE void search(const QString& criteria); + + QSortFilterProxyModel* model() { + return &m_model; + } + + private: + QStandardItemModel m_sourceModel; + QSortFilterProxyModel m_model; +}; + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmltrackproxy.cpp b/src/qml/qmltrackproxy.cpp new file mode 100644 index 000000000000..61adb7af19b8 --- /dev/null +++ b/src/qml/qmltrackproxy.cpp @@ -0,0 +1,244 @@ +#include "qml/qmltrackproxy.h" + +#include + +#include "mixer/basetrackplayer.h" +#include "moc_qmltrackproxy.cpp" +#include "qml/asyncimageprovider.h" +#include "track/track.h" +#include "util/parented_ptr.h" + +#define PROPERTY_IMPL_GETTER(TYPE, NAME, GETTER) \ + TYPE QmlTrackProxy::GETTER() const { \ + const TrackPointer pTrack = m_pTrack; \ + if (pTrack == nullptr) { \ + return TYPE(); \ + } \ + return pTrack->GETTER(); \ + } + +#define PROPERTY_IMPL(TYPE, NAME, GETTER, SETTER) \ + PROPERTY_IMPL_GETTER(TYPE, NAME, GETTER) \ + void QmlTrackProxy::SETTER(const TYPE& value) { \ + const TrackPointer pTrack = m_pTrack; \ + if (pTrack != nullptr) { \ + pTrack->SETTER(value); \ + } \ + } + +namespace mixxx { +namespace qml { + +QmlTrackProxy::QmlTrackProxy(TrackPointer track, QObject* parent) + : QObject(parent), + m_pTrack(track), + m_pBeatsModel(make_parented(this)), + m_pHotcuesModel(make_parented(this)) +#ifdef __STEM__ + , + m_pStemsModel(make_parented(this)) +#endif +{ + if (m_pTrack == nullptr) { + return; + } + connect(m_pTrack.get(), + &Track::artistChanged, + this, + &QmlTrackProxy::artistChanged); + connect(m_pTrack.get(), + &Track::titleChanged, + this, + &QmlTrackProxy::titleChanged); + connect(m_pTrack.get(), + &Track::albumChanged, + this, + &QmlTrackProxy::albumChanged); + connect(m_pTrack.get(), + &Track::albumArtistChanged, + this, + &QmlTrackProxy::albumArtistChanged); + connect(m_pTrack.get(), + &Track::genreChanged, + this, + &QmlTrackProxy::genreChanged); + connect(m_pTrack.get(), + &Track::composerChanged, + this, + &QmlTrackProxy::composerChanged); + connect(m_pTrack.get(), + &Track::groupingChanged, + this, + &QmlTrackProxy::groupingChanged); + connect(m_pTrack.get(), + &Track::yearChanged, + this, + &QmlTrackProxy::yearChanged); + connect(m_pTrack.get(), + &Track::trackNumberChanged, + this, + &QmlTrackProxy::trackNumberChanged); + connect(m_pTrack.get(), + &Track::trackTotalChanged, + this, + &QmlTrackProxy::trackTotalChanged); + connect(m_pTrack.get(), + &Track::commentChanged, + this, + &QmlTrackProxy::commentChanged); + connect(m_pTrack.get(), + &Track::keyChanged, + this, + &QmlTrackProxy::keyTextChanged); + connect(m_pTrack.get(), + &Track::colorUpdated, + this, + &QmlTrackProxy::colorChanged); + connect(m_pTrack.get(), + &Track::beatsUpdated, + this, + &QmlTrackProxy::slotBeatsChanged); + connect(m_pTrack.get(), + &Track::cuesUpdated, + this, + &QmlTrackProxy::slotHotcuesChanged); + connect(m_pTrack.get(), + &Track::durationChanged, + this, + &QmlTrackProxy::durationChanged); +#ifdef __STEM__ + connect(m_pTrack.get(), + &Track::stemsUpdated, + this, + &QmlTrackProxy::slotStemsChanged); +#endif + slotBeatsChanged(); + slotHotcuesChanged(); +#ifdef __STEM__ + slotStemsChanged(); +#endif +} + +void QmlTrackProxy::slotBeatsChanged() { + VERIFY_OR_DEBUG_ASSERT(m_pBeatsModel) { + return; + } + + const TrackPointer pTrack = m_pTrack; + if (pTrack) { + const auto trackEndPosition = mixxx::audio::FramePos{ + pTrack->getDuration() * pTrack->getSampleRate()}; + const auto pBeats = pTrack->getBeats(); + m_pBeatsModel->setBeats(pBeats, trackEndPosition); + } else { + m_pBeatsModel->setBeats(nullptr, audio::kStartFramePos); + } +} + +#ifdef __STEM__ +void QmlTrackProxy::slotStemsChanged() { + VERIFY_OR_DEBUG_ASSERT(m_pStemsModel) { + return; + } + + if (m_pTrack) { + m_pStemsModel->setStems(m_pTrack->getStemInfo()); + emit stemsChanged(); + } +} +#endif + +void QmlTrackProxy::slotHotcuesChanged() { + VERIFY_OR_DEBUG_ASSERT(m_pHotcuesModel) { + return; + } + + QList hotcues; + + if (m_pTrack) { + const auto& cuePoints = m_pTrack->getCuePoints(); + for (const auto& cuePoint : cuePoints) { + if (cuePoint->getHotCue() == Cue::kNoHotCue) { + continue; + } + hotcues.append(cuePoint); + } + } + m_pHotcuesModel->setCues(hotcues); + emit cuesChanged(); +} + +PROPERTY_IMPL(QString, artist, getArtist, setArtist) +PROPERTY_IMPL(QString, title, getTitle, setTitle) +PROPERTY_IMPL(QString, album, getAlbum, setAlbum) +PROPERTY_IMPL(QString, albumArtist, getAlbumArtist, setAlbumArtist) +PROPERTY_IMPL_GETTER(QString, genre, getGenre) +PROPERTY_IMPL(QString, composer, getComposer, setComposer) +PROPERTY_IMPL(QString, grouping, getGrouping, setGrouping) +PROPERTY_IMPL(QString, year, getYear, setYear) +PROPERTY_IMPL(QString, trackNumber, getTrackNumber, setTrackNumber) +PROPERTY_IMPL(QString, trackTotal, getTrackTotal, setTrackTotal) +PROPERTY_IMPL(QString, comment, getComment, setComment) +PROPERTY_IMPL(QString, keyText, getKeyText, setKeyText) + +QColor QmlTrackProxy::getColor() const { + if (m_pTrack == nullptr) { + return QColor(); + } + return RgbColor::toQColor(m_pTrack->getColor()); +} + +double QmlTrackProxy::getDuration() const { + if (m_pTrack == nullptr) { + return -1; + } + return m_pTrack->getDuration(); +} + +int QmlTrackProxy::getSampleRate() const { + if (m_pTrack == nullptr) { + return 0; + } + return m_pTrack->getSampleRate(); +} + +void QmlTrackProxy::setColor(const QColor& value) { + if (m_pTrack) { + std::optional color = RgbColor::fromQColor(value); + m_pTrack->setColor(color); + } +} + +int QmlTrackProxy::getStars() const { + if (m_pTrack == nullptr) { + return -1; + } + return m_pTrack->getRating(); +} + +void QmlTrackProxy::setStars(int value) { + if (m_pTrack && value <= mixxx::TrackRecord::kMaxRating && + value >= mixxx::TrackRecord::kMinRating) { + m_pTrack->setRating(value); + } +} + +QUrl QmlTrackProxy::getCoverArtUrl() const { + if (m_pTrack == nullptr) { + return QUrl(); + } + + const CoverInfo coverInfo = m_pTrack->getCoverInfoWithLocation(); + return AsyncImageProvider::trackLocationToCoverArtUrl(coverInfo.trackLocation); +} + +QUrl QmlTrackProxy::getTrackLocationUrl() const { + if (m_pTrack == nullptr) { + return QUrl(); + } + + return QUrl::fromLocalFile(m_pTrack->getLocation()); +} + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmltrackproxy.h b/src/qml/qmltrackproxy.h new file mode 100644 index 000000000000..8b0e292d355c --- /dev/null +++ b/src/qml/qmltrackproxy.h @@ -0,0 +1,148 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +#include "mixer/basetrackplayer.h" +#include "qml/qmlbeatsmodel.h" +#include "qml/qmlcuesmodel.h" +#include "qml/qmlstemsmodel.h" +#include "track/track_decl.h" +#include "util/parented_ptr.h" + +namespace mixxx { +namespace qml { + +class QmlTrackProxy : public QObject { + Q_OBJECT + + Q_PROPERTY(QString artist READ getArtist WRITE setArtist NOTIFY artistChanged) + Q_PROPERTY(QString title READ getTitle WRITE setTitle NOTIFY titleChanged) + Q_PROPERTY(QString album READ getAlbum WRITE setAlbum NOTIFY albumChanged) + Q_PROPERTY(QString albumArtist READ getAlbumArtist WRITE setAlbumArtist + NOTIFY albumArtistChanged) + Q_PROPERTY(QString genre READ getGenre STORED false NOTIFY genreChanged) + Q_PROPERTY(QString composer READ getComposer WRITE setComposer NOTIFY composerChanged) + Q_PROPERTY(QString grouping READ getGrouping WRITE setGrouping NOTIFY groupingChanged) + Q_PROPERTY(int stars READ getStars WRITE setStars NOTIFY starsChanged) + Q_PROPERTY(QString year READ getYear WRITE setYear NOTIFY yearChanged) + Q_PROPERTY(QString trackNumber READ getTrackNumber WRITE setTrackNumber + NOTIFY trackNumberChanged) + Q_PROPERTY(QString trackTotal READ getTrackTotal WRITE setTrackTotal NOTIFY trackTotalChanged) + Q_PROPERTY(QString comment READ getComment WRITE setComment NOTIFY commentChanged) + Q_PROPERTY(QString keyText READ getKeyText WRITE setKeyText NOTIFY keyTextChanged) + Q_PROPERTY(QColor color READ getColor WRITE setColor NOTIFY colorChanged) + Q_PROPERTY(double duration READ getDuration NOTIFY durationChanged) + Q_PROPERTY(int sampleRate READ getSampleRate NOTIFY sampleRateChanged) + Q_PROPERTY(QUrl coverArtUrl READ getCoverArtUrl NOTIFY coverArtUrlChanged) + Q_PROPERTY(QUrl trackLocationUrl READ getTrackLocationUrl NOTIFY trackLocationUrlChanged) + + Q_PROPERTY(mixxx::qml::QmlBeatsModel* beatsModel READ getBeatsModel CONSTANT); + Q_PROPERTY(mixxx::qml::QmlCuesModel* hotcuesModel READ getCuesModel CONSTANT); +#ifdef __STEM__ + Q_PROPERTY(mixxx::qml::QmlStemsModel* stemsModel READ getStemsModel CONSTANT); +#endif + + QML_NAMED_ELEMENT(Track) + QML_UNCREATABLE("Only accessible via Mixxx.PlayerManager and Mixxx.Library") + public: + explicit QmlTrackProxy(TrackPointer track, QObject* parent = nullptr); + + QString getTrack() const; + QString getTitle() const; + QString getArtist() const; + QString getAlbum() const; + QString getAlbumArtist() const; + QString getGenre() const; + QString getComposer() const; + QString getGrouping() const; + QString getYear() const; + int getStars() const; + QString getTrackNumber() const; + QString getTrackTotal() const; + QString getComment() const; + QString getKeyText() const; + QColor getColor() const; + double getDuration() const; + int getSampleRate() const; + QUrl getCoverArtUrl() const; + QUrl getTrackLocationUrl() const; + + QmlBeatsModel* getBeatsModel() const { + return m_pBeatsModel.get(); + } + + QmlCuesModel* getCuesModel() const { + return m_pHotcuesModel.get(); + } + +#ifdef __STEM__ + QmlStemsModel* getStemsModel() const { + return m_pStemsModel.get(); + } +#endif + + TrackPointer internal() const { + return m_pTrack; + } + + public slots: + void slotBeatsChanged(); + void slotHotcuesChanged(); +#ifdef __STEM__ + void slotStemsChanged(); +#endif + + void setArtist(const QString& artist); + void setTitle(const QString& title); + void setAlbum(const QString& album); + void setAlbumArtist(const QString& albumArtist); + void setComposer(const QString& composer); + void setGrouping(const QString& grouping); + void setStars(int stars); + void setYear(const QString& year); + void setTrackNumber(const QString& trackNumber); + void setTrackTotal(const QString& trackTotal); + void setComment(const QString& comment); + void setKeyText(const QString& keyText); + void setColor(const QColor& color); + + signals: + void albumChanged(); + void titleChanged(); + void artistChanged(); + void albumArtistChanged(); + void genreChanged(); + void composerChanged(); + void groupingChanged(); + void starsChanged(); + void yearChanged(); + void trackNumberChanged(); + void trackTotalChanged(); + void commentChanged(); + void keyTextChanged(); + void colorChanged(); + void durationChanged(); + void sampleRateChanged(); + void coverArtUrlChanged(); + void trackLocationUrlChanged(); + void cuesChanged(); +#ifdef __STEM__ + void stemsChanged(); +#endif + + private: + TrackPointer m_pTrack; + parented_ptr m_pBeatsModel; + parented_ptr m_pHotcuesModel; +#ifdef __STEM__ + parented_ptr m_pStemsModel; +#endif +}; + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmlwaveformdisplay.cpp b/src/qml/qmlwaveformdisplay.cpp index 06d78d31bdfc..da57b9e492aa 100644 --- a/src/qml/qmlwaveformdisplay.cpp +++ b/src/qml/qmlwaveformdisplay.cpp @@ -1,11 +1,10 @@ #include "qml/qmlwaveformdisplay.h" -#include - #include #include #include #include +#include #include #include #include @@ -24,7 +23,7 @@ using namespace allshader; namespace { constexpr int kDefaultSyncInternalMs = 100; -} +} // namespace namespace mixxx { namespace qml { @@ -87,6 +86,7 @@ void QmlWaveformDisplay::geometryChange(const QRectF& newGeometry, const QRectF& QSGNode* QmlWaveformDisplay::updatePaintNode(QSGNode* node, UpdatePaintNodeData*) { if (m_dirtyFlag.testFlag(DirtyFlag::Window)) { delete node; + node = nullptr; m_dirtyFlag.setFlag(DirtyFlag::Window, false); } @@ -122,9 +122,11 @@ QSGNode* QmlWaveformDisplay::updatePaintNode(QSGNode* node, UpdatePaintNodeData* qDebug() << "Ignoring the unsupported" << pQmlRenderer << "renderer"; } auto renderer = pQmlRenderer->create(this); +#ifndef __STEM__ VERIFY_OR_DEBUG_ASSERT(renderer.renderer) { continue; } +#endif addRenderer(renderer.renderer); pTopNode->appendChildNode(std::move(renderer.node)); } @@ -245,7 +247,55 @@ void QmlWaveformDisplay::slotWaveformUpdated() { } QQmlListProperty QmlWaveformDisplay::renderers() { - return {this, &m_waveformRenderers}; + return {this, + nullptr, + &QmlWaveformDisplay::renderers_append, + &QmlWaveformDisplay::renderers_count, + &QmlWaveformDisplay::renderers_at, + &QmlWaveformDisplay::renderers_clear}; +} + +// Static +void QmlWaveformDisplay::renderers_append( + QQmlListProperty* pList, + QmlWaveformRendererFactory* value) { + QmlWaveformDisplay* pWaveform = static_cast(pList->object); + VERIFY_OR_DEBUG_ASSERT(pWaveform) { + return; + } + pWaveform->m_dirtyFlag.setFlag(DirtyFlag::Window, true); + pWaveform->m_waveformRenderers.append(value); +} + +// Static +qsizetype QmlWaveformDisplay::renderers_count(QQmlListProperty* pList) { + QmlWaveformDisplay* pWaveform = static_cast(pList->object); + VERIFY_OR_DEBUG_ASSERT(pWaveform) { + return 0; + } + pWaveform->m_dirtyFlag.setFlag(DirtyFlag::Window, true); + return pWaveform->m_waveformRenderers.count(); +} + +// Static +QmlWaveformRendererFactory* QmlWaveformDisplay::renderers_at( + QQmlListProperty* pList, qsizetype index) { + VERIFY_OR_DEBUG_ASSERT(pList && pList->object) { + return nullptr; + } + QmlWaveformDisplay* pWaveform = static_cast(pList->object); + pWaveform->m_dirtyFlag.setFlag(DirtyFlag::Window, true); + return pWaveform->m_waveformRenderers.at(index); +} + +// Static +void QmlWaveformDisplay::renderers_clear(QQmlListProperty* pList) { + QmlWaveformDisplay* pWaveform = static_cast(pList->object); + VERIFY_OR_DEBUG_ASSERT(pWaveform) { + return; + } + pWaveform->m_dirtyFlag.setFlag(DirtyFlag::Window, true); + return pWaveform->m_waveformRenderers.clear(); } } // namespace qml diff --git a/src/qml/qmlwaveformdisplay.h b/src/qml/qmlwaveformdisplay.h index ae77f5c86f21..6c5088e6361e 100644 --- a/src/qml/qmlwaveformdisplay.h +++ b/src/qml/qmlwaveformdisplay.h @@ -75,6 +75,13 @@ class QmlWaveformDisplay : public QQuickItem, VSyncTimeProvider, public Waveform void componentComplete() override; QQmlListProperty renderers(); + static void renderers_append( + QQmlListProperty* property, + QmlWaveformRendererFactory* value); + static qsizetype renderers_count(QQmlListProperty* property); + static QmlWaveformRendererFactory* renderers_at( + QQmlListProperty* property, qsizetype index); + static void renderers_clear(QQmlListProperty* property); protected: QSGNode* updatePaintNode(QSGNode* old, QQuickItem::UpdatePaintNodeData*) override; diff --git a/src/qml/qmlwaveformoverview.cpp b/src/qml/qmlwaveformoverview.cpp index 54d2d1051ca2..133f627ed637 100644 --- a/src/qml/qmlwaveformoverview.cpp +++ b/src/qml/qmlwaveformoverview.cpp @@ -1,7 +1,9 @@ #include "qml/qmlwaveformoverview.h" -#include "mixer/basetrackplayer.h" #include "moc_qmlwaveformoverview.cpp" +#include "qmlplayerproxy.h" +#include "qmltrackproxy.h" +#include "track/track.h" namespace { constexpr double kDesiredChannelHeight = 255; @@ -12,7 +14,7 @@ namespace qml { QmlWaveformOverview::QmlWaveformOverview(QQuickItem* parent) : QQuickPaintedItem(parent), - m_pPlayer(nullptr), + m_pTrack(nullptr), m_channels(ChannelFlag::BothChannels), m_renderer(Renderer::RGB), m_colorHigh(0xFF0000), @@ -20,39 +22,28 @@ QmlWaveformOverview::QmlWaveformOverview(QQuickItem* parent) m_colorLow(0x0000FF) { } -QmlPlayerProxy* QmlWaveformOverview::getPlayer() const { - return m_pPlayer; +QmlTrackProxy* QmlWaveformOverview::getTrack() const { + return m_pTrack; } -void QmlWaveformOverview::setPlayer(QmlPlayerProxy* pPlayer) { - if (m_pPlayer == pPlayer) { +void QmlWaveformOverview::setTrack(QmlTrackProxy* pTrack) { + if (m_pTrack == pTrack) { return; } - if (m_pPlayer != nullptr) { - m_pPlayer->internalTrackPlayer()->disconnect(this); + if (m_pTrack != nullptr && m_pTrack->internal() != nullptr) { + m_pTrack->internal()->disconnect(this); } - m_pPlayer = pPlayer; + m_pTrack = pTrack; - if (m_pPlayer != nullptr) { - setCurrentTrack(m_pPlayer->internalTrackPlayer()->getLoadedTrack()); - connect(m_pPlayer->internalTrackPlayer(), - &BaseTrackPlayer::newTrackLoaded, - this, - &QmlWaveformOverview::slotTrackLoaded); - connect(m_pPlayer->internalTrackPlayer(), - &BaseTrackPlayer::loadingTrack, - this, - &QmlWaveformOverview::slotTrackLoading); - connect(m_pPlayer->internalTrackPlayer(), - &BaseTrackPlayer::playerEmpty, + if (m_pTrack != nullptr && pTrack->internal() != nullptr) { + connect(pTrack->internal().get(), + &Track::waveformSummaryUpdated, this, - &QmlWaveformOverview::slotTrackUnloaded); + &QmlWaveformOverview::slotWaveformUpdated); } - - emit playerChanged(); - update(); + slotWaveformUpdated(); } QmlWaveformOverview::Channels QmlWaveformOverview::getChannels() const { @@ -68,49 +59,15 @@ void QmlWaveformOverview::setChannels(QmlWaveformOverview::Channels channels) { emit channelsChanged(channels); } -void QmlWaveformOverview::slotTrackLoaded(TrackPointer pTrack) { - // TODO: Investigate if it's a bug that this debug assertion fails when - // passing tracks on the command line - // DEBUG_ASSERT(m_pCurrentTrack == pTrack); - setCurrentTrack(pTrack); -} - -void QmlWaveformOverview::slotTrackLoading(TrackPointer pNewTrack, TrackPointer pOldTrack) { - Q_UNUSED(pOldTrack); // only used in DEBUG_ASSERT - DEBUG_ASSERT(m_pCurrentTrack == pOldTrack); - setCurrentTrack(pNewTrack); -} - -void QmlWaveformOverview::slotTrackUnloaded() { - setCurrentTrack(nullptr); -} - -void QmlWaveformOverview::setCurrentTrack(TrackPointer pTrack) { - // TODO: Check if this is actually possible - if (m_pCurrentTrack == pTrack) { - return; - } - - if (m_pCurrentTrack != nullptr) { - disconnect(m_pCurrentTrack.get(), nullptr, this, nullptr); - } - - m_pCurrentTrack = pTrack; - if (pTrack != nullptr) { - connect(pTrack.get(), - &Track::waveformSummaryUpdated, - this, - &QmlWaveformOverview::slotWaveformUpdated); - } - slotWaveformUpdated(); -} - void QmlWaveformOverview::slotWaveformUpdated() { update(); } void QmlWaveformOverview::paint(QPainter* pPainter) { - TrackPointer pTrack = m_pCurrentTrack; + if (!m_pTrack) { + return; + } + TrackPointer pTrack = m_pTrack->internal(); if (!pTrack) { return; } diff --git a/src/qml/qmlwaveformoverview.h b/src/qml/qmlwaveformoverview.h index 4b51bb83747d..1b3ebe745095 100644 --- a/src/qml/qmlwaveformoverview.h +++ b/src/qml/qmlwaveformoverview.h @@ -6,18 +6,17 @@ #include #include -#include "qml/qmlplayerproxy.h" -#include "track/track.h" +#include "qmlplayerproxy.h" +#include "waveform/waveform.h" namespace mixxx { namespace qml { - class QmlWaveformOverview : public QQuickPaintedItem { Q_OBJECT Q_FLAGS(Channels) - Q_PROPERTY(mixxx::qml::QmlPlayerProxy* player READ getPlayer WRITE setPlayer - NOTIFY playerChanged REQUIRED) + Q_PROPERTY(mixxx::qml::QmlTrackProxy* track READ getTrack WRITE setTrack + NOTIFY trackChanged REQUIRED) Q_PROPERTY(Channels channels READ getChannels WRITE setChannels NOTIFY channelsChanged) Q_PROPERTY(Renderer renderer MEMBER m_renderer NOTIFY rendererChanged) Q_PROPERTY(QColor colorHigh MEMBER m_colorHigh NOTIFY colorHighChanged) @@ -44,19 +43,16 @@ class QmlWaveformOverview : public QQuickPaintedItem { void paint(QPainter* painter) override; - void setPlayer(QmlPlayerProxy* player); - QmlPlayerProxy* getPlayer() const; + void setTrack(QmlTrackProxy* track); + QmlTrackProxy* getTrack() const; void setChannels(Channels channels); Channels getChannels() const; private slots: - void slotTrackLoaded(TrackPointer pLoadedTrack); - void slotTrackLoading(TrackPointer pNewTrack, TrackPointer pOldTrack); - void slotTrackUnloaded(); void slotWaveformUpdated(); signals: - void playerChanged(); + void trackChanged(); void channelsChanged(mixxx::qml::QmlWaveformOverview::Channels channels); #if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) void rendererChanged(Renderer renderer); @@ -68,7 +64,6 @@ class QmlWaveformOverview : public QQuickPaintedItem { void colorLowChanged(const QColor& color); private: - void setCurrentTrack(TrackPointer pTrack); void drawFiltered(QPainter* pPainter, Channels channels, ConstWaveformPointer pWaveform, @@ -78,9 +73,7 @@ class QmlWaveformOverview : public QQuickPaintedItem { ConstWaveformPointer pWaveform, int completion) const; QColor getRgbPenColor(ConstWaveformPointer pWaveform, int completion) const; - - QPointer m_pPlayer; - TrackPointer m_pCurrentTrack; + QmlTrackProxy* m_pTrack; Channels m_channels; Renderer m_renderer; QColor m_colorHigh; diff --git a/src/qml/qmlwaveformrenderer.cpp b/src/qml/qmlwaveformrenderer.cpp index 2f98fe8b2926..487aadcd5faf 100644 --- a/src/qml/qmlwaveformrenderer.cpp +++ b/src/qml/qmlwaveformrenderer.cpp @@ -6,8 +6,12 @@ #include "util/assert.h" #include "waveform/renderers/allshader/waveformrenderbeat.h" #include "waveform/renderers/allshader/waveformrendererendoftrack.h" +#include "waveform/renderers/allshader/waveformrendererfiltered.h" +#include "waveform/renderers/allshader/waveformrendererhsv.h" #include "waveform/renderers/allshader/waveformrendererpreroll.h" #include "waveform/renderers/allshader/waveformrendererrgb.h" +#include "waveform/renderers/allshader/waveformrenderersignalbase.h" +#include "waveform/renderers/allshader/waveformrenderersimple.h" #ifdef __STEM__ #include "waveform/renderers/allshader/waveformrendererstem.h" #endif @@ -18,7 +22,8 @@ namespace mixxx { namespace qml { QmlWaveformRendererMark::QmlWaveformRendererMark() - : m_defaultMark(nullptr), + : m_playMarkerPosition(0.5), + m_defaultMark(nullptr), m_untilMark(std::make_unique()) { } @@ -51,52 +56,142 @@ QmlWaveformRendererFactory::Renderer QmlWaveformRendererPreroll::create( return QmlWaveformRendererFactory::Renderer{pRenderer.get(), std::move(pRenderer)}; } +void QmlWaveformRendererSignal::setup( + allshader::WaveformRendererSignalBase* pRenderer) const { + pRenderer->setAxesColor(m_axesColor); + pRenderer->setLowColor(m_lowColor); + pRenderer->setMidColor(m_midColor); + pRenderer->setHighColor(m_highColor); + connect(this, + &QmlWaveformRendererSignal::axesColorChanged, + pRenderer, + &allshader::WaveformRendererSignalBase::setAxesColor); + connect(this, + &QmlWaveformRendererSignal::lowColorChanged, + pRenderer, + &allshader::WaveformRendererSignalBase::setLowColor); + connect(this, + &QmlWaveformRendererSignal::midColorChanged, + pRenderer, + &allshader::WaveformRendererSignalBase::setMidColor); + connect(this, + &QmlWaveformRendererSignal::highColorChanged, + pRenderer, + &allshader::WaveformRendererSignalBase::setHighColor); + + pRenderer->setAllChannelVisualGain(m_gainAll); + pRenderer->setLowVisualGain(m_gainLow); + pRenderer->setMidVisualGain(m_gainMid); + pRenderer->setHighVisualGain(m_gainHigh); + connect(this, + &QmlWaveformRendererSignal::gainAllChanged, + pRenderer, + &allshader::WaveformRendererSignalBase::setAllChannelVisualGain); + connect(this, + &QmlWaveformRendererSignal::gainLowChanged, + pRenderer, + &allshader::WaveformRendererSignalBase::setLowVisualGain); + connect(this, + &QmlWaveformRendererSignal::gainMidChanged, + pRenderer, + &allshader::WaveformRendererSignalBase::setMidVisualGain); + connect(this, + &QmlWaveformRendererSignal::gainHighChanged, + pRenderer, + &allshader::WaveformRendererSignalBase::setHighVisualGain); + pRenderer->setIgnoreStem(m_ignoreStem); + connect(this, + &QmlWaveformRendererSignal::ignoreStemChanged, + pRenderer, + &allshader::WaveformRendererSignalBase::setIgnoreStem); +} + QmlWaveformRendererFactory::Renderer QmlWaveformRendererRGB::create( WaveformWidgetRenderer* waveformWidget) const { auto pRenderer = std::make_unique( waveformWidget, m_position, m_options); + setup(pRenderer.get()); + return QmlWaveformRendererFactory::Renderer{pRenderer.get(), std::move(pRenderer)}; +} + +QmlWaveformRendererFactory::Renderer QmlWaveformRendererFiltered::create( + WaveformWidgetRenderer* waveformWidget) const { + auto pRenderer = std::make_unique( + waveformWidget, m_ignoreStem, m_options); + + setup(pRenderer.get()); + return QmlWaveformRendererFactory::Renderer{pRenderer.get(), std::move(pRenderer)}; +} + +QmlWaveformRendererFactory::Renderer QmlWaveformRendererHSV::create( + WaveformWidgetRenderer* waveformWidget) const { + auto pRenderer = std::make_unique( + waveformWidget, m_options); + pRenderer->setAxesColor(m_axesColor); - pRenderer->setLowColor(m_lowColor); - pRenderer->setMidColor(m_midColor); - pRenderer->setHighColor(m_highColor); + pRenderer->setColor(m_color); + pRenderer->setIgnoreStem(m_ignoreStem); + pRenderer->setAllChannelVisualGain(m_gainAll); + pRenderer->setLowVisualGain(m_gainLow); + pRenderer->setMidVisualGain(m_gainMid); + pRenderer->setHighVisualGain(m_gainHigh); + connect(this, + &QmlWaveformRendererHSV::gainAllChanged, + pRenderer.get(), + &allshader::WaveformRendererSignalBase::setAllChannelVisualGain); + connect(this, + &QmlWaveformRendererHSV::gainLowChanged, + pRenderer.get(), + &allshader::WaveformRendererSignalBase::setLowVisualGain); + connect(this, + &QmlWaveformRendererHSV::gainMidChanged, + pRenderer.get(), + &allshader::WaveformRendererSignalBase::setMidVisualGain); connect(this, - &QmlWaveformRendererRGB::axesColorChanged, + &QmlWaveformRendererHSV::gainHighChanged, pRenderer.get(), - &allshader::WaveformRendererRGB::setAxesColor); + &allshader::WaveformRendererSignalBase::setHighVisualGain); connect(this, - &QmlWaveformRendererRGB::lowColorChanged, + &QmlWaveformRendererHSV::axesColorChanged, pRenderer.get(), - &allshader::WaveformRendererRGB::setLowColor); + &allshader::WaveformRendererSignalBase::setAxesColor); connect(this, - &QmlWaveformRendererRGB::midColorChanged, + &QmlWaveformRendererHSV::colorChanged, pRenderer.get(), - &allshader::WaveformRendererRGB::setMidColor); + &allshader::WaveformRendererSignalBase::setColor); connect(this, - &QmlWaveformRendererRGB::highColorChanged, + &QmlWaveformRendererHSV::ignoreStemChanged, pRenderer.get(), - &allshader::WaveformRendererRGB::setHighColor); + &allshader::WaveformRendererSignalBase::setIgnoreStem); + return QmlWaveformRendererFactory::Renderer{pRenderer.get(), std::move(pRenderer)}; +} - pRenderer->setAllChannelVisualGain(m_gainAll); - pRenderer->setLowVisualGain(m_gainLow); - pRenderer->setMidVisualGain(m_gainMid); - pRenderer->setHighVisualGain(m_gainHigh); +QmlWaveformRendererFactory::Renderer QmlWaveformRendererSimple::create( + WaveformWidgetRenderer* waveformWidget) const { + auto pRenderer = std::make_unique( + waveformWidget, m_options); + + pRenderer->setAxesColor(m_axesColor); + pRenderer->setColor(m_color); + pRenderer->setAllChannelVisualGain(m_gain); + pRenderer->setIgnoreStem(m_ignoreStem); connect(this, - &QmlWaveformRendererRGB::gainAllChanged, + &QmlWaveformRendererSimple::axesColorChanged, pRenderer.get(), - &allshader::WaveformRendererRGB::setAllChannelVisualGain); + &allshader::WaveformRendererSignalBase::setAxesColor); connect(this, - &QmlWaveformRendererRGB::gainLowChanged, + &QmlWaveformRendererSimple::colorChanged, pRenderer.get(), - &allshader::WaveformRendererRGB::setLowVisualGain); + &allshader::WaveformRendererSignalBase::setColor); connect(this, - &QmlWaveformRendererRGB::gainMidChanged, + &QmlWaveformRendererSimple::gainChanged, pRenderer.get(), - &allshader::WaveformRendererRGB::setMidVisualGain); + &allshader::WaveformRendererSignalBase::setAllChannelVisualGain); connect(this, - &QmlWaveformRendererRGB::gainHighChanged, + &QmlWaveformRendererSimple::ignoreStemChanged, pRenderer.get(), - &allshader::WaveformRendererRGB::setHighVisualGain); + &allshader::WaveformRendererSignalBase::setIgnoreStem); return QmlWaveformRendererFactory::Renderer{pRenderer.get(), std::move(pRenderer)}; } @@ -104,11 +199,15 @@ QmlWaveformRendererFactory::Renderer QmlWaveformRendererBeat::create( WaveformWidgetRenderer* waveformWidget) const { auto pRenderer = std::make_unique( waveformWidget, m_position); - pRenderer->setColor(m_color); + waveformWidget->setDisplayBeatGridAlpha(m_color.alphaF() * 100); + pRenderer->setColor(m_color.rgb()); connect(this, &QmlWaveformRendererBeat::colorChanged, pRenderer.get(), - &allshader::WaveformRenderBeat::setColor); + [waveformWidget, &pRenderer](const QColor& color) { + waveformWidget->setDisplayBeatGridAlpha(color.alphaF() * 100); + pRenderer->setColor(color.rgb()); + }); return QmlWaveformRendererFactory::Renderer{pRenderer.get(), std::move(pRenderer)}; } @@ -164,6 +263,7 @@ QmlWaveformRendererFactory::Renderer QmlWaveformRendererMark::create( pRenderer->setPlayMarkerForegroundColor(m_playMarkerColor); pRenderer->setPlayMarkerBackgroundColor(m_playMarkerBackground); + waveformWidget->setPlayMarkerPosition(m_playMarkerPosition); pRenderer->setUntilMarkShowBeats(m_untilMark->showTime()); pRenderer->setUntilMarkShowTime(m_untilMark->showBeats()); @@ -196,6 +296,12 @@ QmlWaveformRendererFactory::Renderer QmlWaveformRendererMark::create( &QmlWaveformRendererMark::playMarkerBackgroundChanged, pRenderer.get(), &allshader::WaveformRenderMark::setPlayMarkerBackgroundColor); + connect(this, + &QmlWaveformRendererMark::playMarkerPositionChanged, + pRenderer.get(), + [waveformWidget](double value) { + waveformWidget->setPlayMarkerPosition(value); + }); // The initialisation is closely inspired from WaveformMarkSet::setup int priority = 0; diff --git a/src/qml/qmlwaveformrenderer.h b/src/qml/qmlwaveformrenderer.h index 361691ea48a3..3c1d10aae693 100644 --- a/src/qml/qmlwaveformrenderer.h +++ b/src/qml/qmlwaveformrenderer.h @@ -19,8 +19,12 @@ class WaveformRenderBeat; namespace mixxx { namespace qml { +using WaveformRendererPositionSource = ::WaveformRendererAbstract::PositionSource; + class QmlWaveformRendererFactory : public QObject { Q_OBJECT + Q_PROPERTY(WaveformRendererPositionSource position MEMBER + m_position NOTIFY positionChanged) QML_ANONYMOUS public: struct Renderer { @@ -33,6 +37,16 @@ class QmlWaveformRendererFactory : public QObject { } virtual Renderer create(WaveformWidgetRenderer* waveformWidget) const = 0; + + signals: +#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) + void positionChanged(WaveformRendererPositionSource); +#else + void positionChanged(mixxx::qml::WaveformRendererPositionSource); +#endif + + protected: + WaveformRendererPositionSource m_position{::WaveformRendererAbstract::Play}; }; class QmlWaveformRendererEndOfTrack @@ -71,9 +85,11 @@ class QmlWaveformRendererPreroll ::WaveformRendererAbstract::PositionSource m_position{::WaveformRendererAbstract::Play}; }; -class QmlWaveformRendererRGB +typedef WaveformRendererSignalBase::Options WaveformRendererSignalBaseOptions; +class QmlWaveformRendererSignal : public QmlWaveformRendererFactory { Q_OBJECT + Q_PROPERTY(bool ignoreStem MEMBER m_ignoreStem NOTIFY ignoreStemChanged) Q_PROPERTY(QColor axesColor MEMBER m_axesColor NOTIFY axesColorChanged REQUIRED) Q_PROPERTY(QColor lowColor MEMBER m_lowColor NOTIFY lowColorChanged REQUIRED) Q_PROPERTY(QColor midColor MEMBER m_midColor NOTIFY midColorChanged REQUIRED) @@ -82,10 +98,15 @@ class QmlWaveformRendererRGB Q_PROPERTY(double gainLow MEMBER m_gainLow NOTIFY gainLowChanged REQUIRED) Q_PROPERTY(double gainMid MEMBER m_gainMid NOTIFY gainMidChanged REQUIRED) Q_PROPERTY(double gainHigh MEMBER m_gainHigh NOTIFY gainHighChanged REQUIRED) - QML_NAMED_ELEMENT(WaveformRendererRGB) + Q_PROPERTY(WaveformRendererSignalBaseOptions options MEMBER + m_options NOTIFY optionsChanged) + QML_ANONYMOUS public: - Renderer create(WaveformWidgetRenderer* waveformWidget) const override; + Q_ENUM(WaveformRendererSignalBaseOptions) + + protected: + void setup(allshader::WaveformRendererSignalBase* renderer) const; signals: void axesColorChanged(const QColor&); @@ -96,8 +117,14 @@ class QmlWaveformRendererRGB void gainLowChanged(double); void gainMidChanged(double); void gainHighChanged(double); + void ignoreStemChanged(bool); +#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) + void optionsChanged(WaveformRendererSignalBaseOptions); +#else + void optionsChanged(mixxx::qml::WaveformRendererSignalBaseOptions); +#endif - private: + protected: QColor m_axesColor; QColor m_lowColor; QColor m_midColor; @@ -108,9 +135,111 @@ class QmlWaveformRendererRGB double m_gainMid; double m_gainHigh; + bool m_ignoreStem{false}; + ::WaveformRendererAbstract::PositionSource m_position{::WaveformRendererAbstract::Play}; - allshader::WaveformRendererSignalBase::Options m_options{ - allshader::WaveformRendererSignalBase::Option::None}; + WaveformRendererSignalBaseOptions m_options{ + WaveformRendererSignalBase::Option::None}; +}; + +class QmlWaveformRendererRGB + : public QmlWaveformRendererSignal { + Q_OBJECT + QML_NAMED_ELEMENT(WaveformRendererRGB) + + public: + Renderer create(WaveformWidgetRenderer* waveformWidget) const override; +}; + +class QmlWaveformRendererFiltered + : public QmlWaveformRendererSignal { + Q_OBJECT + Q_PROPERTY(bool stacked MEMBER m_stacked FINAL) + + QML_NAMED_ELEMENT(WaveformRendererFiltered) + + public: + Renderer create(WaveformWidgetRenderer* waveformWidget) const override; + + private: + bool m_stacked{false}; +}; + +class QmlWaveformRendererHSV + : public QmlWaveformRendererFactory { + Q_OBJECT + Q_PROPERTY(bool ignoreStem MEMBER m_ignoreStem NOTIFY ignoreStemChanged) + Q_PROPERTY(QColor axesColor MEMBER m_axesColor NOTIFY axesColorChanged REQUIRED) + Q_PROPERTY(QColor color MEMBER m_color NOTIFY colorChanged REQUIRED) + Q_PROPERTY(double gainAll MEMBER m_gainAll NOTIFY gainAllChanged REQUIRED) + Q_PROPERTY(double gainLow MEMBER m_gainLow NOTIFY gainLowChanged REQUIRED) + Q_PROPERTY(double gainMid MEMBER m_gainMid NOTIFY gainMidChanged REQUIRED) + Q_PROPERTY(double gainHigh MEMBER m_gainHigh NOTIFY gainHighChanged REQUIRED) + Q_PROPERTY(WaveformRendererSignalBaseOptions options MEMBER + m_options NOTIFY optionsChanged) + QML_NAMED_ELEMENT(WaveformRendererHSV) + + public: + Renderer create(WaveformWidgetRenderer* waveformWidget) const override; + signals: + void axesColorChanged(const QColor&); + void colorChanged(const QColor&); + void ignoreStemChanged(bool); + void gainAllChanged(double); + void gainLowChanged(double); + void gainMidChanged(double); + void gainHighChanged(double); +#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) + void optionsChanged(WaveformRendererSignalBaseOptions); +#else + void optionsChanged(mixxx::qml::WaveformRendererSignalBaseOptions); +#endif + + private: + QColor m_axesColor; + QColor m_color; + + double m_gainAll; + double m_gainLow; + double m_gainMid; + double m_gainHigh; + + bool m_ignoreStem{false}; + WaveformRendererSignalBaseOptions m_options{ + WaveformRendererSignalBase::Option::None}; +}; + +class QmlWaveformRendererSimple + : public QmlWaveformRendererFactory { + Q_OBJECT + Q_PROPERTY(bool ignoreStem MEMBER m_ignoreStem NOTIFY ignoreStemChanged) + Q_PROPERTY(QColor axesColor MEMBER m_axesColor NOTIFY axesColorChanged REQUIRED) + Q_PROPERTY(QColor color MEMBER m_color NOTIFY colorChanged REQUIRED) + Q_PROPERTY(double gain MEMBER m_gain NOTIFY gainChanged REQUIRED) + Q_PROPERTY(WaveformRendererSignalBaseOptions options MEMBER + m_options NOTIFY optionsChanged) + QML_NAMED_ELEMENT(WaveformRendererSimple) + + public: + Renderer create(WaveformWidgetRenderer* waveformWidget) const override; + signals: + void axesColorChanged(const QColor&); + void colorChanged(const QColor&); + void ignoreStemChanged(bool); + void gainChanged(double); +#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) + void optionsChanged(WaveformRendererSignalBaseOptions); +#else + void optionsChanged(mixxx::qml::WaveformRendererSignalBaseOptions); +#endif + + private: + QColor m_axesColor; + QColor m_color; + double m_gain; + bool m_ignoreStem{false}; + WaveformRendererSignalBaseOptions m_options{ + WaveformRendererSignalBase::Option::None}; }; class QmlWaveformRendererBeat @@ -373,6 +502,8 @@ class QmlWaveformRendererMark Q_PROPERTY(QColor playMarkerColor MEMBER m_playMarkerColor NOTIFY playMarkerColorChanged) Q_PROPERTY(QColor playMarkerBackground MEMBER m_playMarkerBackground NOTIFY playMarkerBackgroundChanged) + Q_PROPERTY(double playMarkerPosition MEMBER m_playMarkerPosition NOTIFY + playMarkerPositionChanged) Q_PROPERTY(QmlWaveformMark* defaultMark MEMBER m_defaultMark NOTIFY defaultMarkChanged) Q_PROPERTY(QmlWaveformUntilMark* untilMark READ untilMark FINAL) Q_CLASSINFO("DefaultProperty", "marks") @@ -397,6 +528,7 @@ class QmlWaveformRendererMark signals: void playMarkerColorChanged(const QColor&); void playMarkerBackgroundChanged(const QColor&); + void playMarkerPositionChanged(double); #if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) void defaultMarkChanged(QmlWaveformMark*); #else @@ -406,6 +538,7 @@ class QmlWaveformRendererMark private: QColor m_playMarkerColor; QColor m_playMarkerBackground; + double m_playMarkerPosition; QList m_marks; QmlWaveformMark* m_defaultMark; std::unique_ptr m_untilMark; diff --git a/src/skin/legacy/legacyskinparser.cpp b/src/skin/legacy/legacyskinparser.cpp index 3ad1e1dc8340..2e0951b3961a 100644 --- a/src/skin/legacy/legacyskinparser.cpp +++ b/src/skin/legacy/legacyskinparser.cpp @@ -1538,6 +1538,19 @@ QWidget* LegacySkinParser::parseSearchBox(const QDomElement& node) { commonWidgetSetup(node, pLineEditSearch, false); pLineEditSearch->setup(node, *m_pContext); + // Translate shortcuts to native text + QString searchInCurrentViewShortcut = + localizeShortcutKeys(m_pKeyboard->getKeyboardConfig()->getValue( + ConfigKey("[KeyboardShortcuts]", + "LibraryMenu_SearchInCurrentView"), + "Ctrl+f")); + QString searchInAllTracksShortcut = + localizeShortcutKeys(m_pKeyboard->getKeyboardConfig()->getValue( + ConfigKey("[KeyboardShortcuts]", + "LibraryMenu_SearchInAllTracks"), + "Ctrl+Shift+F")); + pLineEditSearch->setupToolTip(searchInCurrentViewShortcut, searchInAllTracksShortcut); + m_pLibrary->bindSearchboxWidget(pLineEditSearch); return pLineEditSearch; @@ -2624,9 +2637,6 @@ void LegacySkinParser::addShortcutToToolTip(WBaseWidget* pWidget, QString tooltip; - // translate shortcut to native text - QString nativeShortcut = QKeySequence(shortcut, QKeySequence::PortableText).toString(QKeySequence::NativeText); - tooltip += "\n"; tooltip += tr("Shortcut"); if (!cmd.isEmpty()) { @@ -2634,10 +2644,16 @@ void LegacySkinParser::addShortcutToToolTip(WBaseWidget* pWidget, tooltip += cmd; } tooltip += ": "; - tooltip += nativeShortcut; + tooltip += localizeShortcutKeys(shortcut); pWidget->appendBaseTooltip(tooltip); } +QString LegacySkinParser::localizeShortcutKeys(const QString& shortcut) { + // Translate shortcut to native text + return QKeySequence(shortcut, QKeySequence::PortableText) + .toString(QKeySequence::NativeText); +} + QString LegacySkinParser::parseLaunchImageStyle(const QDomNode& skinDoc) { QString schemeLaunchImageStyle; // Check if the skins has color schemes diff --git a/src/skin/legacy/legacyskinparser.h b/src/skin/legacy/legacyskinparser.h index 059806d6a9c9..54d21aa38bd5 100644 --- a/src/skin/legacy/legacyskinparser.h +++ b/src/skin/legacy/legacyskinparser.h @@ -143,6 +143,7 @@ class LegacySkinParser : public QObject, public SkinParser { bool setupPosition=true); void setupConnections(const QDomNode& node, WBaseWidget* pWidget); void addShortcutToToolTip(WBaseWidget* pWidget, const QString& shortcut, const QString& cmd); + QString localizeShortcutKeys(const QString& shortcut); QString getLibraryStyle(const QDomNode& node); QString lookupNodeGroup(const QDomElement& node); diff --git a/src/test/controller_mapping_validation_test.cpp b/src/test/controller_mapping_validation_test.cpp index 3ceefbe10dfd..b0afc4462f75 100644 --- a/src/test/controller_mapping_validation_test.cpp +++ b/src/test/controller_mapping_validation_test.cpp @@ -7,7 +7,7 @@ #include "controllers/defs_controllers.h" #include "controllers/scripting/legacy/controllerscriptenginelegacy.h" -#ifdef MIXXX_USE_QML +#include "track/track.h" #include "effects/effectsmanager.h" #include "engine/channelhandle.h" #include "engine/enginemixer.h" @@ -15,10 +15,11 @@ #include "library/library.h" #include "mixer/playerinfo.h" #include "mixer/playermanager.h" +#ifdef MIXXX_USE_QML #include "qml/qmlplayermanagerproxy.h" -#include "soundio/soundmanager.h" #endif #include "moc_controller_mapping_validation_test.cpp" +#include "soundio/soundmanager.h" FakeMidiControllerJSProxy::FakeMidiControllerJSProxy() : ControllerJSProxy(nullptr) { @@ -121,18 +122,10 @@ bool FakeController::isMappable() const { return false; } -#ifdef MIXXX_USE_QML -void deleteTrack(Track* pTrack) { - // Delete track objects directly in unit tests with - // no main event loop - delete pTrack; -}; -#endif - void LegacyControllerMappingValidationTest::SetUp() { m_mappingPath = getTestDir().filePath(QStringLiteral("../../res/controllers/")); m_pEnumerator.reset(new MappingInfoEnumerator(QList{m_mappingPath.absolutePath()})); -#ifdef MIXXX_USE_QML + // This setup mirrors coreservices -- it would be nice if we could use coreservices instead // but it does a lot of local disk / settings setup. auto pChannelHandleFactory = std::make_shared(); @@ -164,7 +157,7 @@ void LegacyControllerMappingValidationTest::SetUp() { nullptr, m_pConfig, dbConnectionPooler(), - deleteTrack); + [](Track* pTrack) { delete pTrack; }); m_pRecordingManager = std::make_shared(m_pConfig, m_pEngine.get()); CoverArtCache::createInstance(); @@ -177,16 +170,21 @@ void LegacyControllerMappingValidationTest::SetUp() { m_pRecordingManager.get()); m_pPlayerManager->bindToLibrary(m_pLibrary.get()); +#ifdef MIXXX_USE_QML mixxx::qml::QmlPlayerManagerProxy::registerPlayerManager(m_pPlayerManager); +#endif + ControllerScriptEngineBase::registerPlayerManager(m_pPlayerManager); ControllerScriptEngineBase::registerTrackCollectionManager(m_pTrackCollectionManager); } void LegacyControllerMappingValidationTest::TearDown() { PlayerInfo::destroy(); CoverArtCache::destroy(); +#ifdef MIXXX_USE_QML mixxx::qml::QmlPlayerManagerProxy::registerPlayerManager(nullptr); - ControllerScriptEngineBase::registerTrackCollectionManager(nullptr); #endif + ControllerScriptEngineBase::registerPlayerManager(nullptr); + ControllerScriptEngineBase::registerTrackCollectionManager(nullptr); } bool LegacyControllerMappingValidationTest::testLoadMapping(const MappingInfo& mapping) { diff --git a/src/test/controller_mapping_validation_test.h b/src/test/controller_mapping_validation_test.h index ee5671616799..ab1c0a1c9dd2 100644 --- a/src/test/controller_mapping_validation_test.h +++ b/src/test/controller_mapping_validation_test.h @@ -1,7 +1,6 @@ #pragma once #include - #include "control/controlindicatortimer.h" #include "controllers/controller.h" #include "controllers/controllermappinginfoenumerator.h" @@ -222,7 +221,6 @@ class LegacyControllerMappingValidationTest : public MixxxDbTest, SoundSourcePro protected: void SetUp() override; -#ifdef MIXXX_USE_QML void TearDown() override; TrackPointer getOrAddTrackByLocation( @@ -239,7 +237,6 @@ class LegacyControllerMappingValidationTest : public MixxxDbTest, SoundSourcePro std::shared_ptr m_pTrackCollectionManager; std::shared_ptr m_pRecordingManager; std::shared_ptr m_pLibrary; -#endif bool testLoadMapping(const MappingInfo& mapping); diff --git a/src/test/controllerscriptenginelegacy_test.cpp b/src/test/controllerscriptenginelegacy_test.cpp index ac8f2810a5c6..472c3d217870 100644 --- a/src/test/controllerscriptenginelegacy_test.cpp +++ b/src/test/controllerscriptenginelegacy_test.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -27,7 +28,26 @@ #ifdef MIXXX_USE_QML #include "qml/qmlmixxxcontrollerscreen.h" #endif +#include "control/controlindicatortimer.h" +#include "database/mixxxdb.h" +#include "effects/effectsmanager.h" +#include "engine/channelhandle.h" +#include "engine/channels/enginedeck.h" +#include "engine/enginebuffer.h" +#include "engine/enginemixer.h" +#include "library/coverartcache.h" +#include "library/library.h" +#include "library/trackcollectionmanager.h" +#include "mixer/deck.h" +#include "mixer/playerinfo.h" +#include "mixer/playermanager.h" +#include "recording/recordingmanager.h" +#include "soundio/soundmanager.h" +#include "sources/soundsourceproxy.h" +#include "test/mixxxdbtest.h" #include "test/mixxxtest.h" +#include "test/soundsourceproviderregistration.h" +#include "track/track.h" #include "util/color/colorpalette.h" #include "util/time.h" @@ -38,7 +58,9 @@ typedef std::unique_ptr ScopedTemporaryFile; const RuntimeLoggingCategory logger(QString("test").toLocal8Bit()); -class ControllerScriptEngineLegacyTest : public ControllerScriptEngineLegacy, public MixxxTest { +class ControllerScriptEngineLegacyTest : public ControllerScriptEngineLegacy, + public MixxxDbTest, + SoundSourceProviderRegistration { protected: ControllerScriptEngineLegacyTest() : ControllerScriptEngineLegacy(nullptr, logger) { @@ -57,6 +79,72 @@ class ControllerScriptEngineLegacyTest : public ControllerScriptEngineLegacy, pu mixxx::Time::addTestTime(10ms); QThread::currentThread()->setObjectName("Main"); initialize(); + + // This setup mirrors coreservices -- it would be nice if we could use coreservices instead + // but it does a lot of local disk / settings setup. + auto pChannelHandleFactory = std::make_shared(); + m_pEffectsManager = std::make_shared(config(), pChannelHandleFactory); + m_pEngine = std::make_shared( + config(), + "[Master]", + m_pEffectsManager.get(), + pChannelHandleFactory, + true); + m_pSoundManager = std::make_shared(config(), m_pEngine.get()); + m_pControlIndicatorTimer = std::make_shared(nullptr); + m_pEngine->registerNonEngineChannelSoundIO(gsl::make_not_null(m_pSoundManager.get())); + + CoverArtCache::createInstance(); + + m_pPlayerManager = std::make_shared(config(), + m_pSoundManager.get(), + m_pEffectsManager.get(), + m_pEngine.get()); + + m_pPlayerManager->addConfiguredDecks(); + m_pPlayerManager->addSampler(); + PlayerInfo::create(); + m_pEffectsManager->setup(); + + const auto dbConnection = mixxx::DbConnectionPooled(dbConnectionPooler()); + if (!MixxxDb::initDatabaseSchema(dbConnection)) { + exit(1); + } + + m_pTrackCollectionManager = std::make_shared( + nullptr, + config(), + dbConnectionPooler(), + [](Track* pTrack) { delete pTrack; }); + + m_pRecordingManager = std::make_shared(config(), m_pEngine.get()); + m_pLibrary = std::make_shared( + nullptr, + config(), + dbConnectionPooler(), + m_pTrackCollectionManager.get(), + m_pPlayerManager.get(), + m_pRecordingManager.get()); + + m_pPlayerManager->bindToLibrary(m_pLibrary.get()); + ControllerScriptEngineBase::registerPlayerManager(m_pPlayerManager); + ControllerScriptEngineBase::registerTrackCollectionManager(m_pTrackCollectionManager); + } + + void loadTrackSync(const QString& trackLocation) { + TrackPointer pTrack1 = m_pTrackCollectionManager->getOrAddTrack( + TrackRef::fromFilePath(getTestDir().filePath(trackLocation))); + auto* deck = m_pPlayerManager->getDeck(1); + deck->slotLoadTrack(pTrack1, +#ifdef __STEM__ + mixxx::StemChannelSelection(), +#endif + false); + m_pEngine->process(1024); + while (!deck->getEngineDeck()->getEngineBuffer()->isTrackLoaded()) { + QTest::qSleep(100); + } + processEvents(); } void TearDown() override { @@ -64,6 +152,22 @@ class ControllerScriptEngineLegacyTest : public ControllerScriptEngineLegacy, pu #ifdef MIXXX_USE_QML m_rootItems.clear(); #endif + CoverArtCache::destroy(); + ControllerScriptEngineBase::registerPlayerManager(nullptr); + ControllerScriptEngineBase::registerTrackCollectionManager(nullptr); + } + + ~ControllerScriptEngineLegacyTest() { + // Reset in the correct order to avoid singleton destruction issues + m_pSoundManager.reset(); + m_pPlayerManager.reset(); + PlayerInfo::destroy(); + m_pLibrary.reset(); + m_pRecordingManager.reset(); + m_pEngine.reset(); + m_pEffectsManager.reset(); + m_pTrackCollectionManager.reset(); + m_pControlIndicatorTimer.reset(); } bool evaluateScriptFile(const QFileInfo& scriptFile) { @@ -107,6 +211,15 @@ class ControllerScriptEngineLegacyTest : public ControllerScriptEngineLegacy, pu handleScreenFrame(screeninfo, frame, timestamp); } #endif + + std::shared_ptr m_pEffectsManager; + std::shared_ptr m_pEngine; + std::shared_ptr m_pSoundManager; + std::shared_ptr m_pControlIndicatorTimer; + std::shared_ptr m_pPlayerManager; + std::shared_ptr m_pRecordingManager; + std::shared_ptr m_pLibrary; + std::shared_ptr m_pTrackCollectionManager; }; class ControllerScriptEngineLegacyTimerTest : public ControllerScriptEngineLegacyTest { @@ -834,11 +947,60 @@ TEST_F(ControllerScriptEngineLegacyTest, convertCharsetAllCharset) { } } +TEST_F(ControllerScriptEngineLegacyTest, JavascriptPlayerProxy) { + QMap expectedValues = { + std::pair("artist", "Test Artist"), + std::pair("title", "Test title"), + std::pair("album", "Test Album"), + std::pair("albumArtist", "Test Album Artist"), + std::pair("genre", "Test genre"), + std::pair("composer", "Test Composer"), + std::pair("grouping", ""), + std::pair("year", "2011"), + std::pair("trackNumber", "07"), + std::pair("trackTotal", "60")}; + + m_pJSEngine->globalObject().setProperty( + "testedValues", m_pJSEngine->toScriptValue(expectedValues.keys())); + + const auto* code = + "var result = {};" + "var player = engine.getPlayer('[Channel1]');" + "for(const name of testedValues) {" + " player[`${name}Changed`].connect(newValue => {" + " result[name] = newValue;" + " });" + "}"; + + EXPECT_TRUE(evaluateAndAssert(code)) << "Evaluation error in test code"; + loadTrackSync("id3-test-data/all.mp3"); +#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0) + for (auto [property, expected] : expectedValues.asKeyValueRange()) { +#else + for (auto it = expectedValues.constBegin(); it != expectedValues.constEnd(); ++it) { + const QString& property = it.key(); + const QString& expected = it.value(); +#endif + auto const playerActual = evaluate("player." + property).toString(); + auto const slotActual = evaluate("result." + property).toString(); + EXPECT_QSTRING_EQ(expected, playerActual) + << QString("engine.getPlayer(...).%1 doesn't corresponds to " + "its expected value (expected: %2, actual: %3)") + .arg(property, expected, playerActual) + .toStdString(); + EXPECT_QSTRING_EQ(expected, slotActual) << QString( + "engine.getPlayer(...).%1Changed slot didn't produce the " + "expected value (expected: %2, actual: %3)") + .arg(property, expected, playerActual) + .toStdString(); + } +} + #ifdef MIXXX_USE_QML class MockScreenRender : public ControllerRenderingEngine { public: MockScreenRender(const LegacyControllerMapping::ScreenInfo& info) - : ControllerRenderingEngine(info, new ControllerEngineThreadControl){}; + : ControllerRenderingEngine(info, new ControllerEngineThreadControl) {}; MOCK_METHOD(void, requestSendingFrameData, (Controller * controller, const QByteArray& frame), diff --git a/src/test/id3-test-data/all.mp3 b/src/test/id3-test-data/all.mp3 new file mode 100644 index 000000000000..2a383fc1fe68 Binary files /dev/null and b/src/test/id3-test-data/all.mp3 differ diff --git a/src/test/waveform_upgrade_test.cpp b/src/test/waveform_upgrade_test.cpp index 44387755bca8..21386e1ec3f1 100644 --- a/src/test/waveform_upgrade_test.cpp +++ b/src/test/waveform_upgrade_test.cpp @@ -23,7 +23,7 @@ TEST_F(UpgradeTest, useCorrectWaveformType) { int oldTypeId; WaveformWidgetType::Type expectedType; WaveformWidgetBackend expectedBackend; - allshader::WaveformRendererSignalBase::Options expectedOptions; + WaveformRendererSignalBase::Options expectedOptions; }; QList testCases = { @@ -31,132 +31,132 @@ TEST_F(UpgradeTest, useCorrectWaveformType) { 0, WaveformWidgetType::Empty, WaveformWidgetBackend::None, - allshader::WaveformRendererSignalBase::Option::None}, + WaveformRendererSignalBase::Option::None}, test_case{"SoftwareWaveform", 2, // Filtered WaveformWidgetType::Filtered, WaveformWidgetBackend::AllShader, - allshader::WaveformRendererSignalBase::Option::None}, + WaveformRendererSignalBase::Option::None}, test_case{"QtSimpleWaveform", 3, // Simple Qt WaveformWidgetType::Simple, WaveformWidgetBackend::AllShader, - allshader::WaveformRendererSignalBase::Option::None}, + WaveformRendererSignalBase::Option::None}, test_case{"QtWaveform", 4, // Filtered Qt WaveformWidgetType::Filtered, WaveformWidgetBackend::AllShader, - allshader::WaveformRendererSignalBase::Option::None}, + WaveformRendererSignalBase::Option::None}, test_case{"GLSimpleWaveform", 5, // Simple GL WaveformWidgetType::Simple, WaveformWidgetBackend::AllShader, - allshader::WaveformRendererSignalBase::Option::None}, + WaveformRendererSignalBase::Option::None}, test_case{"GLFilteredWaveform", 6, // Filtered GL WaveformWidgetType::Filtered, WaveformWidgetBackend::AllShader, - allshader::WaveformRendererSignalBase::Option::None}, + WaveformRendererSignalBase::Option::None}, test_case{"GLSLFilteredWaveform", 7, // Filtered GLSL WaveformWidgetType::Filtered, WaveformWidgetBackend::AllShader, - allshader::WaveformRendererSignalBase::Option::HighDetail}, + WaveformRendererSignalBase::Option::HighDetail}, test_case{"HSVWaveform", 8, // HSV WaveformWidgetType::HSV, WaveformWidgetBackend::AllShader, - allshader::WaveformRendererSignalBase::Option::None}, + WaveformRendererSignalBase::Option::None}, test_case{"GLVSyncTest", 9, // VSync GL WaveformWidgetType::VSyncTest, WaveformWidgetBackend::None, - allshader::WaveformRendererSignalBase::Option::None}, + WaveformRendererSignalBase::Option::None}, test_case{"RGBWaveform", 10, // RGB WaveformWidgetType::RGB, WaveformWidgetBackend::AllShader, - allshader::WaveformRendererSignalBase::Option::None}, + WaveformRendererSignalBase::Option::None}, test_case{"GLRGBWaveform", 11, // RGB GL WaveformWidgetType::RGB, WaveformWidgetBackend::AllShader, - allshader::WaveformRendererSignalBase::Option::None}, + WaveformRendererSignalBase::Option::None}, test_case{"GLSLRGBWaveform", 12, // RGB GLSL WaveformWidgetType::RGB, WaveformWidgetBackend::AllShader, - allshader::WaveformRendererSignalBase::Option::HighDetail}, + WaveformRendererSignalBase::Option::HighDetail}, test_case{"QtVSyncTest", 13, // VSync Qt WaveformWidgetType::VSyncTest, WaveformWidgetBackend::None, - allshader::WaveformRendererSignalBase::Option::None}, + WaveformRendererSignalBase::Option::None}, test_case{"QtHSVWaveform", 14, // HSV Qt WaveformWidgetType::HSV, WaveformWidgetBackend::AllShader, - allshader::WaveformRendererSignalBase::Option::None}, + WaveformRendererSignalBase::Option::None}, test_case{"QtRGBWaveform", 15, // RGB Qt WaveformWidgetType::RGB, WaveformWidgetBackend::AllShader, - allshader::WaveformRendererSignalBase::Option::None}, + WaveformRendererSignalBase::Option::None}, test_case{"GLSLRGBStackedWaveform", 16, // RGB Stacked WaveformWidgetType::Stacked, WaveformWidgetBackend::AllShader, - allshader::WaveformRendererSignalBase::Option::HighDetail}, + WaveformRendererSignalBase::Option::HighDetail}, test_case{"AllShaderRGBWaveform", 17, // RGB (all-shaders) WaveformWidgetType::RGB, WaveformWidgetBackend::AllShader, - allshader::WaveformRendererSignalBase::Option::None}, + WaveformRendererSignalBase::Option::None}, test_case{"AllShaderLRRGBWaveform", 18, // L/R RGB (all-shaders) WaveformWidgetType::RGB, WaveformWidgetBackend::AllShader, - allshader::WaveformRendererSignalBase::Option::SplitStereoSignal}, + WaveformRendererSignalBase::Option::SplitStereoSignal}, test_case{"AllShaderFilteredWaveform", 19, // Filtered (all-shaders) WaveformWidgetType::Filtered, WaveformWidgetBackend::AllShader, - allshader::WaveformRendererSignalBase::Option::None}, + WaveformRendererSignalBase::Option::None}, test_case{"AllShaderSimpleWaveform", 20, // Simple (all-shaders) WaveformWidgetType::Simple, WaveformWidgetBackend::AllShader, - allshader::WaveformRendererSignalBase::Option::None}, + WaveformRendererSignalBase::Option::None}, test_case{"AllShaderHSVWaveform", 21, // HSV (all-shaders) WaveformWidgetType::HSV, WaveformWidgetBackend::AllShader, - allshader::WaveformRendererSignalBase::Option::None}, + WaveformRendererSignalBase::Option::None}, test_case{"AllShaderTexturedFiltered", 22, // Filtered (textured) (all-shaders) WaveformWidgetType::Filtered, WaveformWidgetBackend::AllShader, - allshader::WaveformRendererSignalBase::Option::HighDetail}, + WaveformRendererSignalBase::Option::HighDetail}, test_case{"AllShaderTexturedRGB", 23, // RGB (textured) (all-shaders) WaveformWidgetType::RGB, WaveformWidgetBackend::AllShader, - allshader::WaveformRendererSignalBase::Option::HighDetail}, + WaveformRendererSignalBase::Option::HighDetail}, test_case{"AllShaderTexturedStacked", 24, // Stacked (textured) (all-shaders) WaveformWidgetType::Stacked, WaveformWidgetBackend::AllShader, - allshader::WaveformRendererSignalBase::Option::HighDetail}, + WaveformRendererSignalBase::Option::HighDetail}, test_case{"AllShaderRGBStackedWaveform", 26, // Stacked (all-shaders) WaveformWidgetType::Stacked, WaveformWidgetBackend::AllShader, - allshader::WaveformRendererSignalBase::Option::None}, + WaveformRendererSignalBase::Option::None}, test_case{"Count_WaveformwidgetType", 27, // Also used as invalid value WaveformWidgetType::RGB, WaveformWidgetBackend::AllShader, - allshader::WaveformRendererSignalBase::Option::None}}; + WaveformRendererSignalBase::Option::None}}; for (const auto& testCase : testCases) { int waveformType = testCase.oldTypeId; diff --git a/src/util/screensaver.cpp b/src/util/screensaver.cpp index 9eae4a1b4cd0..88f6e880ea56 100644 --- a/src/util/screensaver.cpp +++ b/src/util/screensaver.cpp @@ -36,7 +36,8 @@ With the help of the following source codes: # include #endif -#if defined(__LINUX__) || (defined(HAVE_XSCREENSAVER_SUSPEND) && HAVE_XSCREENSAVER_SUSPEND) +#if (defined(__LINUX__) && defined(__X11__)) || \ + (defined(HAVE_XSCREENSAVER_SUSPEND) && HAVE_XSCREENSAVER_SUSPEND) # define None XNone # define Window XWindow # include @@ -146,7 +147,7 @@ void ScreenSaverHelper::uninhibitInternal() s_enabled = false; } -#elif defined(Q_OS_LINUX) +#elif (defined(Q_OS_LINUX) && defined(__X11__)) const char *SCREENSAVERS[][4] = { // org.freedesktop.ScreenSaver is the standard. should work for gnome and kde too, // but I add their specific names too diff --git a/src/waveform/renderers/allshader/waveformrendererendoftrack.cpp b/src/waveform/renderers/allshader/waveformrendererendoftrack.cpp index 478c7629a41b..fa998de33bc5 100644 --- a/src/waveform/renderers/allshader/waveformrendererendoftrack.cpp +++ b/src/waveform/renderers/allshader/waveformrendererendoftrack.cpp @@ -44,6 +44,12 @@ void WaveformRendererEndOfTrack::draw(QPainter* painter, QPaintEvent* event) { bool WaveformRendererEndOfTrack::init() { m_timer.restart(); + if (m_waveformRenderer->getGroup().isEmpty()) { + m_pEndOfTrackControl.reset(); + m_pTimeRemainingControl.reset(); + return true; + } + m_pEndOfTrackControl.reset(new ControlProxy( m_waveformRenderer->getGroup(), "end_of_track")); m_pTimeRemainingControl.reset(new ControlProxy( @@ -70,7 +76,7 @@ void WaveformRendererEndOfTrack::preprocess() { } bool WaveformRendererEndOfTrack::preprocessInner() { - if (!m_pEndOfTrackControl->toBool()) { + if (!m_pEndOfTrackControl || !m_pEndOfTrackControl->toBool()) { return false; } diff --git a/src/waveform/renderers/allshader/waveformrendererfiltered.cpp b/src/waveform/renderers/allshader/waveformrendererfiltered.cpp index efab7bdd60ad..13c14ea5582c 100644 --- a/src/waveform/renderers/allshader/waveformrendererfiltered.cpp +++ b/src/waveform/renderers/allshader/waveformrendererfiltered.cpp @@ -13,8 +13,9 @@ namespace allshader { WaveformRendererFiltered::WaveformRendererFiltered( WaveformWidgetRenderer* waveformWidget, - bool bRgbStacked) - : WaveformRendererSignalBase(waveformWidget), + bool bRgbStacked, + ::WaveformRendererSignalBase::Options options) + : WaveformRendererSignalBase(waveformWidget, options), m_bRgbStacked(bRgbStacked) { initForRectangles(0); setUsePreprocess(true); @@ -56,7 +57,7 @@ bool WaveformRendererFiltered::preprocessInner() { #ifdef __STEM__ auto stemInfo = pTrack->getStemInfo(); // If this track is a stem track, skip the rendering - if (!stemInfo.isEmpty() && waveform->hasStem()) { + if (!stemInfo.isEmpty() && waveform->hasStem() && !m_ignoreStem) { return false; } #endif diff --git a/src/waveform/renderers/allshader/waveformrendererfiltered.h b/src/waveform/renderers/allshader/waveformrendererfiltered.h index e807d32a83ee..5317358b924b 100644 --- a/src/waveform/renderers/allshader/waveformrendererfiltered.h +++ b/src/waveform/renderers/allshader/waveformrendererfiltered.h @@ -13,7 +13,8 @@ class allshader::WaveformRendererFiltered final public rendergraph::GeometryNode { public: explicit WaveformRendererFiltered(WaveformWidgetRenderer* waveformWidget, - bool rgbStacked); + bool rgbStacked, + ::WaveformRendererSignalBase::Options options); // Pure virtual from WaveformRendererSignalBase, not used void onSetup(const QDomNode& node) override; diff --git a/src/waveform/renderers/allshader/waveformrendererhsv.cpp b/src/waveform/renderers/allshader/waveformrendererhsv.cpp index bbf367407477..db48a960790a 100644 --- a/src/waveform/renderers/allshader/waveformrendererhsv.cpp +++ b/src/waveform/renderers/allshader/waveformrendererhsv.cpp @@ -12,8 +12,9 @@ using namespace rendergraph; namespace allshader { -WaveformRendererHSV::WaveformRendererHSV(WaveformWidgetRenderer* waveformWidget) - : WaveformRendererSignalBase(waveformWidget) { +WaveformRendererHSV::WaveformRendererHSV(WaveformWidgetRenderer* waveformWidget, + ::WaveformRendererSignalBase::Options options) + : WaveformRendererSignalBase(waveformWidget, options) { initForRectangles(0); setUsePreprocess(true); } @@ -54,7 +55,7 @@ bool WaveformRendererHSV::preprocessInner() { #ifdef __STEM__ auto stemInfo = pTrack->getStemInfo(); // If this track is a stem track, skip the rendering - if (!stemInfo.isEmpty() && waveform->hasStem()) { + if (!stemInfo.isEmpty() && waveform->hasStem() && !m_ignoreStem) { return false; } #endif @@ -80,9 +81,7 @@ bool WaveformRendererHSV::preprocessInner() { getGains(&allGain, false, nullptr, nullptr, nullptr); // Get base color of waveform in the HSV format (s and v isn't use) - float h, s, v; - getHsvF(m_waveformRenderer->getWaveformSignalColors()->getLowColor(), &h, &s, &v); - + float h = m_signalColor_h; const float breadth = static_cast(m_waveformRenderer->getBreadth()); const float halfBreadth = breadth / 2.0f; diff --git a/src/waveform/renderers/allshader/waveformrendererhsv.h b/src/waveform/renderers/allshader/waveformrendererhsv.h index d308a269eaf6..678d0809b193 100644 --- a/src/waveform/renderers/allshader/waveformrendererhsv.h +++ b/src/waveform/renderers/allshader/waveformrendererhsv.h @@ -12,7 +12,8 @@ class allshader::WaveformRendererHSV final : public allshader::WaveformRendererSignalBase, public rendergraph::GeometryNode { public: - explicit WaveformRendererHSV(WaveformWidgetRenderer* waveformWidget); + explicit WaveformRendererHSV(WaveformWidgetRenderer* waveformWidget, + ::WaveformRendererSignalBase::Options options); // Pure virtual from WaveformRendererSignalBase, not used void onSetup(const QDomNode& node) override; diff --git a/src/waveform/renderers/allshader/waveformrendererrgb.cpp b/src/waveform/renderers/allshader/waveformrendererrgb.cpp index 88daccb60cfd..d1c97e575d43 100644 --- a/src/waveform/renderers/allshader/waveformrendererrgb.cpp +++ b/src/waveform/renderers/allshader/waveformrendererrgb.cpp @@ -20,30 +20,14 @@ inline float math_pow2(float x) { WaveformRendererRGB::WaveformRendererRGB(WaveformWidgetRenderer* waveformWidget, ::WaveformRendererAbstract::PositionSource type, - WaveformRendererSignalBase::Options options) - : WaveformRendererSignalBase(waveformWidget), + ::WaveformRendererSignalBase::Options options) + : WaveformRendererSignalBase(waveformWidget, options), m_isSlipRenderer(type == ::WaveformRendererAbstract::Slip), m_options(options) { initForRectangles(0); setUsePreprocess(true); } -void WaveformRendererRGB::setAxesColor(const QColor& axesColor) { - getRgbF(axesColor, &m_axesColor_r, &m_axesColor_g, &m_axesColor_b, &m_axesColor_a); -} - -void WaveformRendererRGB::setLowColor(const QColor& lowColor) { - getRgbF(lowColor, &m_rgbLowColor_r, &m_rgbLowColor_g, &m_rgbLowColor_b); -} - -void WaveformRendererRGB::setMidColor(const QColor& midColor) { - getRgbF(midColor, &m_rgbMidColor_r, &m_rgbMidColor_g, &m_rgbMidColor_b); -} - -void WaveformRendererRGB::setHighColor(const QColor& highColor) { - getRgbF(highColor, &m_rgbHighColor_r, &m_rgbHighColor_g, &m_rgbHighColor_b); -} - void WaveformRendererRGB::onSetup(const QDomNode&) { } @@ -83,7 +67,7 @@ bool WaveformRendererRGB::preprocessInner() { #ifdef __STEM__ auto stemInfo = pTrack->getStemInfo(); // If this track is a stem track, skip the rendering - if (!stemInfo.isEmpty() && waveform->hasStem()) { + if (!stemInfo.isEmpty() && waveform->hasStem() && !m_ignoreStem) { return false; } #endif @@ -115,7 +99,7 @@ bool WaveformRendererRGB::preprocessInner() { const float heightFactorAbs = allGain * halfBreadth / m_maxValue; const float heightFactor[2] = {-heightFactorAbs, heightFactorAbs}; - const bool splitLeftRight = m_options & WaveformRendererSignalBase::Option::SplitStereoSignal; + const bool splitLeftRight = m_options & ::WaveformRendererSignalBase::Option::SplitStereoSignal; const float low_r = static_cast(m_rgbLowColor_r); const float mid_r = static_cast(m_rgbMidColor_r); diff --git a/src/waveform/renderers/allshader/waveformrendererrgb.h b/src/waveform/renderers/allshader/waveformrendererrgb.h index 7451f65c1254..132c86b71916 100644 --- a/src/waveform/renderers/allshader/waveformrendererrgb.h +++ b/src/waveform/renderers/allshader/waveformrendererrgb.h @@ -15,7 +15,8 @@ class allshader::WaveformRendererRGB final explicit WaveformRendererRGB(WaveformWidgetRenderer* waveformWidget, ::WaveformRendererAbstract::PositionSource type = ::WaveformRendererAbstract::Play, - WaveformRendererSignalBase::Options options = WaveformRendererSignalBase::Option::None); + ::WaveformRendererSignalBase::Options options = + ::WaveformRendererSignalBase::Option::None); // Pure virtual from WaveformRendererSignalBase, not used void onSetup(const QDomNode& node) override; @@ -27,15 +28,9 @@ class allshader::WaveformRendererRGB final // Virtuals for rendergraph::Node void preprocess() override; - public slots: - void setAxesColor(const QColor& axesColor); - void setLowColor(const QColor& lowColor); - void setMidColor(const QColor& midColor); - void setHighColor(const QColor& highColor); - private: bool m_isSlipRenderer; - WaveformRendererSignalBase::Options m_options; + ::WaveformRendererSignalBase::Options m_options; bool preprocessInner(); diff --git a/src/waveform/renderers/allshader/waveformrenderersignalbase.cpp b/src/waveform/renderers/allshader/waveformrenderersignalbase.cpp index ea906dba69ea..1e48cdbc0edf 100644 --- a/src/waveform/renderers/allshader/waveformrenderersignalbase.cpp +++ b/src/waveform/renderers/allshader/waveformrenderersignalbase.cpp @@ -1,14 +1,41 @@ #include "waveform/renderers/allshader/waveformrenderersignalbase.h" +#include "util/colorcomponents.h" + namespace allshader { WaveformRendererSignalBase::WaveformRendererSignalBase( - WaveformWidgetRenderer* waveformWidget) - : ::WaveformRendererSignalBase(waveformWidget) { + WaveformWidgetRenderer* waveformWidget, ::WaveformRendererSignalBase::Options options) + : ::WaveformRendererSignalBase(waveformWidget, options), + m_ignoreStem(false) { } void WaveformRendererSignalBase::draw(QPainter*, QPaintEvent*) { DEBUG_ASSERT(false); } +void WaveformRendererSignalBase::setAxesColor(const QColor& axesColor) { + getRgbF(axesColor, &m_axesColor_r, &m_axesColor_g, &m_axesColor_b, &m_axesColor_a); +} + +void WaveformRendererSignalBase::setColor(const QColor& color) { + getRgbF(color, &m_signalColor_r, &m_signalColor_g, &m_signalColor_b); + getHsvF(color, &m_signalColor_h, &m_signalColor_s, &m_signalColor_v); +} + +void WaveformRendererSignalBase::setLowColor(const QColor& lowColor) { + getRgbF(lowColor, &m_rgbLowColor_r, &m_rgbLowColor_g, &m_rgbLowColor_b); + getRgbF(lowColor, &m_lowColor_r, &m_lowColor_g, &m_lowColor_b); +} + +void WaveformRendererSignalBase::setMidColor(const QColor& midColor) { + getRgbF(midColor, &m_rgbMidColor_r, &m_rgbMidColor_g, &m_rgbMidColor_b); + getRgbF(midColor, &m_midColor_r, &m_midColor_g, &m_midColor_b); +} + +void WaveformRendererSignalBase::setHighColor(const QColor& highColor) { + getRgbF(highColor, &m_rgbHighColor_r, &m_rgbHighColor_g, &m_rgbHighColor_b); + getRgbF(highColor, &m_highColor_r, &m_highColor_g, &m_highColor_b); +} + } // namespace allshader diff --git a/src/waveform/renderers/allshader/waveformrenderersignalbase.h b/src/waveform/renderers/allshader/waveformrenderersignalbase.h index 1299fe04fa86..a7214f6969b9 100644 --- a/src/waveform/renderers/allshader/waveformrenderersignalbase.h +++ b/src/waveform/renderers/allshader/waveformrenderersignalbase.h @@ -15,23 +15,30 @@ class WaveformRendererSignalBase; class allshader::WaveformRendererSignalBase : public ::WaveformRendererSignalBase { public: - enum class Option { - None = 0b0, - SplitStereoSignal = 0b1, - HighDetail = 0b10, - AllOptionsCombined = SplitStereoSignal | HighDetail, - }; - Q_DECLARE_FLAGS(Options, Option) - void draw(QPainter* painter, QPaintEvent* event) override final; static constexpr float m_maxValue{static_cast(std::numeric_limits::max())}; - explicit WaveformRendererSignalBase(WaveformWidgetRenderer* waveformWidget); + explicit WaveformRendererSignalBase(WaveformWidgetRenderer* waveformWidget, + ::WaveformRendererSignalBase::Options options); virtual bool supportsSlip() const { return false; } + public slots: + void setAxesColor(const QColor& axesColor); + void setColor(const QColor& lowColor); + void setLowColor(const QColor& lowColor); + void setMidColor(const QColor& midColor); + void setHighColor(const QColor& highColor); + + void setIgnoreStem(bool value) { + m_ignoreStem = value; + } + + protected: + bool m_ignoreStem; + DISALLOW_COPY_AND_ASSIGN(WaveformRendererSignalBase); }; diff --git a/src/waveform/renderers/allshader/waveformrenderersimple.cpp b/src/waveform/renderers/allshader/waveformrenderersimple.cpp index 2626e80e4311..3d50774018a7 100644 --- a/src/waveform/renderers/allshader/waveformrenderersimple.cpp +++ b/src/waveform/renderers/allshader/waveformrenderersimple.cpp @@ -12,8 +12,8 @@ using namespace rendergraph; namespace allshader { WaveformRendererSimple::WaveformRendererSimple( - WaveformWidgetRenderer* waveformWidget) - : WaveformRendererSignalBase(waveformWidget) { + WaveformWidgetRenderer* waveformWidget, ::WaveformRendererSignalBase::Options options) + : WaveformRendererSignalBase(waveformWidget, options) { initForRectangles(0); setUsePreprocess(true); } @@ -54,7 +54,7 @@ bool WaveformRendererSimple::preprocessInner() { #ifdef __STEM__ auto stemInfo = pTrack->getStemInfo(); // If this track is a stem track, skip the rendering - if (!stemInfo.isEmpty() && waveform->hasStem()) { + if (!stemInfo.isEmpty() && waveform->hasStem() && !m_ignoreStem) { return false; } #endif @@ -82,8 +82,7 @@ bool WaveformRendererSimple::preprocessInner() { // Per-band gain from the EQ knobs. float allGain{1.0}; - float bandGain[3] = {1.0, 1.0, 1.0}; - getGains(&allGain, false, &bandGain[0], &bandGain[1], &bandGain[2]); + getGains(&allGain, false, nullptr, nullptr, nullptr); const float breadth = static_cast(m_waveformRenderer->getBreadth()); const float halfBreadth = breadth / 2.0f; diff --git a/src/waveform/renderers/allshader/waveformrenderersimple.h b/src/waveform/renderers/allshader/waveformrenderersimple.h index 10c9418186b0..91bcf4303b0c 100644 --- a/src/waveform/renderers/allshader/waveformrenderersimple.h +++ b/src/waveform/renderers/allshader/waveformrenderersimple.h @@ -12,7 +12,8 @@ class allshader::WaveformRendererSimple final : public allshader::WaveformRendererSignalBase, public rendergraph::GeometryNode { public: - explicit WaveformRendererSimple(WaveformWidgetRenderer* waveformWidget); + explicit WaveformRendererSimple(WaveformWidgetRenderer* waveformWidget, + ::WaveformRendererSignalBase::Options options); // Pure virtual from WaveformRendererSignalBase, not used void onSetup(const QDomNode& node) override; diff --git a/src/waveform/renderers/allshader/waveformrendererslipmode.cpp b/src/waveform/renderers/allshader/waveformrendererslipmode.cpp index af122d2d5bf9..08a37653e65c 100644 --- a/src/waveform/renderers/allshader/waveformrendererslipmode.cpp +++ b/src/waveform/renderers/allshader/waveformrendererslipmode.cpp @@ -44,6 +44,11 @@ void WaveformRendererSlipMode::draw(QPainter* painter, QPaintEvent* event) { bool WaveformRendererSlipMode::init() { m_timer.restart(); + if (m_waveformRenderer->getGroup().isEmpty()) { + m_pSlipModeControl.reset(); + return true; + } + m_pSlipModeControl.reset(new ControlProxy( m_waveformRenderer->getGroup(), QStringLiteral("slip_enabled"))); @@ -79,7 +84,8 @@ void WaveformRendererSlipMode::preprocess() { } bool WaveformRendererSlipMode::preprocessInner() { - if (!m_pSlipModeControl->toBool() || !m_waveformRenderer->isSlipActive()) { + if (!m_pSlipModeControl || !m_pSlipModeControl->toBool() || + !m_waveformRenderer->isSlipActive()) { return false; } diff --git a/src/waveform/renderers/allshader/waveformrendererstem.cpp b/src/waveform/renderers/allshader/waveformrendererstem.cpp index 73bbba64cec1..73ed176ed88d 100644 --- a/src/waveform/renderers/allshader/waveformrendererstem.cpp +++ b/src/waveform/renderers/allshader/waveformrendererstem.cpp @@ -33,8 +33,9 @@ namespace allshader { WaveformRendererStem::WaveformRendererStem( WaveformWidgetRenderer* waveformWidget, - ::WaveformRendererAbstract::PositionSource type) - : WaveformRendererSignalBase(waveformWidget), + ::WaveformRendererAbstract::PositionSource type, + ::WaveformRendererSignalBase::Options options) + : WaveformRendererSignalBase(waveformWidget, options), m_isSlipRenderer(type == ::WaveformRendererAbstract::Slip), m_splitStemTracks(false), m_outlineOpacity(0.15f), @@ -47,6 +48,11 @@ void WaveformRendererStem::onSetup(const QDomNode&) { } bool WaveformRendererStem::init() { + m_pStemGain.clear(); + m_pStemMute.clear(); + if (m_waveformRenderer->getGroup().isEmpty()) { + return true; + } for (int stemIdx = 0; stemIdx < mixxx::kMaxSupportedStems; stemIdx++) { QString stemGroup = EngineDeck::getGroupForStem(m_waveformRenderer->getGroup(), stemIdx); m_pStemGain.emplace_back( @@ -223,11 +229,15 @@ bool WaveformRendererStem::preprocessInner() { // Apply the gains if (layerIdx) { - max *= m_pStemMute[stemIdx]->toBool() || + bool isMuted = m_pStemMute.empty() ? false : m_pStemMute[stemIdx]->toBool(); + float volume = m_pStemGain.empty() + ? 1.f + : static_cast(m_pStemGain[stemIdx]->get()); + max *= isMuted || (selectedStems && !(selectedStems & 1 << stemIdx)) ? 0.f - : static_cast(m_pStemGain[stemIdx]->get()); + : volume; } // Lines are thin rectangles diff --git a/src/waveform/renderers/allshader/waveformrendererstem.h b/src/waveform/renderers/allshader/waveformrendererstem.h index d9ad7b873ca6..bac42434a718 100644 --- a/src/waveform/renderers/allshader/waveformrendererstem.h +++ b/src/waveform/renderers/allshader/waveformrendererstem.h @@ -19,7 +19,9 @@ class allshader::WaveformRendererStem final public: explicit WaveformRendererStem(WaveformWidgetRenderer* waveformWidget, ::WaveformRendererAbstract::PositionSource type = - ::WaveformRendererAbstract::Play); + ::WaveformRendererAbstract::Play, + ::WaveformRendererSignalBase::Options options = + ::WaveformRendererSignalBase::Option::None); // Pure virtual from WaveformRendererSignalBase, not used void onSetup(const QDomNode& node) override; diff --git a/src/waveform/renderers/allshader/waveformrenderertextured.cpp b/src/waveform/renderers/allshader/waveformrenderertextured.cpp index 6245204825f8..f5ee356b958d 100644 --- a/src/waveform/renderers/allshader/waveformrenderertextured.cpp +++ b/src/waveform/renderers/allshader/waveformrenderertextured.cpp @@ -31,8 +31,8 @@ WaveformRendererTextured::WaveformRendererTextured( WaveformWidgetRenderer* waveformWidget, ::WaveformWidgetType::Type t, ::WaveformRendererAbstract::PositionSource type, - WaveformRendererSignalBase::Options options) - : WaveformRendererSignalBase(waveformWidget), + ::WaveformRendererSignalBase::Options options) + : WaveformRendererSignalBase(waveformWidget, options), m_unitQuadListId(-1), m_textureId(0), m_textureRenderedWaveformCompletion(0), @@ -380,7 +380,7 @@ void WaveformRendererTextured::paintGL() { if (m_type == ::WaveformWidgetType::RGB) { m_frameShaderProgram->setUniformValue("splitStereoSignal", - m_options & WaveformRendererSignalBase::Option::SplitStereoSignal); + m_options & ::WaveformRendererSignalBase::Option::SplitStereoSignal); } m_frameShaderProgram->setUniformValue("axesColor", diff --git a/src/waveform/renderers/allshader/waveformrenderertextured.h b/src/waveform/renderers/allshader/waveformrenderertextured.h index 3769ae6d09c8..600119587567 100644 --- a/src/waveform/renderers/allshader/waveformrenderertextured.h +++ b/src/waveform/renderers/allshader/waveformrenderertextured.h @@ -24,8 +24,8 @@ class allshader::WaveformRendererTextured final : public allshader::WaveformRend WaveformWidgetType::Type t, ::WaveformRendererAbstract::PositionSource type = ::WaveformRendererAbstract::Play, - WaveformRendererSignalBase::Options options = - WaveformRendererSignalBase::Option::None); + ::WaveformRendererSignalBase::Options options = + ::WaveformRendererSignalBase::Option::None); ~WaveformRendererTextured() override; // override ::WaveformRendererSignalBase @@ -72,7 +72,7 @@ class allshader::WaveformRendererTextured final : public allshader::WaveformRend // shaders bool m_isSlipRenderer; - WaveformRendererSignalBase::Options m_options; + ::WaveformRendererSignalBase::Options m_options; bool m_shadersValid; WaveformWidgetType::Type m_type; const QString m_pFragShader; diff --git a/src/waveform/renderers/allshader/waveformrendermark.cpp b/src/waveform/renderers/allshader/waveformrendermark.cpp index 7bd519498568..110649024dff 100644 --- a/src/waveform/renderers/allshader/waveformrendermark.cpp +++ b/src/waveform/renderers/allshader/waveformrendermark.cpp @@ -176,7 +176,7 @@ allshader::WaveformRenderMark::WaveformRenderMark( m_pPlayPosNode->initForRectangles(1); appendChildNode(std::move(pNode)); } - +#ifndef __SCENEGRAPH__ auto* pWaveformWidgetFactory = WaveformWidgetFactory::instance(); connect(pWaveformWidgetFactory, &WaveformWidgetFactory::untilMarkShowBeatsChanged, @@ -198,6 +198,7 @@ allshader::WaveformRenderMark::WaveformRenderMark( &WaveformWidgetFactory::untilMarkTextHeightLimitChanged, this, &WaveformRenderMark::setUntilMarkTextHeightLimit); +#endif } void allshader::WaveformRenderMark::draw(QPainter*, QPaintEvent*) { @@ -224,8 +225,12 @@ void allshader::WaveformRenderMark::setup(const QDomNode& node, const SkinContex } bool allshader::WaveformRenderMark::init() { - m_pTimeRemainingControl = std::make_unique( - m_waveformRenderer->getGroup(), "time_remaining"); + if (!m_waveformRenderer->getGroup().isEmpty()) { + m_pTimeRemainingControl = std::make_unique( + m_waveformRenderer->getGroup(), "time_remaining"); + } else { + m_pTimeRemainingControl.reset(); + } ::WaveformRenderMarkBase::init(); return true; } @@ -585,7 +590,7 @@ void allshader::WaveformRenderMark::updateUntilMark( } const double endPosition = m_waveformRenderer->getTrackSamples(); - const double remainingTime = m_pTimeRemainingControl->get(); + const double remainingTime = m_pTimeRemainingControl ? m_pTimeRemainingControl->get() : 0; mixxx::BeatsPointer trackBeats = trackInfo->getBeats(); if (!trackBeats) { diff --git a/src/waveform/renderers/deprecated/glwaveformrenderersignal.h b/src/waveform/renderers/deprecated/glwaveformrenderersignal.h index 76532a120145..57539bc91d27 100644 --- a/src/waveform/renderers/deprecated/glwaveformrenderersignal.h +++ b/src/waveform/renderers/deprecated/glwaveformrenderersignal.h @@ -12,8 +12,9 @@ /// QPainter API which Qt translates to OpenGL under the hood. class GLWaveformRendererSignal : public WaveformRendererSignalBase, public GLWaveformRenderer { public: - GLWaveformRendererSignal(WaveformWidgetRenderer* waveformWidgetRenderer) - : WaveformRendererSignalBase(waveformWidgetRenderer) { + GLWaveformRendererSignal(WaveformWidgetRenderer* waveformWidgetRenderer, + ::WaveformRendererSignalBase::Options options) + : WaveformRendererSignalBase(waveformWidgetRenderer, options) { } }; diff --git a/src/waveform/renderers/glvsynctestrenderer.cpp b/src/waveform/renderers/glvsynctestrenderer.cpp index a1d1cb7da275..b93f623092b0 100644 --- a/src/waveform/renderers/glvsynctestrenderer.cpp +++ b/src/waveform/renderers/glvsynctestrenderer.cpp @@ -7,7 +7,8 @@ GLVSyncTestRenderer::GLVSyncTestRenderer( WaveformWidgetRenderer* waveformWidgetRenderer) - : GLWaveformRendererSignal(waveformWidgetRenderer), + : GLWaveformRendererSignal(waveformWidgetRenderer, + WaveformRendererSignalBase::Option::None), m_drawcount(0) { } diff --git a/src/waveform/renderers/qtvsynctestrenderer.cpp b/src/waveform/renderers/qtvsynctestrenderer.cpp index d6b84441f598..1ca4b6c63ff0 100644 --- a/src/waveform/renderers/qtvsynctestrenderer.cpp +++ b/src/waveform/renderers/qtvsynctestrenderer.cpp @@ -9,8 +9,9 @@ QtVSyncTestRenderer::QtVSyncTestRenderer( WaveformWidgetRenderer* waveformWidgetRenderer) - : WaveformRendererSignalBase(waveformWidgetRenderer), - m_drawcount(0) { + : WaveformRendererSignalBase(waveformWidgetRenderer, + ::WaveformRendererSignalBase::Option::None), + m_drawcount(0) { } QtVSyncTestRenderer::~QtVSyncTestRenderer() { diff --git a/src/waveform/renderers/qtwaveformrendererfilteredsignal.cpp b/src/waveform/renderers/qtwaveformrendererfilteredsignal.cpp index e58ebd5a54ad..1fc93604401f 100644 --- a/src/waveform/renderers/qtwaveformrendererfilteredsignal.cpp +++ b/src/waveform/renderers/qtwaveformrendererfilteredsignal.cpp @@ -12,8 +12,9 @@ #include QtWaveformRendererFilteredSignal::QtWaveformRendererFilteredSignal( - WaveformWidgetRenderer* waveformWidgetRenderer) - : WaveformRendererSignalBase(waveformWidgetRenderer) { + WaveformWidgetRenderer* waveformWidgetRenderer, + ::WaveformRendererSignalBase::Options options) + : WaveformRendererSignalBase(waveformWidgetRenderer, options) { } QtWaveformRendererFilteredSignal::~QtWaveformRendererFilteredSignal() { diff --git a/src/waveform/renderers/qtwaveformrendererfilteredsignal.h b/src/waveform/renderers/qtwaveformrendererfilteredsignal.h index c7fed5db4b4d..945bd2af04ed 100644 --- a/src/waveform/renderers/qtwaveformrendererfilteredsignal.h +++ b/src/waveform/renderers/qtwaveformrendererfilteredsignal.h @@ -9,7 +9,9 @@ class ControlObject; class QtWaveformRendererFilteredSignal : public WaveformRendererSignalBase { public: - explicit QtWaveformRendererFilteredSignal(WaveformWidgetRenderer* waveformWidgetRenderer); + explicit QtWaveformRendererFilteredSignal( + WaveformWidgetRenderer* waveformWidgetRenderer, + ::WaveformRendererSignalBase::Options options); virtual ~QtWaveformRendererFilteredSignal(); virtual void onSetup(const QDomNode &node); diff --git a/src/waveform/renderers/waveformmark.cpp b/src/waveform/renderers/waveformmark.cpp index ae4e09e41593..55997d03929a 100644 --- a/src/waveform/renderers/waveformmark.cpp +++ b/src/waveform/renderers/waveformmark.cpp @@ -126,15 +126,15 @@ WaveformMark::WaveformMark(const QString& group, m_showUntilNext = isShowUntilNextPositionControl(positionControl); } - if (!positionControl.isEmpty()) { + if (!positionControl.isEmpty() && !group.isEmpty()) { m_pPositionCO = std::make_unique(group, positionControl); } - if (!endPositionControl.isEmpty()) { + if (!endPositionControl.isEmpty() && !group.isEmpty()) { m_pEndPositionCO = std::make_unique(group, endPositionControl); m_pTypeCO = std::make_unique(group, typeControl); } - if (!visibilityControl.isEmpty()) { + if (!visibilityControl.isEmpty() && !group.isEmpty()) { ConfigKey key = ConfigKey::parseCommaSeparated(visibilityControl); m_pVisibleCO = std::make_unique(key); } diff --git a/src/waveform/renderers/waveformrendererfilteredsignal.cpp b/src/waveform/renderers/waveformrendererfilteredsignal.cpp index 15b6c2d7e38f..97b11b1583c0 100644 --- a/src/waveform/renderers/waveformrendererfilteredsignal.cpp +++ b/src/waveform/renderers/waveformrendererfilteredsignal.cpp @@ -7,8 +7,9 @@ #include "util/painterscope.h" WaveformRendererFilteredSignal::WaveformRendererFilteredSignal( - WaveformWidgetRenderer* waveformWidgetRenderer) - : WaveformRendererSignalBase(waveformWidgetRenderer) { + WaveformWidgetRenderer* waveformWidgetRenderer, + ::WaveformRendererSignalBase::Options options) + : WaveformRendererSignalBase(waveformWidgetRenderer, options) { } WaveformRendererFilteredSignal::~WaveformRendererFilteredSignal() { diff --git a/src/waveform/renderers/waveformrendererfilteredsignal.h b/src/waveform/renderers/waveformrendererfilteredsignal.h index ba8e41a2eba9..00c3510e4496 100644 --- a/src/waveform/renderers/waveformrendererfilteredsignal.h +++ b/src/waveform/renderers/waveformrendererfilteredsignal.h @@ -9,7 +9,7 @@ class WaveformRendererFilteredSignal : public WaveformRendererSignalBase { public: explicit WaveformRendererFilteredSignal( - WaveformWidgetRenderer* waveformWidget); + WaveformWidgetRenderer* waveformWidget, ::WaveformRendererSignalBase::Options options); virtual ~WaveformRendererFilteredSignal(); virtual void onSetup(const QDomNode& node); diff --git a/src/waveform/renderers/waveformrendererhsv.cpp b/src/waveform/renderers/waveformrendererhsv.cpp index 0efba33e8451..2951421108cf 100644 --- a/src/waveform/renderers/waveformrendererhsv.cpp +++ b/src/waveform/renderers/waveformrendererhsv.cpp @@ -8,8 +8,9 @@ #include "waveformwidgetrenderer.h" WaveformRendererHSV::WaveformRendererHSV( - WaveformWidgetRenderer* waveformWidgetRenderer) - : WaveformRendererSignalBase(waveformWidgetRenderer) { + WaveformWidgetRenderer* waveformWidgetRenderer, + ::WaveformRendererSignalBase::Options options) + : WaveformRendererSignalBase(waveformWidgetRenderer, options) { } WaveformRendererHSV::~WaveformRendererHSV() { diff --git a/src/waveform/renderers/waveformrendererhsv.h b/src/waveform/renderers/waveformrendererhsv.h index 4c13b61c9a33..55693ba56638 100644 --- a/src/waveform/renderers/waveformrendererhsv.h +++ b/src/waveform/renderers/waveformrendererhsv.h @@ -6,7 +6,7 @@ class WaveformRendererHSV : public WaveformRendererSignalBase { public: explicit WaveformRendererHSV( - WaveformWidgetRenderer* waveformWidget); + WaveformWidgetRenderer* waveformWidget, ::WaveformRendererSignalBase::Options options); virtual ~WaveformRendererHSV(); virtual void onSetup(const QDomNode& node); diff --git a/src/waveform/renderers/waveformrendererrgb.cpp b/src/waveform/renderers/waveformrendererrgb.cpp index c6b948335f2e..ba521a74ce86 100644 --- a/src/waveform/renderers/waveformrendererrgb.cpp +++ b/src/waveform/renderers/waveformrendererrgb.cpp @@ -6,8 +6,9 @@ #include "util/painterscope.h" WaveformRendererRGB::WaveformRendererRGB( - WaveformWidgetRenderer* waveformWidgetRenderer) - : WaveformRendererSignalBase(waveformWidgetRenderer) { + WaveformWidgetRenderer* waveformWidgetRenderer, + ::WaveformRendererSignalBase::Options options) + : WaveformRendererSignalBase(waveformWidgetRenderer, options) { } WaveformRendererRGB::~WaveformRendererRGB() { diff --git a/src/waveform/renderers/waveformrendererrgb.h b/src/waveform/renderers/waveformrendererrgb.h index fa8e8bbc12f9..9d452f9aa6a0 100644 --- a/src/waveform/renderers/waveformrendererrgb.h +++ b/src/waveform/renderers/waveformrendererrgb.h @@ -6,7 +6,7 @@ class WaveformRendererRGB : public WaveformRendererSignalBase { public: explicit WaveformRendererRGB( - WaveformWidgetRenderer* waveformWidget); + WaveformWidgetRenderer* waveformWidget, ::WaveformRendererSignalBase::Options options); virtual ~WaveformRendererRGB(); virtual void onSetup(const QDomNode& node); diff --git a/src/waveform/renderers/waveformrenderersignalbase.cpp b/src/waveform/renderers/waveformrenderersignalbase.cpp index ed2a75abe084..a38043aa1acc 100644 --- a/src/waveform/renderers/waveformrenderersignalbase.cpp +++ b/src/waveform/renderers/waveformrenderersignalbase.cpp @@ -12,7 +12,7 @@ const QString kEffectGroupFormat = QStringLiteral("[EqualizerRack1_%1_Effect1]") } // namespace WaveformRendererSignalBase::WaveformRendererSignalBase( - WaveformWidgetRenderer* waveformWidgetRenderer) + WaveformWidgetRenderer* waveformWidgetRenderer, Options) : WaveformRendererAbstract(waveformWidgetRenderer), m_pEQEnabled(nullptr), m_pLowFilterControlObject(nullptr), @@ -54,47 +54,35 @@ WaveformRendererSignalBase::WaveformRendererSignalBase( m_rgbHighColor_b(0) { } -WaveformRendererSignalBase::~WaveformRendererSignalBase() { - deleteControls(); -} - -void WaveformRendererSignalBase::deleteControls() { - if (m_pEQEnabled) { - delete m_pEQEnabled; - } - if (m_pLowFilterControlObject) { - delete m_pLowFilterControlObject; - } - if (m_pMidFilterControlObject) { - delete m_pMidFilterControlObject; - } - if (m_pHighFilterControlObject) { - delete m_pHighFilterControlObject; - } - if (m_pLowKillControlObject) { - delete m_pLowKillControlObject; - } - if (m_pMidKillControlObject) { - delete m_pMidKillControlObject; - } - if (m_pHighKillControlObject) { - delete m_pHighKillControlObject; - } -} +WaveformRendererSignalBase::~WaveformRendererSignalBase() = default; bool WaveformRendererSignalBase::init() { - deleteControls(); - - //create controls - m_pEQEnabled = new ControlProxy( - m_waveformRenderer->getGroup(), "filterWaveformEnable"); - const QString effectGroup = kEffectGroupFormat.arg(m_waveformRenderer->getGroup()); - m_pLowFilterControlObject = new ControlProxy(effectGroup, QStringLiteral("parameter1")); - m_pMidFilterControlObject = new ControlProxy(effectGroup, QStringLiteral("parameter2")); - m_pHighFilterControlObject = new ControlProxy(effectGroup, QStringLiteral("parameter3")); - m_pLowKillControlObject = new ControlProxy(effectGroup, QStringLiteral("button_parameter1")); - m_pMidKillControlObject = new ControlProxy(effectGroup, QStringLiteral("button_parameter2")); - m_pHighKillControlObject = new ControlProxy(effectGroup, QStringLiteral("button_parameter3")); + if (!m_waveformRenderer->getGroup().isEmpty()) { + // create controls + m_pEQEnabled = std::make_unique( + m_waveformRenderer->getGroup(), "filterWaveformEnable"); + const QString effectGroup = kEffectGroupFormat.arg(m_waveformRenderer->getGroup()); + m_pLowFilterControlObject = std::make_unique( + effectGroup, QStringLiteral("parameter1")); + m_pMidFilterControlObject = std::make_unique( + effectGroup, QStringLiteral("parameter2")); + m_pHighFilterControlObject = std::make_unique( + effectGroup, QStringLiteral("parameter3")); + m_pLowKillControlObject = std::make_unique( + effectGroup, QStringLiteral("button_parameter1")); + m_pMidKillControlObject = std::make_unique( + effectGroup, QStringLiteral("button_parameter2")); + m_pHighKillControlObject = std::make_unique( + effectGroup, QStringLiteral("button_parameter3")); + } else { + m_pEQEnabled.reset(); + m_pLowFilterControlObject.reset(); + m_pMidFilterControlObject.reset(); + m_pHighFilterControlObject.reset(); + m_pLowKillControlObject.reset(); + m_pMidKillControlObject.reset(); + m_pHighKillControlObject.reset(); + } return onInit(); } @@ -195,7 +183,7 @@ void WaveformRendererSignalBase::getGains(float* pAllGain, CSAMPLE_GAIN lowVisualGain = 1.0, midVisualGain = 1.0, highVisualGain = 1.0; // Only adjust low/mid/high gains if EQs are enabled. - if (m_pEQEnabled->get() > 0.0) { + if (m_pEQEnabled && m_pEQEnabled->get() > 0.0) { if (m_pLowFilterControlObject && m_pMidFilterControlObject && m_pHighFilterControlObject) { diff --git a/src/waveform/renderers/waveformrenderersignalbase.h b/src/waveform/renderers/waveformrenderersignalbase.h index 3611136bc7e2..fbbafd0ee3c9 100644 --- a/src/waveform/renderers/waveformrenderersignalbase.h +++ b/src/waveform/renderers/waveformrenderersignalbase.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "skin/legacy/skincontext.h" #include "util/span.h" #include "util/types.h" @@ -12,8 +14,16 @@ class WaveformSignalColors; class WaveformRendererSignalBase : public QObject, public WaveformRendererAbstract { Q_OBJECT public: + enum class Option { + None = 0b0, + SplitStereoSignal = 0b1, + HighDetail = 0b10, + AllOptionsCombined = SplitStereoSignal | HighDetail, + }; + Q_DECLARE_FLAGS(Options, Option) + explicit WaveformRendererSignalBase( - WaveformWidgetRenderer* waveformWidgetRenderer); + WaveformWidgetRenderer* waveformWidgetRenderer, Options options); virtual ~WaveformRendererSignalBase(); virtual bool init(); @@ -39,8 +49,6 @@ class WaveformRendererSignalBase : public QObject, public WaveformRendererAbstra } protected: - void deleteControls(); - void getGains(float* pAllGain, bool applyCompensation, float* pLowGain, @@ -48,13 +56,13 @@ class WaveformRendererSignalBase : public QObject, public WaveformRendererAbstra float* highGain); protected: - ControlProxy* m_pEQEnabled; - ControlProxy* m_pLowFilterControlObject; - ControlProxy* m_pMidFilterControlObject; - ControlProxy* m_pHighFilterControlObject; - ControlProxy* m_pLowKillControlObject; - ControlProxy* m_pMidKillControlObject; - ControlProxy* m_pHighKillControlObject; + std::unique_ptr m_pEQEnabled; + std::unique_ptr m_pLowFilterControlObject; + std::unique_ptr m_pMidFilterControlObject; + std::unique_ptr m_pHighFilterControlObject; + std::unique_ptr m_pLowKillControlObject; + std::unique_ptr m_pMidKillControlObject; + std::unique_ptr m_pHighKillControlObject; Qt::Alignment m_alignment; Qt::Orientation m_orientation; @@ -66,6 +74,7 @@ class WaveformRendererSignalBase : public QObject, public WaveformRendererAbstra float m_axesColor_r, m_axesColor_g, m_axesColor_b, m_axesColor_a; float m_signalColor_r, m_signalColor_g, m_signalColor_b; + float m_signalColor_h, m_signalColor_s, m_signalColor_v; float m_lowColor_r, m_lowColor_g, m_lowColor_b; float m_midColor_r, m_midColor_g, m_midColor_b; float m_highColor_r, m_highColor_g, m_highColor_b; diff --git a/src/waveform/renderers/waveformwidgetrenderer.cpp b/src/waveform/renderers/waveformwidgetrenderer.cpp index 60e86243cfdf..1a11c1b30e84 100644 --- a/src/waveform/renderers/waveformwidgetrenderer.cpp +++ b/src/waveform/renderers/waveformwidgetrenderer.cpp @@ -103,18 +103,23 @@ bool WaveformWidgetRenderer::init() { m_truePosSample[type] = -1.0; } - VERIFY_OR_DEBUG_ASSERT(!m_group.isEmpty()) { - return false; + // It is possible for a renderer to be defined with no group. This usually + // indicate that the position and track will be controlled by the owner. + // This is used in QML currently. + if (!m_group.isEmpty()) { + m_pRateRatioCO = std::make_unique( + m_group, QStringLiteral("rate_ratio")); + m_pGainControlObject = std::make_unique( + m_group, QStringLiteral("total_gain")); + m_pTrackSamplesControlObject = std::make_unique( + m_group, QStringLiteral("track_samples")); + + m_visualPlayPosition = VisualPlayPosition::getVisualPlayPosition(m_group); } - m_visualPlayPosition = VisualPlayPosition::getVisualPlayPosition(m_group); - - m_pRateRatioCO = std::make_unique( - m_group, QStringLiteral("rate_ratio")); - m_pGainControlObject = std::make_unique( - m_group, QStringLiteral("total_gain")); - m_pTrackSamplesControlObject = std::make_unique( - m_group, QStringLiteral("track_samples")); + VERIFY_OR_DEBUG_ASSERT(m_visualPlayPosition) { + return false; + } for (int i = 0; i < m_rendererStack.size(); ++i) { if (!m_rendererStack[i]->init()) { @@ -137,15 +142,17 @@ void WaveformWidgetRenderer::onPreRender(VSyncTimeProvider* vsyncThread) { } // For a valid track to render we need - m_trackSamples = m_pTrackSamplesControlObject->get(); + m_trackSamples = m_pTrackSamplesControlObject + ? m_pTrackSamplesControlObject->get() + : m_pTrack->getSampleRate() * m_pTrack->getDuration(); if (m_trackSamples <= 0) { return; } //Fetch parameters before rendering in order the display all sub-renderers with the same values - double rateRatio = m_pRateRatioCO->get(); + double rateRatio = m_pRateRatioCO ? m_pRateRatioCO->get() : 1.0; - m_gain = m_pGainControlObject->get(); + m_gain = m_pGainControlObject ? m_pGainControlObject->get() : 1.0; // Compute visual sample to pixel ratio // Allow waveform to spread one visual sample across a hundred pixels diff --git a/src/waveform/renderers/waveformwidgetrenderer.h b/src/waveform/renderers/waveformwidgetrenderer.h index 9e1996051a4a..3541dcc9010e 100644 --- a/src/waveform/renderers/waveformwidgetrenderer.h +++ b/src/waveform/renderers/waveformwidgetrenderer.h @@ -42,6 +42,10 @@ class WaveformWidgetRenderer { void onPreRender(VSyncTimeProvider* vsyncThread); void draw(QPainter* painter, QPaintEvent* event); + void setVisualPlayPosition(const QSharedPointer& value) { + m_visualPlayPosition = value; + } + const QString& getGroup() const { return m_group; } diff --git a/src/waveform/visualplayposition.cpp b/src/waveform/visualplayposition.cpp index 6fce7f1bf3f4..758f1a37d095 100644 --- a/src/waveform/visualplayposition.cpp +++ b/src/waveform/visualplayposition.cpp @@ -17,7 +17,9 @@ VisualPlayPosition::VisualPlayPosition(const QString& key) } VisualPlayPosition::~VisualPlayPosition() { - m_listVisualPlayPosition.remove(m_key); + if (!m_key.isEmpty()) { + m_listVisualPlayPosition.remove(m_key); + } } void VisualPlayPosition::set( diff --git a/src/waveform/visualplayposition.h b/src/waveform/visualplayposition.h index 9b94c91571a4..bded14a5f38e 100644 --- a/src/waveform/visualplayposition.h +++ b/src/waveform/visualplayposition.h @@ -49,7 +49,7 @@ class VisualPlayPositionData { class VisualPlayPosition : public QObject { Q_OBJECT public: - VisualPlayPosition(const QString& m_key); + VisualPlayPosition(const QString& m_key = {}); virtual ~VisualPlayPosition(); // WARNING: Not thread safe. This function must be called only from the diff --git a/src/waveform/waveformwidgetfactory.cpp b/src/waveform/waveformwidgetfactory.cpp index 015ee7125fb6..6883a32c42b0 100644 --- a/src/waveform/waveformwidgetfactory.cpp +++ b/src/waveform/waveformwidgetfactory.cpp @@ -1,5 +1,6 @@ #include "waveform/waveformwidgetfactory.h" +#include "waveform/renderers/waveformrendererabstract.h" #include "waveform/waveform.h" #ifdef MIXXX_USE_QOPENGL @@ -937,7 +938,7 @@ void WaveformWidgetFactory::evaluateWidgets() { m_waveformWidgetHandles.clear(); QHash> collectedHandles; QHash + WaveformRendererSignalBase::Options> supportedOptions; for (WaveformWidgetType::Type type : WaveformWidgetType::kValues) { switch (type) { @@ -1001,22 +1002,21 @@ void WaveformWidgetFactory::evaluateWidgets() { m_waveformWidgetHandles.push_back(WaveformWidgetAbstractHandle(type, backends #ifdef MIXXX_USE_QOPENGL , - supportedOptions.value(type, allshader::WaveformRendererSignalBase::Option::None) + supportedOptions.value(type, WaveformRendererSignalBase::Option::None) #endif )); } } WaveformWidgetAbstract* WaveformWidgetFactory::createAllshaderWaveformWidget( - WaveformWidgetType::Type type, WWaveformViewer* viewer) { - allshader::WaveformRendererSignalBase::Options options = - m_config->getValue(ConfigKey("[Waveform]", "waveform_options"), - allshader::WaveformRendererSignalBase::Option::None); + WaveformWidgetType::Type type, + WWaveformViewer* viewer, + WaveformRendererSignalBase::Options options) { return new allshader::WaveformWidget(viewer, type, viewer->getGroup(), options); } WaveformWidgetAbstract* WaveformWidgetFactory::createFilteredWaveformWidget( - WWaveformViewer* viewer) { + WWaveformViewer* viewer, WaveformRendererSignalBase::Options options) { // On the UI, hardware acceleration is a boolean (0 => software rendering, 1 // => hardware acceleration), but in the setting, we keep the granularity so // in case of issue when we release, we can communicate workaround on @@ -1029,15 +1029,16 @@ WaveformWidgetAbstract* WaveformWidgetFactory::createFilteredWaveformWidget( switch (backend) { #ifdef MIXXX_USE_QOPENGL case WaveformWidgetBackend::AllShader: { - return createAllshaderWaveformWidget(WaveformWidgetType::Type::Filtered, viewer); + return createAllshaderWaveformWidget(WaveformWidgetType::Type::Filtered, viewer, options); } #endif default: - return new SoftwareWaveformWidget(viewer->getGroup(), viewer); + return new SoftwareWaveformWidget(viewer->getGroup(), viewer, options); } } -WaveformWidgetAbstract* WaveformWidgetFactory::createHSVWaveformWidget(WWaveformViewer* viewer) { +WaveformWidgetAbstract* WaveformWidgetFactory::createHSVWaveformWidget( + WWaveformViewer* viewer, WaveformRendererSignalBase::Options options) { // On the UI, hardware acceleration is a boolean (0 => software rendering, 1 // => hardware acceleration), but in the setting, we keep the granularity so // in case of issue when we release, we can communicate workaround on @@ -1050,14 +1051,15 @@ WaveformWidgetAbstract* WaveformWidgetFactory::createHSVWaveformWidget(WWaveform switch (backend) { #ifdef MIXXX_USE_QOPENGL case WaveformWidgetBackend::AllShader: - return createAllshaderWaveformWidget(WaveformWidgetType::HSV, viewer); + return createAllshaderWaveformWidget(WaveformWidgetType::HSV, viewer, options); #endif default: - return new HSVWaveformWidget(viewer->getGroup(), viewer); + return new HSVWaveformWidget(viewer->getGroup(), viewer, options); } } -WaveformWidgetAbstract* WaveformWidgetFactory::createRGBWaveformWidget(WWaveformViewer* viewer) { +WaveformWidgetAbstract* WaveformWidgetFactory::createRGBWaveformWidget( + WWaveformViewer* viewer, WaveformRendererSignalBase::Options options) { // On the UI, hardware acceleration is a boolean (0 => software rendering, 1 // => hardware acceleration), but in the setting, we keep the granularity so // in case of issue when we release, we can communicate workaround on @@ -1070,15 +1072,15 @@ WaveformWidgetAbstract* WaveformWidgetFactory::createRGBWaveformWidget(WWaveform switch (backend) { #ifdef MIXXX_USE_QOPENGL case WaveformWidgetBackend::AllShader: - return createAllshaderWaveformWidget(WaveformWidgetType::Type::RGB, viewer); + return createAllshaderWaveformWidget(WaveformWidgetType::Type::RGB, viewer, options); #endif default: - return new RGBWaveformWidget(viewer->getGroup(), viewer); + return new RGBWaveformWidget(viewer->getGroup(), viewer, options); } } WaveformWidgetAbstract* WaveformWidgetFactory::createStackedWaveformWidget( - WWaveformViewer* viewer) { + WWaveformViewer* viewer, WaveformRendererSignalBase::Options options) { #ifdef MIXXX_USE_QOPENGL // On the UI, hardware acceleration is a boolean (0 => software rendering, 1 // => hardware acceleration), but in the setting, we keep the granularity so @@ -1090,14 +1092,15 @@ WaveformWidgetAbstract* WaveformWidgetFactory::createStackedWaveformWidget( preferredBackend()); switch (backend) { case WaveformWidgetBackend::AllShader: - return createAllshaderWaveformWidget(WaveformWidgetType::Type::Stacked, viewer); + return createAllshaderWaveformWidget(WaveformWidgetType::Type::Stacked, viewer, options); #endif default: return new EmptyWaveformWidget(viewer->getGroup(), viewer); } } -WaveformWidgetAbstract* WaveformWidgetFactory::createSimpleWaveformWidget(WWaveformViewer* viewer) { +WaveformWidgetAbstract* WaveformWidgetFactory::createSimpleWaveformWidget( + WWaveformViewer* viewer, WaveformRendererSignalBase::Options options) { // On the UI, hardware acceleration is a boolean (0 => software rendering, 1 // => hardware acceleration), but in the setting, we keep the granularity so // in case of issue when we release, we can communicate workaround on @@ -1110,7 +1113,7 @@ WaveformWidgetAbstract* WaveformWidgetFactory::createSimpleWaveformWidget(WWavef switch (backend) { #ifdef MIXXX_USE_QOPENGL case WaveformWidgetBackend::AllShader: - return createAllshaderWaveformWidget(WaveformWidgetType::Type::Simple, viewer); + return createAllshaderWaveformWidget(WaveformWidgetType::Type::Simple, viewer, options); #endif default: return new EmptyWaveformWidget(viewer->getGroup(), viewer); @@ -1134,24 +1137,28 @@ WaveformWidgetAbstract* WaveformWidgetFactory::createWaveformWidget( type = WaveformWidgetType::Empty; } + WaveformRendererSignalBase::Options options = + m_config->getValue(ConfigKey("[Waveform]", "waveform_options"), + WaveformRendererSignalBase::Option::None); + switch (type) { case WaveformWidgetType::Simple: - widget = createSimpleWaveformWidget(viewer); + widget = createSimpleWaveformWidget(viewer, options); break; case WaveformWidgetType::Filtered: - widget = createFilteredWaveformWidget(viewer); + widget = createFilteredWaveformWidget(viewer, options); break; case WaveformWidgetType::HSV: - widget = createHSVWaveformWidget(viewer); + widget = createHSVWaveformWidget(viewer, options); break; case WaveformWidgetType::VSyncTest: widget = createVSyncTestWaveformWidget(viewer); break; case WaveformWidgetType::RGB: - widget = createRGBWaveformWidget(viewer); + widget = createRGBWaveformWidget(viewer, options); break; case WaveformWidgetType::Stacked: - widget = createStackedWaveformWidget(viewer); + widget = createStackedWaveformWidget(viewer, options); break; default: widget = new EmptyWaveformWidget(viewer->getGroup(), viewer); diff --git a/src/waveform/waveformwidgetfactory.h b/src/waveform/waveformwidgetfactory.h index 2a1e3d63fd90..73d7c06ea8b0 100644 --- a/src/waveform/waveformwidgetfactory.h +++ b/src/waveform/waveformwidgetfactory.h @@ -10,6 +10,7 @@ #include "util/performancetimer.h" #include "util/singleton.h" #include "waveform/renderers/allshader/waveformrenderersignalbase.h" +#include "waveform/renderers/waveformrenderersignalbase.h" #include "waveform/widgets/waveformwidgettype.h" #include "waveform/widgets/waveformwidgetvars.h" @@ -61,11 +62,11 @@ class WaveformWidgetAbstractHandle { } #ifdef MIXXX_USE_QOPENGL - allshader::WaveformRendererSignalBase::Options supportedOptions( + WaveformRendererSignalBase::Options supportedOptions( WaveformWidgetBackend backend) const { return backend == WaveformWidgetBackend::AllShader ? m_supportedOption - : allshader::WaveformRendererSignalBase::Option::None; + : WaveformRendererSignalBase::Option::None; } #endif @@ -76,7 +77,7 @@ class WaveformWidgetAbstractHandle { QList m_backends; #ifdef MIXXX_USE_QOPENGL // Only relevant for Allshader (accelerated) backend. Other backends don't implement options - allshader::WaveformRendererSignalBase::Options m_supportedOption; + WaveformRendererSignalBase::Options m_supportedOption; #endif friend class WaveformWidgetFactory; @@ -277,7 +278,9 @@ class WaveformWidgetFactory : public QObject, template QString buildWidgetDisplayName() const; WaveformWidgetAbstract* createAllshaderWaveformWidget( - WaveformWidgetType::Type type, WWaveformViewer* viewer); + WaveformWidgetType::Type type, + WWaveformViewer* viewer, + WaveformRendererSignalBase::Options option); WaveformWidgetAbstract* createWaveformWidget(WaveformWidgetType::Type type, WWaveformViewer* viewer); int findIndexOf(WWaveformViewer* viewer) const; @@ -324,11 +327,17 @@ class WaveformWidgetFactory : public QObject, VisualsManager* m_pVisualsManager; // not owned // TODO(#13245): Migrate the following methods to smart pointer. - WaveformWidgetAbstract* createFilteredWaveformWidget(WWaveformViewer* viewer); - WaveformWidgetAbstract* createHSVWaveformWidget(WWaveformViewer* viewer); - WaveformWidgetAbstract* createRGBWaveformWidget(WWaveformViewer* viewer); - WaveformWidgetAbstract* createStackedWaveformWidget(WWaveformViewer* viewer); - WaveformWidgetAbstract* createSimpleWaveformWidget(WWaveformViewer* viewer); + WaveformWidgetAbstract* createFilteredWaveformWidget( + WWaveformViewer* viewer, + WaveformRendererSignalBase::Options option); + WaveformWidgetAbstract* createHSVWaveformWidget(WWaveformViewer* viewer, + WaveformRendererSignalBase::Options option); + WaveformWidgetAbstract* createRGBWaveformWidget(WWaveformViewer* viewer, + WaveformRendererSignalBase::Options option); + WaveformWidgetAbstract* createStackedWaveformWidget(WWaveformViewer* viewer, + WaveformRendererSignalBase::Options option); + WaveformWidgetAbstract* createSimpleWaveformWidget(WWaveformViewer* viewer, + WaveformRendererSignalBase::Options option); WaveformWidgetAbstract* createVSyncTestWaveformWidget(WWaveformViewer* viewer); //Debug diff --git a/src/waveform/widgets/allshader/waveformwidget.cpp b/src/waveform/widgets/allshader/waveformwidget.cpp index d29480668dc0..8b82797461e9 100644 --- a/src/waveform/widgets/allshader/waveformwidget.cpp +++ b/src/waveform/widgets/allshader/waveformwidget.cpp @@ -25,7 +25,7 @@ namespace allshader { WaveformWidget::WaveformWidget(QWidget* parent, WaveformWidgetType::Type type, const QString& group, - WaveformRendererSignalBase::Options options) + ::WaveformRendererSignalBase::Options options) : WGLWidget(parent), WaveformWidgetAbstract(group), m_pWaveformRendererSignal(nullptr) { @@ -101,10 +101,10 @@ WaveformWidget::~WaveformWidget() { std::unique_ptr WaveformWidget::addWaveformSignalRenderer(WaveformWidgetType::Type type, - WaveformRendererSignalBase::Options options, + ::WaveformRendererSignalBase::Options options, ::WaveformRendererAbstract::PositionSource positionSource) { #ifndef QT_OPENGL_ES_2 - if (options & WaveformRendererSignalBase::Option::HighDetail) { + if (options & ::WaveformRendererSignalBase::Option::HighDetail) { switch (type) { case ::WaveformWidgetType::RGB: case ::WaveformWidgetType::Filtered: @@ -119,16 +119,16 @@ WaveformWidget::addWaveformSignalRenderer(WaveformWidgetType::Type type, switch (type) { case ::WaveformWidgetType::Simple: - return addWaveformSignalRenderer(); + return addWaveformSignalRenderer(options); case ::WaveformWidgetType::RGB: return addWaveformSignalRenderer(positionSource, options); case ::WaveformWidgetType::HSV: - return addWaveformSignalRenderer(); + return addWaveformSignalRenderer(options); case ::WaveformWidgetType::Filtered: - return addWaveformSignalRenderer(false); + return addWaveformSignalRenderer(false, options); case ::WaveformWidgetType::Stacked: return addWaveformSignalRenderer( - true); // true for RGB Stacked + true, options); // true for RGB Stacked default: break; } @@ -198,16 +198,16 @@ void WaveformWidget::leaveEvent(QEvent* pEvent) { /* static */ WaveformRendererSignalBase::Options WaveformWidget::supportedOptions( WaveformWidgetType::Type type) { - WaveformRendererSignalBase::Options options = WaveformRendererSignalBase::Option::None; + ::WaveformRendererSignalBase::Options options = ::WaveformRendererSignalBase::Option::None; switch (type) { case WaveformWidgetType::Type::RGB: - options = WaveformRendererSignalBase::Option::AllOptionsCombined; + options = ::WaveformRendererSignalBase::Option::AllOptionsCombined; break; case WaveformWidgetType::Type::Filtered: - options = WaveformRendererSignalBase::Option::HighDetail; + options = ::WaveformRendererSignalBase::Option::HighDetail; break; case WaveformWidgetType::Type::Stacked: - options = WaveformRendererSignalBase::Option::HighDetail; + options = ::WaveformRendererSignalBase::Option::HighDetail; break; default: break; diff --git a/src/waveform/widgets/allshader/waveformwidget.h b/src/waveform/widgets/allshader/waveformwidget.h index 88a871f1fe78..fd5246e31adb 100644 --- a/src/waveform/widgets/allshader/waveformwidget.h +++ b/src/waveform/widgets/allshader/waveformwidget.h @@ -20,7 +20,7 @@ class allshader::WaveformWidget final : public ::WGLWidget, explicit WaveformWidget(QWidget* parent, WaveformWidgetType::Type type, const QString& group, - WaveformRendererSignalBase::Options options); + ::WaveformRendererSignalBase::Options options); ~WaveformWidget() override; WaveformWidgetType::Type getType() const override { @@ -40,7 +40,7 @@ class allshader::WaveformWidget final : public ::WGLWidget, return this; } static WaveformWidgetVars vars(); - static WaveformRendererSignalBase::Options supportedOptions(WaveformWidgetType::Type type); + static ::WaveformRendererSignalBase::Options supportedOptions(WaveformWidgetType::Type type); private: void castToQWidget() override; @@ -61,7 +61,7 @@ class allshader::WaveformWidget final : public ::WGLWidget, std::unique_ptr addWaveformSignalRenderer( WaveformWidgetType::Type type, - WaveformRendererSignalBase::Options options, + ::WaveformRendererSignalBase::Options options, ::WaveformRendererAbstract::PositionSource positionSource); WaveformWidgetType::Type m_type; diff --git a/src/waveform/widgets/hsvwaveformwidget.cpp b/src/waveform/widgets/hsvwaveformwidget.cpp index c3316889ff88..e66530c4671a 100644 --- a/src/waveform/widgets/hsvwaveformwidget.cpp +++ b/src/waveform/widgets/hsvwaveformwidget.cpp @@ -11,13 +11,15 @@ #include "waveform/renderers/waveformrendermark.h" #include "waveform/renderers/waveformrendermarkrange.h" -HSVWaveformWidget::HSVWaveformWidget(const QString& group, QWidget* parent) +HSVWaveformWidget::HSVWaveformWidget(const QString& group, + QWidget* parent, + ::WaveformRendererSignalBase::Options options) : NonGLWaveformWidgetAbstract(group, parent) { addRenderer(); addRenderer(); addRenderer(); addRenderer(); - addRenderer(); + addRenderer(options); addRenderer(); addRenderer(); diff --git a/src/waveform/widgets/hsvwaveformwidget.h b/src/waveform/widgets/hsvwaveformwidget.h index 64770edaf10f..5f4a1a6f197d 100644 --- a/src/waveform/widgets/hsvwaveformwidget.h +++ b/src/waveform/widgets/hsvwaveformwidget.h @@ -1,6 +1,7 @@ #pragma once #include "nonglwaveformwidgetabstract.h" +#include "waveform/renderers/waveformrenderersignalbase.h" class QWidget; @@ -28,6 +29,8 @@ class HSVWaveformWidget : public NonGLWaveformWidgetAbstract { virtual void paintEvent(QPaintEvent* event); private: - HSVWaveformWidget(const QString& group, QWidget* parent); + HSVWaveformWidget(const QString& group, + QWidget* parent, + WaveformRendererSignalBase::Options options); friend class WaveformWidgetFactory; }; diff --git a/src/waveform/widgets/rgbwaveformwidget.cpp b/src/waveform/widgets/rgbwaveformwidget.cpp index 3c7a61805d2e..72b907ede6af 100644 --- a/src/waveform/widgets/rgbwaveformwidget.cpp +++ b/src/waveform/widgets/rgbwaveformwidget.cpp @@ -11,13 +11,15 @@ #include "waveform/renderers/waveformrendermark.h" #include "waveform/renderers/waveformrendermarkrange.h" -RGBWaveformWidget::RGBWaveformWidget(const QString& group, QWidget* parent) +RGBWaveformWidget::RGBWaveformWidget(const QString& group, + QWidget* parent, + WaveformRendererSignalBase::Options options) : NonGLWaveformWidgetAbstract(group, parent) { addRenderer(); addRenderer(); addRenderer(); addRenderer(); - addRenderer(); + addRenderer(options); addRenderer(); addRenderer(); diff --git a/src/waveform/widgets/rgbwaveformwidget.h b/src/waveform/widgets/rgbwaveformwidget.h index 3c925c5359db..255415b697e2 100644 --- a/src/waveform/widgets/rgbwaveformwidget.h +++ b/src/waveform/widgets/rgbwaveformwidget.h @@ -1,6 +1,7 @@ #pragma once #include "nonglwaveformwidgetabstract.h" +#include "waveform/renderers/waveformrenderersignalbase.h" class QWidget; @@ -28,6 +29,8 @@ class RGBWaveformWidget : public NonGLWaveformWidgetAbstract { virtual void paintEvent(QPaintEvent* event); private: - RGBWaveformWidget(const QString& group, QWidget* parent); + RGBWaveformWidget(const QString& group, + QWidget* parent, + WaveformRendererSignalBase::Options options); friend class WaveformWidgetFactory; }; diff --git a/src/waveform/widgets/softwarewaveformwidget.cpp b/src/waveform/widgets/softwarewaveformwidget.cpp index 69099904e608..7f321b67a4d5 100644 --- a/src/waveform/widgets/softwarewaveformwidget.cpp +++ b/src/waveform/widgets/softwarewaveformwidget.cpp @@ -11,13 +11,15 @@ #include "waveform/renderers/waveformrendermark.h" #include "waveform/renderers/waveformrendermarkrange.h" -SoftwareWaveformWidget::SoftwareWaveformWidget(const QString& group, QWidget* parent) +SoftwareWaveformWidget::SoftwareWaveformWidget(const QString& group, + QWidget* parent, + WaveformRendererSignalBase::Options options) : NonGLWaveformWidgetAbstract(group, parent) { addRenderer(); addRenderer(); addRenderer(); addRenderer(); - addRenderer(); + addRenderer(options); addRenderer(); addRenderer(); diff --git a/src/waveform/widgets/softwarewaveformwidget.h b/src/waveform/widgets/softwarewaveformwidget.h index c79d0a545eaf..5227435f213f 100644 --- a/src/waveform/widgets/softwarewaveformwidget.h +++ b/src/waveform/widgets/softwarewaveformwidget.h @@ -1,6 +1,7 @@ #pragma once #include "nonglwaveformwidgetabstract.h" +#include "waveform/renderers/waveformrenderersignalbase.h" class QWidget; @@ -29,6 +30,8 @@ class SoftwareWaveformWidget : public NonGLWaveformWidgetAbstract { virtual void paintEvent(QPaintEvent* event); private: - SoftwareWaveformWidget(const QString& groupp, QWidget* parent); + SoftwareWaveformWidget(const QString& groupp, + QWidget* parent, + WaveformRendererSignalBase::Options options); friend class WaveformWidgetFactory; }; diff --git a/src/widget/wlibrary.h b/src/widget/wlibrary.h index ebce2f45fa63..64c68cfe6f38 100644 --- a/src/widget/wlibrary.h +++ b/src/widget/wlibrary.h @@ -61,7 +61,8 @@ class WLibrary : public QStackedWidget, public WBaseWidget { } signals: - FocusWidget setLibraryFocus(FocusWidget newFocus); + FocusWidget setLibraryFocus(FocusWidget newFocus, + Qt::FocusReason focusReason = Qt::OtherFocusReason); public slots: // Show the view registered with the given name. Does nothing if the current diff --git a/src/widget/wlibrarysidebar.h b/src/widget/wlibrarysidebar.h index f74345c5f22a..2e092751ae66 100644 --- a/src/widget/wlibrarysidebar.h +++ b/src/widget/wlibrarysidebar.h @@ -38,7 +38,8 @@ class WLibrarySidebar : public QTreeView, public WBaseWidget { void rightClicked(const QPoint&, const QModelIndex&); void renameItem(const QModelIndex&); void deleteItem(const QModelIndex&); - FocusWidget setLibraryFocus(FocusWidget newFocus); + FocusWidget setLibraryFocus(FocusWidget newFocus, + Qt::FocusReason focusReason = Qt::OtherFocusReason); protected: bool event(QEvent* pEvent) override; diff --git a/src/widget/wmainmenubar.cpp b/src/widget/wmainmenubar.cpp index 0d07eaff3966..b22196815dfa 100644 --- a/src/widget/wmainmenubar.cpp +++ b/src/widget/wmainmenubar.cpp @@ -169,6 +169,34 @@ void WMainMenuBar::initialize() { pLibraryMenu->addSeparator(); + QString searchHereTitle = tr("Search in Current View..."); + QString searchHereText = tr("Search for tracks in the current library view"); + auto* pSearchHere = new QAction(searchHereTitle, this); + pSearchHere->setShortcut(QKeySequence(m_pKbdConfig->getValue( + ConfigKey("[KeyboardShortcuts]", "LibraryMenu_SearchInCurrentView"), + tr("Ctrl+f")))); + pSearchHere->setShortcutContext(Qt::ApplicationShortcut); + pSearchHere->setStatusTip(searchHereText); + pSearchHere->setWhatsThis(buildWhatsThis(searchHereTitle, searchHereText)); + connect(pSearchHere, &QAction::triggered, this, &WMainMenuBar::searchInCurrentView); + pLibraryMenu->addAction(pSearchHere); + + QString searchAllTitle = tr("Search in Tracks Library..."); + QString searchAllText = + tr("Search in the internal track collection under \"Tracks\" in " + "the library"); + auto* pSearchAll = new QAction(searchAllTitle, this); + pSearchAll->setShortcut(QKeySequence(m_pKbdConfig->getValue( + ConfigKey("[KeyboardShortcuts]", "LibraryMenu_SearchInAllTracks"), + tr("Ctrl+Shift+F")))); + pSearchAll->setShortcutContext(Qt::ApplicationShortcut); + pSearchAll->setStatusTip(searchAllText); + pSearchAll->setWhatsThis(buildWhatsThis(searchAllText, searchAllText)); + connect(pSearchAll, &QAction::triggered, this, &WMainMenuBar::searchInAllTracks); + pLibraryMenu->addAction(pSearchAll); + + pLibraryMenu->addSeparator(); + QString createPlaylistTitle = tr("Create &New Playlist"); QString createPlaylistText = tr("Create a new playlist"); auto* pLibraryCreatePlaylist = new QAction(createPlaylistTitle, this); diff --git a/src/widget/wmainmenubar.h b/src/widget/wmainmenubar.h index 98ec66c698f7..dd9fafadbcee 100644 --- a/src/widget/wmainmenubar.h +++ b/src/widget/wmainmenubar.h @@ -67,6 +67,8 @@ class WMainMenuBar : public QMenuBar { #ifdef __ENGINEPRIME__ void exportLibrary(); #endif + void searchInCurrentView(); + void searchInAllTracks(); void showAbout(); void showKeywheel(bool visible); void showPreferences(); diff --git a/src/widget/wsearchlineedit.cpp b/src/widget/wsearchlineedit.cpp index 871abd384415..9a4b53b5522d 100644 --- a/src/widget/wsearchlineedit.cpp +++ b/src/widget/wsearchlineedit.cpp @@ -122,12 +122,6 @@ WSearchLineEdit::WSearchLineEdit(QWidget* pParent, UserSettingsPointer pConfig) this, &WSearchLineEdit::slotClearSearch); - QShortcut* setFocusShortcut = new QShortcut(QKeySequence(tr("Ctrl+F", "Search|Focus")), this); - connect(setFocusShortcut, - &QShortcut::activated, - this, - &WSearchLineEdit::slotSetShortcutFocus); - // Set up a timer to search after a few hundred milliseconds timeout. This // stops us from thrashing the database if you type really fast. m_debouncingTimer.setSingleShot(true); @@ -217,31 +211,35 @@ void WSearchLineEdit::setup(const QDomNode& node, const SkinContext& context) { setPalette(pal); m_clearButton->setToolTip(tr("Clear input") + "\n" + - tr("Clear the search bar input field") + "\n\n" + - - tr("Shortcut") + ": \n" + - tr("Ctrl+Backspace")); + tr("Clear the search bar input field")); +} +void WSearchLineEdit::setupToolTip(const QString& searchInCurrentViewShortcut, + const QString& searchInAllTracksShortcut) { setBaseTooltip(tr("Search", "noun") + "\n" + - tr("Enter a string to search for") + "\n" + - tr("Use operators like bpm:115-128, artist:BooFar, -year:1990") + - "\n" + tr("For more information see User Manual > Mixxx Library") + - "\n\n" + - tr("Shortcuts") + ": \n" + - tr("Ctrl+F") + " " + - tr("Focus", "Give search bar input focus") + "\n" + - tr("Return") + " " + - tr("Trigger search before search-as-you-type timeout or" - "jump to tracks view afterwards") + + tr("Enter a string to search for.") + " " + + tr("Use operators like bpm:115-128, artist:BooFar, -year:1990.") + + "\n" + tr("See User Manual > Mixxx Library for more information.") + + "\n\n" + searchInCurrentViewShortcut + ": " + + tr("Focus/Select All (Search in current view)", + "Give search bar input focus") + + "\n" + searchInAllTracksShortcut + ": " + + tr("Focus/Select All (Search in \'Tracks\' library view)") + + "\n\n" + tr("Additional Shortcuts When Focused:") + "\n" + + tr("Return") + ": " + + tr("Trigger search before search-as-you-type timeout or " + "focus tracks view afterwards") + "\n" + - tr("Ctrl+Backspace") + " " + - tr("Clear input", "Clear the search bar input field") + "\n" + - tr("Ctrl+Space") + " " + + tr("Esc or Ctrl+Return") + ": " + + tr("Immediately trigger search and focus tracks view", + "Exit search bar and leave focus") + + "\n" + tr("Ctrl+Space") + ": " + tr("Toggle search history", "Shows/hides the search history entries") + "\n" + - tr("Delete or Backspace") + " " + tr("Delete query from history") + "\n" + - tr("Esc") + " " + tr("Exit search", "Exit search bar and leave focus")); + tr("Delete or Backspace") + + " (" + tr("in search history") + "): " + + tr("Delete query from history")); } void WSearchLineEdit::loadQueriesFromConfig() { @@ -311,15 +309,12 @@ QString WSearchLineEdit::getSearchText() const { if (isEnabled()) { DEBUG_ASSERT(!currentText().isNull()); QString text = currentText(); - QCompleter* pCompleter = completer(); - if (pCompleter && hasSelectedText()) { - if (text.startsWith(pCompleter->completionPrefix()) && - pCompleter->completionPrefix().size() == lineEdit()->cursorPosition()) { - // Search for the entered text until the user has accepted the - // completion by pressing Enter or changed/deselected the selected - // completion text with Right or Left key - return pCompleter->completionPrefix(); - } + QString completionPrefix; + if (hasCompletionAvailable(&completionPrefix)) { + // Search for the entered text until the user has accepted the + // completion by pressing Enter or changed/deselected the selected + // completion text with Right or Left key + return completionPrefix; } return text; } else { @@ -402,7 +397,12 @@ void WSearchLineEdit::keyPressEvent(QKeyEvent* keyEvent) { if (slotClearSearchIfClearButtonHasFocus()) { return; } - if (hasSelectedText()) { + if (keyEvent->modifiers() & Qt::ControlModifier) { + // Esc and Ctrl+Enter should have the same effect + emit setLibraryFocus(FocusWidget::TracksTable); + return; + } + if (hasCompletionAvailable()) { QComboBox::keyPressEvent(keyEvent); slotTriggerSearch(); return; @@ -793,11 +793,18 @@ void WSearchLineEdit::slotTextChanged(const QString& text) { m_saveTimer.start(kSaveTimeoutMillis); } -void WSearchLineEdit::slotSetShortcutFocus() { - if (hasFocus()) { +void WSearchLineEdit::setFocus(Qt::FocusReason focusReason) { + if (!hasFocus()) { + // selectAll will be called by setFocus - but only if hasFocus + // was false previously and focusReason is Tab, Backtab or Shortcut + QWidget::setFocus(focusReason); + } else if (focusReason == Qt::TabFocusReason || + focusReason == Qt::BacktabFocusReason || + focusReason == Qt::ShortcutFocusReason) { + // If this widget already had focus (which can happen when the user + // presses the shortcut key while already in the searchbox), + // we need to manually simulate this behavior instead. lineEdit()->selectAll(); - } else { - setFocus(Qt::ShortcutFocusReason); } } @@ -813,3 +820,17 @@ void WSearchLineEdit::slotSetFont(const QFont& font) { bool WSearchLineEdit::hasSelectedText() const { return lineEdit()->hasSelectedText(); } + +bool WSearchLineEdit::hasCompletionAvailable(QString* completionPrefix) const { + QCompleter* pCompleter = completer(); + QString prefix = pCompleter ? pCompleter->completionPrefix() : QString(); + if (!prefix.isEmpty() && hasSelectedText() && + lineEdit()->text().startsWith(prefix) && + prefix.size() == lineEdit()->cursorPosition()) { + if (completionPrefix) { + *completionPrefix = prefix; + } + return true; + } + return false; +} diff --git a/src/widget/wsearchlineedit.h b/src/widget/wsearchlineedit.h index 110230d00808..f83f76647f04 100644 --- a/src/widget/wsearchlineedit.h +++ b/src/widget/wsearchlineedit.h @@ -36,6 +36,10 @@ class WSearchLineEdit : public QComboBox, public WBaseWidget { ~WSearchLineEdit(); void setup(const QDomNode& node, const SkinContext& context); + void setupToolTip(const QString& searchInCurrentViewShortcut, + const QString& searchInAllTracksShortcut); + + void setFocus(Qt::FocusReason focusReason); protected: void resizeEvent(QResizeEvent*) override; @@ -47,7 +51,8 @@ class WSearchLineEdit : public QComboBox, public WBaseWidget { signals: void search(const QString& text); - FocusWidget setLibraryFocus(FocusWidget newFocusWidget); + FocusWidget setLibraryFocus(FocusWidget newFocusWidget, + Qt::FocusReason focusReason = Qt::OtherFocusReason); public slots: void slotSetFont(const QFont& font); @@ -65,7 +70,6 @@ class WSearchLineEdit : public QComboBox, public WBaseWidget { void slotDeleteCurrentItem(); private slots: - void slotSetShortcutFocus(); void slotTextChanged(const QString& text); void slotIndexChanged(int index); @@ -92,6 +96,7 @@ class WSearchLineEdit : public QComboBox, public WBaseWidget { void deleteSelectedListItem(); void triggerSearchDebounced(); bool hasSelectedText() const; + bool hasCompletionAvailable(QString* completionPrefix = nullptr) const; inline int findCurrentTextIndex() { return findData(currentText().trimmed(), Qt::DisplayRole);