diff --git a/CHANGELOG.md b/CHANGELOG.md index 920c326e66..b66b206f78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,7 @@ set the stutter configuration independently of the song configuration if you wis #### Audio Clips - Added audio output modes, and changed audio clip monitoring to be seperate from source selection. Monitoring is now on when the output is a SAMPLER or a LOOPER, chosen by turning the select knob in an audio clip. +- Added the ability to trim from the start of an audio clip without reversing it (enable in Community Features menu). #### Instrument Clip View diff --git a/docs/community_features.md b/docs/community_features.md index 1d8f01cdfe..ea17fe4e70 100644 --- a/docs/community_features.md +++ b/docs/community_features.md @@ -1175,6 +1175,18 @@ for the Lumi Keys Studio Edition, described below. - While Lumi has limited options for MPE separation, it will be configured to align with the dominant MPE range defined on the Deluge (upper or lower dominant). +### 4.11.0 - Audio Clip View - Trimming Clips + +- ([#3291]) Added a new `STRING_FOR_COMMUNITY_FEATURE_TRIM_FROM_START_OF_AUDIO_CLIP` feature to allow users to trim from the start of a clip without needing to reverse it. + - Pressing a pad in the first column of an audio clip now makes it flash green allowing you to move the start position. The new start position snaps to column one. + - You can revert to an earlier portion by pressing undo (or reverse the clip and altering as before). + - This lets you easily alter the start of an audio clip without time stretching. + - Previously, this was only possible by reversing the audio clip and trimming the start as if it were the end. + - This feature is `OFF` by default and can be set to `ON` or `OFF` via `SETTINGS > COMMUNITY FEATURES`. + +See this demo for more details: +[Audio Clip View - Trimming Tips](https://www.youtube.com/watch?v=iWhVUsx40Mg&t=45s&ab_channel=RonCavagnaro). + ## 5. Community Features Menu (aka Runtime Settings) In the main menu of the Deluge (accessed by pressing both "SHIFT" + the "SELECT" encoder) there is @@ -1240,6 +1252,8 @@ Note: these settings are saved to `SETTINGS/CommunityFeatures.XML` on your SD ca * When On, the number of `TAP TEMPO` button presses to engage `TAP TEMPO` is changed to `FOUR (4)` to avoid mistakingly changing tempo. * `Horizontal menus (HORI)` * When On, some menu items render in horizontal menus, with multiple items visible and editable at the same time. +* `Trim from start of audio clips (TRIM)` + * When On, the ability to trim from the start of an audio clip without needing to reverse it is enabled. ## 6. Sysex Handling @@ -1576,6 +1590,8 @@ different firmware [#3285]: https://github.com/SynthstromAudible/DelugeFirmware/pull/3285 +[#3291]: https://github.com/SynthstromAudible/DelugeFirmware/pull/3291 + [Automation View Documentation]: features/automation_view.md [Arpeggiator Documentation]: features/arpeggiator.md diff --git a/docs/menu_hierarchies.md b/docs/menu_hierarchies.md index 861e665887..09dd6a56df 100644 --- a/docs/menu_hierarchies.md +++ b/docs/menu_hierarchies.md @@ -424,6 +424,9 @@ NOTE: These options can change depending on how your default resolution is set - Grid View Loop Pads (LOOP) - OFF - ON + - Trim from start of audio clips (TRIM) + - OFF + - ON Firmware Version (FIRM) @@ -548,7 +551,7 @@ The Song menu contains the following menu hierarchy: - Pan - Reverb Sidechain (SIDE) - Volume Ducking (VOLU) - + - Stutter (STUT) - Quantize (QTZ) - Reverse (REVE) @@ -843,7 +846,7 @@ The Sound menu contains the following menu hierarchy: - Pan - Reverb Sidechain (SIDE) - Volume Ducking (VOLU) - + - Stutter (STUT) - Use Song Settings (SONG) - Quantize (QTZ) @@ -1228,7 +1231,7 @@ The Kit FX menu contains the following menu hierarchy: - Pan - Reverb Sidechain (SIDE) - Volume Ducking (VOLU) - + - Stutter (STUT) - Use Song Settings (SONG) - Quantize (QTZ) @@ -1430,7 +1433,7 @@ The CV menu contains the following menu hierarchy: - Gate - Sync NOTE: These options can change depending on how your default resolution is set - + - Off - 2-Bar - 1-Bar @@ -1804,7 +1807,7 @@ The Audio Clip menu contains the following menu hierarchy: - Pan - Reverb Sidechain (SIDE) - Volume Ducking (VOLU) - + - Stutter (STUT) - Use Song Settings (SONG) - Quantize (QTZ) diff --git a/src/deluge/gui/colour/rgb.h b/src/deluge/gui/colour/rgb.h index a9d883adbe..72686f6dfc 100644 --- a/src/deluge/gui/colour/rgb.h +++ b/src/deluge/gui/colour/rgb.h @@ -32,6 +32,14 @@ class RGB { /// Blue channel channel_type b = 0; + /// Copies RGB values from a colour + constexpr RGB& operator=(const RGB& other) { + r = other.r; + g = other.g; + b = other.b; + return *this; + } + /** * @brief Construct a monochrome (white) shade * diff --git a/src/deluge/gui/l10n/english.json b/src/deluge/gui/l10n/english.json index ba212be581..2055637972 100644 --- a/src/deluge/gui/l10n/english.json +++ b/src/deluge/gui/l10n/english.json @@ -553,6 +553,7 @@ "STRING_FOR_COMMUNITY_FEATURE_GRID_VIEW_LOOP_PADS": "Grid View Loop Layer Pads", "STRING_FOR_COMMUNITY_FEATURE_ALTERNATIVE_TAP_TEMPO_BEHAVIOUR": "Alternative Tap Tempo Behaviour", "STRING_FOR_COMMUNITY_FEATURE_HORIZONTAL_MENUS": "Horizontal menus", + "STRING_FOR_COMMUNITY_FEATURE_TRIM_FROM_START_OF_AUDIO_CLIP": "Trim from start of audio clips", "STRING_FOR_TRACK_STILL_HAS_CLIPS_IN_SESSION": "Track still has clips in session", "STRING_FOR_DELETE_ALL_TRACKS_CLIPS_FIRST": "Delete all track's clips first", diff --git a/src/deluge/gui/l10n/g_english.cpp b/src/deluge/gui/l10n/g_english.cpp index 56378a482c..e73ab1705f 100644 --- a/src/deluge/gui/l10n/g_english.cpp +++ b/src/deluge/gui/l10n/g_english.cpp @@ -507,6 +507,7 @@ PLACE_SDRAM_DATA Language english{ {STRING_FOR_COMMUNITY_FEATURE_GRID_VIEW_LOOP_PADS, "Grid View Loop Layer Pads"}, {STRING_FOR_COMMUNITY_FEATURE_ALTERNATIVE_TAP_TEMPO_BEHAVIOUR, "Alternative Tap Tempo Behaviour"}, {STRING_FOR_COMMUNITY_FEATURE_HORIZONTAL_MENUS, "Horizontal menus"}, + {STRING_FOR_COMMUNITY_FEATURE_TRIM_FROM_START_OF_AUDIO_CLIP, "Trim from start of audio clips"}, {STRING_FOR_TRACK_STILL_HAS_CLIPS_IN_SESSION, "Track still has clips in session"}, {STRING_FOR_DELETE_ALL_TRACKS_CLIPS_FIRST, "Delete all track's clips first"}, {STRING_FOR_CANT_DELETE_FINAL_CLIP, "Can't delete final Clip"}, diff --git a/src/deluge/gui/l10n/g_seven_segment.cpp b/src/deluge/gui/l10n/g_seven_segment.cpp index d04f68453e..5cd8cb9191 100644 --- a/src/deluge/gui/l10n/g_seven_segment.cpp +++ b/src/deluge/gui/l10n/g_seven_segment.cpp @@ -408,6 +408,7 @@ PLACE_SDRAM_DATA Language seven_segment{ {STRING_FOR_COMMUNITY_FEATURE_ALTERNATIVE_PLAYBACK_START_BEHAVIOUR, "STAR"}, {STRING_FOR_COMMUNITY_FEATURE_GRID_VIEW_LOOP_PADS, "LOOP"}, {STRING_FOR_COMMUNITY_FEATURE_ALTERNATIVE_TAP_TEMPO_BEHAVIOUR, "TAPT"}, + {STRING_FOR_COMMUNITY_FEATURE_TRIM_FROM_START_OF_AUDIO_CLIP, "TRIM"}, {STRING_FOR_TRACK_STILL_HAS_CLIPS_IN_SESSION, "CANT"}, {STRING_FOR_DELETE_ALL_TRACKS_CLIPS_FIRST, "CANT"}, {STRING_FOR_CANT_DELETE_FINAL_CLIP, "CANT"}, diff --git a/src/deluge/gui/l10n/seven_segment.json b/src/deluge/gui/l10n/seven_segment.json index 3618d0ca82..e437cb7f31 100644 --- a/src/deluge/gui/l10n/seven_segment.json +++ b/src/deluge/gui/l10n/seven_segment.json @@ -421,6 +421,7 @@ "STRING_FOR_COMMUNITY_FEATURE_ALTERNATIVE_PLAYBACK_START_BEHAVIOUR": "STAR", "STRING_FOR_COMMUNITY_FEATURE_GRID_VIEW_LOOP_PADS": "LOOP", "STRING_FOR_COMMUNITY_FEATURE_ALTERNATIVE_TAP_TEMPO_BEHAVIOUR": "TAPT", + "STRING_FOR_COMMUNITY_FEATURE_TRIM_FROM_START_OF_AUDIO_CLIP": "TRIM", "STRING_FOR_TRACK_STILL_HAS_CLIPS_IN_SESSION": "CANT", "STRING_FOR_DELETE_ALL_TRACKS_CLIPS_FIRST": "CANT", @@ -450,7 +451,6 @@ "STRING_FOR_WRONG_SIZE": "SIZE FAIL", "STRING_FOR_BAD_KEY": "KEY FAIL", - "STRING_FOR_CLIP_CLEARED": "CLEAR", "STRING_FOR_SAMPLE_CLEARED": "CLEAR", "STRING_FOR_NOTES_CLEARED": "CLEAR", "STRING_FOR_AUTOMATION_CLEARED": "CLEAR", diff --git a/src/deluge/gui/l10n/strings.h b/src/deluge/gui/l10n/strings.h index cbf5a65154..4afe6330fd 100644 --- a/src/deluge/gui/l10n/strings.h +++ b/src/deluge/gui/l10n/strings.h @@ -538,6 +538,7 @@ enum class String : size_t { STRING_FOR_COMMUNITY_FEATURE_GRID_VIEW_LOOP_PADS, STRING_FOR_COMMUNITY_FEATURE_ALTERNATIVE_TAP_TEMPO_BEHAVIOUR, STRING_FOR_COMMUNITY_FEATURE_HORIZONTAL_MENUS, + STRING_FOR_COMMUNITY_FEATURE_TRIM_FROM_START_OF_AUDIO_CLIP, STRING_FOR_TRACK_STILL_HAS_CLIPS_IN_SESSION, STRING_FOR_DELETE_ALL_TRACKS_CLIPS_FIRST, diff --git a/src/deluge/gui/menu_item/runtime_feature/settings.cpp b/src/deluge/gui/menu_item/runtime_feature/settings.cpp index 00f97341b2..d130281aa0 100644 --- a/src/deluge/gui/menu_item/runtime_feature/settings.cpp +++ b/src/deluge/gui/menu_item/runtime_feature/settings.cpp @@ -49,6 +49,7 @@ SettingToggle menuAlternativePlaybackStartBehaviour(RuntimeFeatureSettingType::A SettingToggle menuEnableGridViewLoopPads(RuntimeFeatureSettingType::EnableGridViewLoopPads); SettingToggle menuAlternativeTapTempoBehaviour(RuntimeFeatureSettingType::AlternativeTapTempoBehaviour); SettingToggle menuHorizontalMenus(RuntimeFeatureSettingType::HorizontalMenus); +SettingToggle menuTrimFromStartOfAudioClip(RuntimeFeatureSettingType::TrimFromStartOfAudioClip); std::array subMenuEntries{ &menuDrumRandomizer, @@ -72,7 +73,8 @@ std::arraygetCurrentlyRecordingLinearly()) { - return getCurrentAudioClip()->recorder->sample; - } - else { - return (Sample*)getCurrentAudioClip()->sampleHolder.audioFile; + AudioClip& clip = *getCurrentAudioClip(); + if (clip.getCurrentlyRecordingLinearly()) { + return clip.recorder->sample; } + return static_cast(clip.sampleHolder.audioFile); } bool AudioClipView::opened() { @@ -79,6 +79,7 @@ bool AudioClipView::opened() { void AudioClipView::focusRegained() { ClipView::focusRegained(); endMarkerVisible = false; + startMarkerVisible = false; indicator_leds::setLedState(IndicatorLED::BACK, false); view.focusRegained(); view.setActiveModControllableTimelineCounter(getCurrentClip()); @@ -105,72 +106,117 @@ bool AudioClipView::renderMainPads(uint32_t whichRows, RGB image[][kDisplayWidth return true; } - int32_t endSquareDisplay = divide_round_negative( - getCurrentAudioClip()->loopLength - currentSong->xScroll[NAVIGATION_CLIP] - 1, - currentSong->xZoom[NAVIGATION_CLIP]); // Rounds it well down, so we get the "final square" kinda... - // If no Sample, just clear display if (!getSample()) { + for (int32_t y = 0; y < kDisplayHeight; y++) { + memset(image[y], 0, kDisplayWidth * 3); + } + return true; + } + // If no audio clip, clear display + AudioClip* clipPtr = getCurrentAudioClip(); + if (!clipPtr) { for (int32_t y = 0; y < kDisplayHeight; y++) { memset(image[y], 0, kDisplayWidth * 3); } + return true; } - // Or if yes Sample... - else { + AudioClip& clip = *clipPtr; + SampleRecorder* recorder = clip.recorder; - SampleRecorder* recorder = getCurrentAudioClip()->recorder; + // end marker column + int32_t endSquareDisplay = divide_round_negative(clip.loopLength - currentSong->xScroll[NAVIGATION_CLIP] - 1, + currentSong->xZoom[NAVIGATION_CLIP]); - int64_t xScrollSamples; - int64_t xZoomSamples; + // start marker column + int32_t startSquareDisplay = + divide_round_negative(0 - currentSong->xScroll[NAVIGATION_CLIP], currentSong->xZoom[NAVIGATION_CLIP]); - getCurrentAudioClip()->getScrollAndZoomInSamples( - currentSong->xScroll[NAVIGATION_CLIP], currentSong->xZoom[NAVIGATION_CLIP], &xScrollSamples, &xZoomSamples); + int64_t xScrollSamples; + int64_t xZoomSamples; + clip.getScrollAndZoomInSamples(currentSong->xScroll[NAVIGATION_CLIP], currentSong->xZoom[NAVIGATION_CLIP], + &xScrollSamples, &xZoomSamples); - RGB rgb = getCurrentAudioClip()->getColour(); + RGB rgb = clip.getColour(); - int32_t visibleWaveformXEnd = endSquareDisplay + 1; - if (endMarkerVisible && blinkOn) { - visibleWaveformXEnd--; - } - int32_t xEnd = std::min(kDisplayWidth, visibleWaveformXEnd); + // Adjust xEnd if end marker is blinking + int32_t visibleWaveformXEnd = endSquareDisplay + 1; + if (endMarkerVisible && blinkOn) { + visibleWaveformXEnd--; + } + int32_t xEnd = std::min(kDisplayWidth, visibleWaveformXEnd); - bool success = waveformRenderer.renderFullScreen(getSample(), xScrollSamples, xZoomSamples, image, - &getCurrentAudioClip()->renderData, recorder, rgb, - getCurrentAudioClip()->sampleControls.reversed, xEnd); + bool success = waveformRenderer.renderFullScreen(getSample(), xScrollSamples, xZoomSamples, image, &clip.renderData, + recorder, rgb, clip.sampleControls.reversed, xEnd); - // If card being accessed and waveform would have to be re-examined, come back later - if (!success && image == PadLEDs::image) { - uiNeedsRendering(this, whichRows, 0); - return true; - } + // If card being accessed and waveform would have to be re-examined, come back later + if (!success && image == PadLEDs::image) { + uiNeedsRendering(this, whichRows, 0); + return true; } + // If asked, draw grey regions + flashing columns if (drawUndefinedArea) { - for (int32_t y = 0; y < kDisplayHeight; y++) { + // -------- END marker ---------- if (endSquareDisplay < kDisplayWidth) { - if (endSquareDisplay >= 0) { - if (endMarkerVisible && blinkOn) { - image[y][endSquareDisplay][0] = 255; - image[y][endSquareDisplay][1] = 0; - image[y][endSquareDisplay][2] = 0; + // If endMarkerVisible, show red (bright vs. dim). + if (endMarkerVisible) { + if (blinkOn) { + image[y][endSquareDisplay] = colours::red; + } + else { + image[y][endSquareDisplay] = colours::red_dull; + } } } - int32_t xDisplay = endSquareDisplay + 1; + if (xDisplay < kDisplayWidth) { + if (xDisplay < 0) { + xDisplay = 0; + } + RGB greyCol = colours::grey; + std::fill(&image[y][xDisplay], &image[y][kDisplayWidth], greyCol); + } + } - if (xDisplay >= kDisplayWidth) { - continue; + // -------- START marker ---------- + + if (startSquareDisplay >= 0) { + if (startSquareDisplay < kDisplayWidth) { + // Fill grey area first + // int32_t fillEnd = startSquareDisplay; + // if (fillEnd > kDisplayWidth) { + // fillEnd = kDisplayWidth; + // } + // for (int32_t xPos = 0; xPos < fillEnd; ++xPos) { + // image[y][xPos][0] = colours::grey; + // } + + // Then overlay the green start marker if visible + if (startMarkerVisible) { + if (blinkOn) { + // bright green + image[y][startSquareDisplay] = colours::green; + } + else { + // dim green - using a darker version of green + image[y][startSquareDisplay] = colours::green.dim(); + } + } + // else { + // // If not visible, ensure this column is grey + // image[y][startSquareDisplay] = colours::grey; + // } } - else if (xDisplay < 0) { - xDisplay = 0; + else { + RGB greyCol = colours::grey; + std::fill(&image[y][0], &image[y][kDisplayWidth], greyCol); } - - std::fill(&image[y][xDisplay], &image[y][xDisplay] + (kDisplayWidth - xDisplay), colours::grey); } } } @@ -183,7 +229,6 @@ ActionResult AudioClipView::timerCallback() { uiNeedsRendering(this, 0xFFFFFFFF, 0); // Very inefficient! uiTimerManager.setTimer(TimerName::UI_SPECIFIC, kSampleMarkerBlinkTime); - return ActionResult::DEALT_WITH; } @@ -214,11 +259,7 @@ bool AudioClipView::renderSidebar(uint32_t whichRows, RGB image[][kDisplayWidth return true; } -const uint8_t zeroes[] = {0, 0, 0, 0, 0, 0, 0, 0}; -const uint8_t twos[] = {2, 2, 2, 2, 2, 2, 2, 2}; - void AudioClipView::graphicsRoutine() { - if (isUIModeActive(UI_MODE_AUDIO_CLIP_COLLAPSING)) { return; } @@ -230,7 +271,6 @@ void AudioClipView::graphicsRoutine() { || playbackHandler.ticksLeftInCountIn) { newTickSquare = 255; } - // Tempoless or arranger recording else if (!playbackHandler.isEitherClockActive() || (currentPlaybackMode == &arrangement && getCurrentClip()->getCurrentlyRecordingLinearly())) { @@ -242,45 +282,45 @@ void AudioClipView::graphicsRoutine() { needsRenderingDependingOnSubMode(); } } - else { newTickSquare = getTickSquare(); if (getCurrentAudioClip()->getCurrentlyRecordingLinearly()) { needsRenderingDependingOnSubMode(); } - if (newTickSquare < 0 || newTickSquare >= kDisplayWidth) { newTickSquare = 255; } } if (PadLEDs::flashCursor != FLASH_CURSOR_OFF && (newTickSquare != lastTickSquare || mustRedrawTickSquares)) { - uint8_t tickSquares[kDisplayHeight]; memset(tickSquares, newTickSquare, kDisplayHeight); - const uint8_t* colours = getCurrentClip()->getCurrentlyRecordingLinearly() ? twos : zeroes; - PadLEDs::setTickSquares(tickSquares, colours); + std::array coloursArray = {0}; + if (getCurrentClip()->getCurrentlyRecordingLinearly()) { + coloursArray.fill(2); + } - lastTickSquare = newTickSquare; + PadLEDs::setTickSquares(tickSquares, coloursArray.data()); + lastTickSquare = newTickSquare; mustRedrawTickSquares = false; } } void AudioClipView::needsRenderingDependingOnSubMode() { - switch (currentUIMode) { case UI_MODE_HORIZONTAL_SCROLL: case UI_MODE_HORIZONTAL_ZOOM: break; - default: uiNeedsRendering(this, 0xFFFFFFFF, 0); } } +// If you want your specialized button logic (session view, clip view, etc.), +// put that here. Otherwise call the parent: ActionResult AudioClipView::buttonAction(deluge::hid::Button b, bool on, bool inCardRoutine) { using namespace deluge::hid::button; @@ -439,18 +479,13 @@ ActionResult AudioClipView::buttonAction(deluge::hid::Button b, bool on, bool in } ActionResult AudioClipView::padAction(int32_t x, int32_t y, int32_t on) { - - // Edit pad action... if (x < kDisplayWidth) { - if (Buttons::isButtonPressed(deluge::hid::button::TEMPO_ENC)) { if (on) { playbackHandler.grabTempoFromClip(getCurrentAudioClip()); } } - else { - if (sdRoutineLock) { return ActionResult::REMIND_ME_OUTSIDE_CARD_ROUTINE; } @@ -458,63 +493,87 @@ ActionResult AudioClipView::padAction(int32_t x, int32_t y, int32_t on) { // Maybe go to SoundEditor ActionResult soundEditorResult = soundEditor.potentialShortcutPadAction(x, y, on); if (soundEditorResult != ActionResult::NOT_DEALT_WITH) { - if (soundEditorResult == ActionResult::DEALT_WITH) { endMarkerVisible = false; + startMarkerVisible = false; uiTimerManager.unsetTimer(TimerName::UI_SPECIFIC); uiNeedsRendering(this, 0xFFFFFFFF, 0); } - return soundEditorResult; } - else if (on && !currentUIMode) { - AudioClip* clip = getCurrentAudioClip(); + if (!clip) { + return ActionResult::DEALT_WITH; + } + AudioClip& clipRef = *clip; - int32_t endSquareDisplay = divide_round_negative( - clip->loopLength - currentSong->xScroll[NAVIGATION_CLIP] - 1, - currentSong->xZoom[NAVIGATION_CLIP]); // Rounds it well down, so we get the "final square" kinda... + int32_t endSquareDisplay = + divide_round_negative(clipRef.loopLength - currentSong->xScroll[NAVIGATION_CLIP] - 1, + currentSong->xZoom[NAVIGATION_CLIP]); - // Marker already visible - if (endMarkerVisible) { + int32_t startSquareDisplay = divide_round_negative(0 - currentSong->xScroll[NAVIGATION_CLIP], + currentSong->xZoom[NAVIGATION_CLIP]); - // If tapped on the marker itself again, make it invisible - if (x == endSquareDisplay) { - if (blinkOn) { - uiNeedsRendering(this, 0xFFFFFFFF, 0); - } - uiTimerManager.unsetTimer(TimerName::UI_SPECIFIC); + // =========== Handling END marker ============= + if (endMarkerVisible) { + // If user taps the same or adjacent end marker column => toggle off + if (x == endSquareDisplay || x == startSquareDisplay) { endMarkerVisible = false; + uiTimerManager.unsetTimer(TimerName::UI_SPECIFIC); + uiNeedsRendering(this, 0xFFFFFFFF, 0); } - - // Otherwise, move it else { - Sample* sample = getSample(); if (sample) { - - // Ok, move the marker! int32_t newLength = (x + 1) * currentSong->xZoom[NAVIGATION_CLIP] + currentSong->xScroll[NAVIGATION_CLIP]; - int32_t oldLength = clip->loopLength; - uint64_t oldLengthSamples = clip->sampleHolder.getDurationInSamples(true); - changeUnderlyingSampleLength(clip, sample, newLength, oldLength, oldLengthSamples); - - goto needRendering; + int32_t oldLength = clipRef.loopLength; + uint64_t oldLengthSamples = clipRef.sampleHolder.getDurationInSamples(true); + changeUnderlyingSampleLength(clipRef, sample, newLength, oldLength, oldLengthSamples); + uiNeedsRendering(this, 0xFFFFFFFF, 0); + } + } + } + // =========== Handling START marker ============= + else if (startMarkerVisible) { + if (x == startSquareDisplay || x == endSquareDisplay) { + startMarkerVisible = false; // Toggle start marker off + uiTimerManager.unsetTimer(TimerName::UI_SPECIFIC); + uiNeedsRendering(this, 0xFFFFFFFF, 0); + } + else { + Sample* sample = getSample(); + if (sample) { + int32_t newStartTicks = + x * currentSong->xZoom[NAVIGATION_CLIP] + currentSong->xScroll[NAVIGATION_CLIP]; + int32_t oldLength = clipRef.loopLength; + uint64_t oldLengthSamples = clipRef.sampleHolder.getDurationInSamples(true); + changeUnderlyingSampleStart(clipRef, sample, newStartTicks, oldLength, oldLengthSamples); + uiNeedsRendering(this, 0xFFFFFFFF, 0); } } } - - // Or, marker not already visible else { + // No marker is visible. Are we near the end or start? if (x == endSquareDisplay || x == endSquareDisplay + 1) { endMarkerVisible = true; -needRendering: - uiTimerManager.setTimer(TimerName::UI_SPECIFIC, kSampleMarkerBlinkTime); + startMarkerVisible = false; blinkOn = true; + uiTimerManager.setTimer(TimerName::UI_SPECIFIC, kSampleMarkerBlinkTime); uiNeedsRendering(this, 0xFFFFFFFF, 0); } + else if (x == startSquareDisplay) { + + // WIP: Allow the user to trim from the start of the audio clip + if (runtimeFeatureSettings.get(RuntimeFeatureSettingType::TrimFromStartOfAudioClip)) { + startMarkerVisible = true; + endMarkerVisible = false; + blinkOn = true; + uiTimerManager.setTimer(TimerName::UI_SPECIFIC, kSampleMarkerBlinkTime); + uiNeedsRendering(this, 0xFFFFFFFF, 0); + } + } } } } @@ -530,80 +589,44 @@ ActionResult AudioClipView::padAction(int32_t x, int32_t y, int32_t on) { return ActionResult::DEALT_WITH; } } - return ActionResult::DEALT_WITH; } -void AudioClipView::changeUnderlyingSampleLength(AudioClip* clip, const Sample* sample, int32_t newLength, + +// ----------- "End" pointer logic ----------- +void AudioClipView::changeUnderlyingSampleLength(AudioClip& clip, const Sample* sample, int32_t newLength, int32_t oldLength, uint64_t oldLengthSamples) const { uint64_t* valueToChange; int64_t newEndPosSamples; uint64_t newLengthSamples = - (uint64_t)(oldLengthSamples * newLength + (oldLength >> 1)) / (uint32_t)oldLength; // Rounded - // AudioClip reversed - if (clip->sampleControls.reversed) { - - newEndPosSamples = clip->sampleHolder.getEndPos(true) - newLengthSamples; - - // If the end pos is very close to the end pos marked in the audio file, assume some - // rounding happened along the way and just go with the original - if (sample->fileLoopStartSamples) { - int64_t distanceFromFileEndMarker = newEndPosSamples - (uint64_t)sample->fileLoopStartSamples; - if (distanceFromFileEndMarker < 0) { - distanceFromFileEndMarker = -distanceFromFileEndMarker; // abs - } - if (distanceFromFileEndMarker < 10) { - newEndPosSamples = sample->fileLoopStartSamples; - } - } - - // Or if very close to actual wave start... - { - int64_t distanceFromFileEndMarker = newEndPosSamples; - if (distanceFromFileEndMarker < 0) { - distanceFromFileEndMarker = -distanceFromFileEndMarker; // abs - } - if (distanceFromFileEndMarker < 10) { - newEndPosSamples = 0; - } - } + (uint64_t)(oldLengthSamples * (uint64_t)newLength + (oldLength >> 1)) / (uint32_t)oldLength; - // If end pos less than 0, not allowed + // If end pos less than 0, not allowed + if (clip.sampleControls.reversed) { + newEndPosSamples = clip.sampleHolder.endPos - newLengthSamples; if (newEndPosSamples < 0) { newEndPosSamples = 0; } - - valueToChange = &clip->sampleHolder.startPos; + valueToChange = &clip.sampleHolder.startPos; } - // AudioClip playing forward else { - newEndPosSamples = clip->sampleHolder.startPos + newLengthSamples; + newEndPosSamples = clip.sampleHolder.startPos + newLengthSamples; + if (newEndPosSamples > sample->lengthInSamples) { + newEndPosSamples = sample->lengthInSamples; + } + valueToChange = &clip.sampleHolder.endPos; - // If the end pos is very close to the end pos marked in the audio file, assume some - // rounding happened along the way and just go with the original - if (sample->fileLoopEndSamples) { - int64_t distanceFromFileEndMarker = newEndPosSamples - (uint64_t)sample->fileLoopEndSamples; + // If the end pos is very close to the end pos marked in the audio file... + if (sample->fileLoopStartSamples) { + int64_t distanceFromFileEndMarker = newEndPosSamples - (uint64_t)sample->fileLoopStartSamples; if (distanceFromFileEndMarker < 0) { - distanceFromFileEndMarker = -distanceFromFileEndMarker; // abs + distanceFromFileEndMarker = -distanceFromFileEndMarker; } if (distanceFromFileEndMarker < 10) { - newEndPosSamples = sample->fileLoopEndSamples; - } - } - - // Or if very close to actual wave length... - { - int64_t distanceFromWaveformEnd = newEndPosSamples - (uint64_t)sample->lengthInSamples; - if (distanceFromWaveformEnd < 0) { - distanceFromWaveformEnd = -distanceFromWaveformEnd; // abs - } - if (distanceFromWaveformEnd < 10) { - newEndPosSamples = sample->lengthInSamples; + newEndPosSamples = sample->fileLoopStartSamples; } } - - valueToChange = &clip->sampleHolder.endPos; } ActionType actionType = @@ -615,8 +638,7 @@ void AudioClipView::changeUnderlyingSampleLength(AudioClip* clip, const Sample* *valueToChange = newEndPosSamples; Action* action = actionLogger.getNewAction(actionType, ActionAddition::NOT_ALLOWED); - currentSong->setClipLength(clip, newLength, action); - + currentSong->setClipLength(&clip, newLength, action); if (action) { if (action->firstConsequence && action->firstConsequence->type == Consequence::CLIP_LENGTH) { ConsequenceClipLength* consequence = (ConsequenceClipLength*)action->firstConsequence; @@ -627,19 +649,70 @@ void AudioClipView::changeUnderlyingSampleLength(AudioClip* clip, const Sample* } } -void AudioClipView::playbackEnded() { +// ----------- "Start" pointer logic ----------- +void AudioClipView::changeUnderlyingSampleStart(AudioClip& clip, const Sample* sample, int32_t newStartTicks, + int32_t oldLength, uint64_t oldLengthSamples) const { + int32_t oldEndTick = oldLength; + int32_t newLengthTicks = oldEndTick - newStartTicks; + if (newLengthTicks < 1) { + newLengthTicks = 1; + } + uint64_t newLengthSamples = + static_cast(oldLengthSamples * newLengthTicks + (oldLength / 2)) / static_cast(oldLength); - // A few reasons we might want to redraw the waveform. If a Sample had only partially recorded, it will have just - // been discarded. Or, if tempoless or arrangement recording, zoom and everything will have just changed - uiNeedsRendering(this, 0xFFFFFFFF, 0); + if (clip.sampleControls.reversed) { + uint64_t oldValue = clip.sampleHolder.endPos; + uint64_t newEndPos = clip.sampleHolder.startPos + newLengthSamples; + if (newEndPos > sample->lengthInSamples) { + newEndPos = sample->lengthInSamples; + } + clip.sampleHolder.endPos = newEndPos; + + ActionType actionType = + (newLengthTicks < oldLength) ? ActionType::CLIP_LENGTH_DECREASE : ActionType::CLIP_LENGTH_INCREASE; + Action* action = actionLogger.getNewAction(actionType, ActionAddition::NOT_ALLOWED); + currentSong->setClipLength(&clip, newLengthTicks, action); + if (action) { + if (action->firstConsequence && action->firstConsequence->type == Consequence::CLIP_LENGTH) { + ConsequenceClipLength* consequence = (ConsequenceClipLength*)action->firstConsequence; + consequence->pointerToMarkerValue = &clip.sampleHolder.endPos; + consequence->markerValueToRevertTo = oldValue; + } + actionLogger.closeAction(actionType); + } + } + else { + uint64_t oldValue = clip.sampleHolder.startPos; + uint64_t newStartPos = clip.sampleHolder.endPos - newLengthSamples; + if ((int64_t)newStartPos < 0) { + newStartPos = 0; + } + clip.sampleHolder.startPos = newStartPos; + + ActionType actionType = + (newLengthTicks < oldLength) ? ActionType::CLIP_LENGTH_DECREASE : ActionType::CLIP_LENGTH_INCREASE; + Action* action = actionLogger.getNewAction(actionType, ActionAddition::NOT_ALLOWED); + currentSong->setClipLength(&clip, newLengthTicks, action); + if (action) { + if (action->firstConsequence && action->firstConsequence->type == Consequence::CLIP_LENGTH) { + ConsequenceClipLength* consequence = (ConsequenceClipLength*)action->firstConsequence; + consequence->pointerToMarkerValue = &clip.sampleHolder.startPos; + consequence->markerValueToRevertTo = oldValue; + } + actionLogger.closeAction(actionType); + } + } } -void AudioClipView::clipNeedsReRendering(Clip* clip) { - if (clip == getCurrentAudioClip()) { +void AudioClipView::playbackEnded() { + uiNeedsRendering(this, 0xFFFFFFFF, 0); +} +void AudioClipView::clipNeedsReRendering(Clip* c) { + if (c == getCurrentAudioClip()) { // Scroll back left if we need to - it's possible that the length just reverted, if recording got aborted. // Ok, coming back to this, it seems it was a bit hacky that I put this in this function... - if (currentSong->xScroll[NAVIGATION_CLIP] >= clip->loopLength) { + if (currentSong->xScroll[NAVIGATION_CLIP] >= c->loopLength) { horizontalScrollForLinearRecording(0); } else { @@ -648,8 +721,8 @@ void AudioClipView::clipNeedsReRendering(Clip* clip) { } } -void AudioClipView::sampleNeedsReRendering(Sample* sample) { - if (sample == getSample()) { +void AudioClipView::sampleNeedsReRendering(Sample* s) { + if (s == getSample()) { uiNeedsRendering(this, 0xFFFFFFFF, 0); } } @@ -663,8 +736,8 @@ void AudioClipView::selectEncoderAction(int8_t offset) { } void AudioClipView::setClipLengthEqualToSampleLength() { - AudioClip* audioClip = getCurrentAudioClip(); - SamplePlaybackGuide guide = audioClip->guide; + AudioClip& audioClip = *getCurrentAudioClip(); + SamplePlaybackGuide guide = audioClip.guide; SampleHolder* sampleHolder = (SampleHolder*)guide.audioFileHolder; if (sampleHolder) { adjustLoopLength(sampleHolder->getLoopLengthAtSystemSampleRate(true)); @@ -684,7 +757,6 @@ void AudioClipView::adjustLoopLength(int32_t newLength) { if (newLength > oldLength) { // If we're still within limits if (newLength <= (uint32_t)kMaxSequenceLength) { - action = lengthenClip(newLength); doReRender: // use getRootUI() in case this is called from audio clip automation view @@ -693,16 +765,10 @@ void AudioClipView::adjustLoopLength(int32_t newLength) { } else if (newLength < oldLength) { if (newLength > 0) { - action = shortenClip(newLength); - // Scroll / zoom as needed if (!scrollLeftIfTooFarRight(newLength)) { - // If this zoom level no longer valid... - if (zoomToMax(true)) { - // editor.displayZoomLevel(true); - } - else { + if (!zoomToMax(true)) { goto doReRender; } } @@ -710,7 +776,6 @@ void AudioClipView::adjustLoopLength(int32_t newLength) { } displayNumberOfBarsAndBeats(newLength, currentSong->xZoom[NAVIGATION_CLIP], false, "LONG"); - if (action) { action->xScrollClip[AFTER] = currentSong->xScroll[NAVIGATION_CLIP]; } @@ -718,12 +783,10 @@ void AudioClipView::adjustLoopLength(int32_t newLength) { } ActionResult AudioClipView::horizontalEncoderAction(int32_t offset) { - // Shift and x pressed - edit length of clip without timestretching if (isNoUIModeActive() && Buttons::isButtonPressed(deluge::hid::button::X_ENC) && Buttons::isShiftButtonPressed()) { return editClipLengthWithoutTimestretching(offset); } - else { // Otherwise, let parent do scrolling and zooming return ClipView::horizontalEncoderAction(offset); @@ -737,27 +800,23 @@ ActionResult AudioClipView::editClipLengthWithoutTimestretching(int32_t offset) return ActionResult::DEALT_WITH; } - // Ok, move the marker! - int32_t oldLength = getCurrentClip()->loopLength; - uint64_t oldLengthSamples = getCurrentAudioClip()->sampleHolder.getDurationInSamples(true); - // If we're not scrolled all the way to the right, go there now - if (scrollRightToEndOfLengthIfNecessary(oldLength)) { + if (scrollRightToEndOfLengthIfNecessary(getCurrentClip()->loopLength)) { return ActionResult::DEALT_WITH; } - // Or if still here, we've already scrolled far-right - if (sdRoutineLock) { return ActionResult::REMIND_ME_OUTSIDE_CARD_ROUTINE; } - Action* action = nullptr; + int32_t oldLength = getCurrentClip()->loopLength; + uint64_t oldLengthSamples = getCurrentAudioClip()->sampleHolder.getDurationInSamples(true); + Action* action = nullptr; uint32_t newLength = changeClipLength(offset, oldLength, action); - AudioClip* audioClip = getCurrentAudioClip(); - SamplePlaybackGuide guide = audioClip->guide; + AudioClip& audioClip = *getCurrentAudioClip(); + SamplePlaybackGuide guide = audioClip.guide; SampleHolder* sampleHolder = (SampleHolder*)guide.audioFileHolder; if (sampleHolder) { Sample* sample = static_cast(sampleHolder->audioFile); @@ -767,7 +826,6 @@ ActionResult AudioClipView::editClipLengthWithoutTimestretching(int32_t offset) } displayNumberOfBarsAndBeats(newLength, currentSong->xZoom[NAVIGATION_CLIP], false, "LONG"); - if (action) { action->xScrollClip[AFTER] = currentSong->xScroll[NAVIGATION_CLIP]; } @@ -788,11 +846,9 @@ ActionResult AudioClipView::verticalEncoderAction(int32_t offset, bool inCardRou } bool AudioClipView::setupScroll(uint32_t oldScroll) { - if (!getCurrentAudioClip()->currentlyScrollableAndZoomable()) { return false; } - return ClipView::setupScroll(oldScroll); } @@ -804,9 +860,7 @@ uint32_t AudioClipView::getMaxLength() { if (endMarkerVisible) { return getCurrentClip()->loopLength + 1; } - else { - return getCurrentClip()->loopLength; - } + return getCurrentClip()->loopLength; } uint32_t AudioClipView::getMaxZoom() { diff --git a/src/deluge/gui/views/audio_clip_view.h b/src/deluge/gui/views/audio_clip_view.h index 2d89243881..61e3f12484 100644 --- a/src/deluge/gui/views/audio_clip_view.h +++ b/src/deluge/gui/views/audio_clip_view.h @@ -67,10 +67,15 @@ class AudioClipView final : public ClipView, public ClipMinder { void needsRenderingDependingOnSubMode(); int32_t lastTickSquare; bool mustRedrawTickSquares; - bool endMarkerVisible; + + bool endMarkerVisible; // True if user is currently adjusting the clip's end + bool startMarkerVisible; // True if user is currently adjusting the clip's start bool blinkOn; - void changeUnderlyingSampleLength(AudioClip* clip, const Sample* sample, int32_t newLength, int32_t oldLength, + + void changeUnderlyingSampleLength(AudioClip& clip, const Sample* sample, int32_t newLength, int32_t oldLength, uint64_t oldLengthSamples) const; + void changeUnderlyingSampleStart(AudioClip& clip, const Sample* sample, int32_t newStartTicks, int32_t oldLength, + uint64_t oldLengthSamples) const; }; extern AudioClipView audioClipView; diff --git a/src/deluge/gui/waveform/waveform_renderer.cpp b/src/deluge/gui/waveform/waveform_renderer.cpp index 397f0b9794..083e4d36b3 100644 --- a/src/deluge/gui/waveform/waveform_renderer.cpp +++ b/src/deluge/gui/waveform/waveform_renderer.cpp @@ -619,15 +619,12 @@ void WaveformRenderer::drawColBar(int32_t xDisplay, int32_t min24, int32_t max24 colourAmount = ((howMuchThisSquare * brightness) >> 8); } - for (int32_t c = 0; c < RGB::size(); c++) { - int32_t valueHere = (colourAmount * colourAmount) >> 8; + int32_t valueHere = (colourAmount * colourAmount) >> 8; + RGB color = rgb.has_value() + ? rgb.value().transform([valueHere](auto channel) { return (valueHere * channel) >> 8; }) + : RGB::monochrome(valueHere); - if (rgb.has_value()) { - valueHere = (valueHere * rgb.value()[c]) >> 8; - } - - thisImage[y + (kDisplayHeight >> 1)][xDisplay][c] = valueHere; - } + thisImage[y + (kDisplayHeight >> 1)][xDisplay] = color; } } diff --git a/src/deluge/hid/led/pad_leds.cpp b/src/deluge/hid/led/pad_leds.cpp index 99b07f7d60..7efc0800e8 100644 --- a/src/deluge/hid/led/pad_leds.cpp +++ b/src/deluge/hid/led/pad_leds.cpp @@ -292,9 +292,7 @@ RGB prepareColour(int32_t x, int32_t y, RGB colourSource) { } void writeToSideBar(uint8_t sideBarX, uint8_t yDisplay, uint8_t red, uint8_t green, uint8_t blue) { - image[yDisplay][sideBarX + kDisplayWidth][0] = red; - image[yDisplay][sideBarX + kDisplayWidth][1] = green; - image[yDisplay][sideBarX + kDisplayWidth][2] = blue; + image[yDisplay][sideBarX + kDisplayWidth] = RGB(red, green, blue); } void setupInstrumentClipCollapseAnimation(bool collapsingOutOfClipMinder) { diff --git a/src/deluge/io/midi/sysex.cpp b/src/deluge/io/midi/sysex.cpp index 3029a9652d..f4b2aa1b34 100644 --- a/src/deluge/io/midi/sysex.cpp +++ b/src/deluge/io/midi/sysex.cpp @@ -161,9 +161,7 @@ void Debug::loadPacketReceived(uint8_t* data, int32_t len) { uint32_t pad = (18 * 8 * pos) / (load_bufsize - 0xffff); uint8_t col = pad % 18; uint8_t row = pad / 18; - PadLEDs::image[row][col][0] = (255 / 7) * row; - PadLEDs::image[row][col][1] = 0; - PadLEDs::image[row][col][2] = 255 - (255 / 7) * row; + PadLEDs::image[row][col] = RGB((255 / 7) * row, 0, 255 - (255 / 7) * row); if ((pos / 512) % 16 == 0) { PadLEDs::sendOutMainPadColours(); PadLEDs::sendOutSidebarColours(); diff --git a/src/deluge/model/settings/runtime_feature_settings.cpp b/src/deluge/model/settings/runtime_feature_settings.cpp index fb13e7f5e7..cab2db9451 100644 --- a/src/deluge/model/settings/runtime_feature_settings.cpp +++ b/src/deluge/model/settings/runtime_feature_settings.cpp @@ -194,6 +194,11 @@ void RuntimeFeatureSettings::init() { SetupOnOffSetting(settings[RuntimeFeatureSettingType::HorizontalMenus], STRING_FOR_COMMUNITY_FEATURE_HORIZONTAL_MENUS, "enableHorizontalMenus", RuntimeFeatureStateToggle::On); + + // Trim from start of audio clip + SetupOnOffSetting(settings[RuntimeFeatureSettingType::TrimFromStartOfAudioClip], + STRING_FOR_COMMUNITY_FEATURE_TRIM_FROM_START_OF_AUDIO_CLIP, "trimFromStartOfAudioClip", + RuntimeFeatureStateToggle::On); } void RuntimeFeatureSettings::readSettingsFromFile() { diff --git a/src/deluge/model/settings/runtime_feature_settings.h b/src/deluge/model/settings/runtime_feature_settings.h index 0a66dcad58..b69b7408a9 100644 --- a/src/deluge/model/settings/runtime_feature_settings.h +++ b/src/deluge/model/settings/runtime_feature_settings.h @@ -65,6 +65,7 @@ enum RuntimeFeatureSettingType : uint32_t { EnableGridViewLoopPads, AlternativeTapTempoBehaviour, HorizontalMenus, + TrimFromStartOfAudioClip, MaxElement // Keep as boundary };