From 3449e075d8a9ae98abd1315898752ffe6839abf0 Mon Sep 17 00:00:00 2001 From: Rob MacDonald Date: Wed, 15 Jan 2025 17:31:25 +0000 Subject: [PATCH 1/3] sample_marker_editor: Add start marker editing and improve waveform rendering - Add start marker functionality with blue color indicator - Implement waveform rendering between start and end markers - Fix clip playback state during marker edits - Clean up code structure and improve variable naming --- src/deluge/gui/views/audio_clip_view.cpp | 217 ++++++++++++++++++++--- src/deluge/gui/views/audio_clip_view.h | 5 +- 2 files changed, 192 insertions(+), 30 deletions(-) diff --git a/src/deluge/gui/views/audio_clip_view.cpp b/src/deluge/gui/views/audio_clip_view.cpp index cd9eb089df..2f93409f80 100644 --- a/src/deluge/gui/views/audio_clip_view.cpp +++ b/src/deluge/gui/views/audio_clip_view.cpp @@ -1,5 +1,5 @@ /* - * Copyright © 2019-2023 Synthstrom Audible Limited + * Copyright 2019-2023 Synthstrom Audible Limited * * This file is part of The Synthstrom Audible Deluge Firmware. * @@ -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,40 +106,49 @@ bool AudioClipView::renderMainPads(uint32_t whichRows, RGB image[][kDisplayWidth return true; } + AudioClip* clip = getCurrentAudioClip(); int32_t endSquareDisplay = divide_round_negative( - getCurrentAudioClip()->loopLength - currentSong->xScroll[NAVIGATION_CLIP] - 1, + clip->loopLength - currentSong->xScroll[NAVIGATION_CLIP] - 1, currentSong->xZoom[NAVIGATION_CLIP]); // Rounds it well down, so we get the "final square" kinda... + int32_t startSquareDisplay = divide_round_negative( + clip->startPos - currentSong->xScroll[NAVIGATION_CLIP], + currentSong->xZoom[NAVIGATION_CLIP]); + // If no Sample, just clear display if (!getSample()) { - for (int32_t y = 0; y < kDisplayHeight; y++) { memset(image[y], 0, kDisplayWidth * 3); } } - // Or if yes Sample... else { - - SampleRecorder* recorder = getCurrentAudioClip()->recorder; + SampleRecorder* recorder = clip->recorder; int64_t xScrollSamples; int64_t xZoomSamples; - getCurrentAudioClip()->getScrollAndZoomInSamples( + clip->getScrollAndZoomInSamples( currentSong->xScroll[NAVIGATION_CLIP], currentSong->xZoom[NAVIGATION_CLIP], &xScrollSamples, &xZoomSamples); - RGB rgb = getCurrentAudioClip()->getColour(); + RGB rgb = clip->getColour(); + + int32_t visibleWaveformXStart = 0; + if (startMarkerVisible && blinkOn && startSquareDisplay >= 0) { + visibleWaveformXStart = startSquareDisplay + 1; + } int32_t visibleWaveformXEnd = endSquareDisplay + 1; if (endMarkerVisible && blinkOn) { visibleWaveformXEnd--; } + int32_t xEnd = std::min(kDisplayWidth, visibleWaveformXEnd); + int32_t xStart = std::max(0, visibleWaveformXStart); bool success = waveformRenderer.renderFullScreen(getSample(), xScrollSamples, xZoomSamples, image, - &getCurrentAudioClip()->renderData, recorder, rgb, - getCurrentAudioClip()->sampleControls.reversed, xEnd); + &clip->renderData, recorder, rgb, + clip->sampleControls.reversed, xEnd, xStart); // If card being accessed and waveform would have to be re-examined, come back later if (!success && image == PadLEDs::image) { @@ -148,29 +158,34 @@ bool AudioClipView::renderMainPads(uint32_t whichRows, RGB image[][kDisplayWidth } if (drawUndefinedArea) { - for (int32_t y = 0; y < kDisplayHeight; y++) { + // Draw start marker in blue + if (startSquareDisplay >= 0 && startSquareDisplay < kDisplayWidth) { + if (startMarkerVisible && blinkOn) { + image[y][startSquareDisplay][0] = 0; + image[y][startSquareDisplay][1] = 0; + image[y][startSquareDisplay][2] = 255; + } + } - if (endSquareDisplay < kDisplayWidth) { - - if (endSquareDisplay >= 0) { - if (endMarkerVisible && blinkOn) { - image[y][endSquareDisplay][0] = 255; - image[y][endSquareDisplay][1] = 0; - image[y][endSquareDisplay][2] = 0; - } + // Draw end marker in red + if (endSquareDisplay < kDisplayWidth && endSquareDisplay >= 0) { + if (endMarkerVisible && blinkOn) { + image[y][endSquareDisplay][0] = 255; + image[y][endSquareDisplay][1] = 0; + image[y][endSquareDisplay][2] = 0; } + } + // Fill undefined area after end marker + if (endSquareDisplay < kDisplayWidth) { int32_t xDisplay = endSquareDisplay + 1; - - if (xDisplay >= kDisplayWidth) { - continue; - } - else if (xDisplay < 0) { - xDisplay = 0; + if (xDisplay < kDisplayWidth) { + if (xDisplay < 0) { + xDisplay = 0; + } + std::fill(&image[y][xDisplay], &image[y][xDisplay] + (kDisplayWidth - xDisplay), colours::grey); } - - std::fill(&image[y][xDisplay], &image[y][xDisplay] + (kDisplayWidth - xDisplay), colours::grey); } } } @@ -410,6 +425,7 @@ ActionResult AudioClipView::buttonAction(deluge::hid::Button b, bool on, bool in display->displayPopup(deluge::l10n::get(deluge::l10n::String::STRING_FOR_CLIP_CLEARED)); } endMarkerVisible = false; + startMarkerVisible = false; uiTimerManager.unsetTimer(TimerName::UI_SPECIFIC); uiNeedsRendering(this, 0xFFFFFFFF, 0); } @@ -423,8 +439,9 @@ ActionResult AudioClipView::buttonAction(deluge::hid::Button b, bool on, bool in if (result != ActionResult::REMIND_ME_OUTSIDE_CARD_ROUTINE) { deactivateMarkerIfNecessary: - if (endMarkerVisible) { + if (endMarkerVisible || startMarkerVisible) { endMarkerVisible = false; + startMarkerVisible = false; if (getCurrentUI() == this) { uiTimerManager.unsetTimer(TimerName::UI_SPECIFIC); } @@ -461,6 +478,7 @@ ActionResult AudioClipView::padAction(int32_t x, int32_t y, int32_t on) { if (soundEditorResult == ActionResult::DEALT_WITH) { endMarkerVisible = false; + startMarkerVisible = false; uiTimerManager.unsetTimer(TimerName::UI_SPECIFIC); uiNeedsRendering(this, 0xFFFFFFFF, 0); } @@ -476,6 +494,10 @@ ActionResult AudioClipView::padAction(int32_t x, int32_t y, int32_t on) { clip->loopLength - currentSong->xScroll[NAVIGATION_CLIP] - 1, currentSong->xZoom[NAVIGATION_CLIP]); // Rounds it well down, so we get the "final square" kinda... + int32_t startSquareDisplay = divide_round_negative( + clip->startPos - currentSong->xScroll[NAVIGATION_CLIP], + currentSong->xZoom[NAVIGATION_CLIP]); + // Marker already visible if (endMarkerVisible) { @@ -506,10 +528,47 @@ ActionResult AudioClipView::padAction(int32_t x, int32_t y, int32_t on) { } } + // Or, start marker already visible + else if (startMarkerVisible) { + + // If tapped on the marker itself again, make it invisible + if (x == startSquareDisplay) { + if (blinkOn) { + uiNeedsRendering(this, 0xFFFFFFFF, 0); + } + uiTimerManager.unsetTimer(TimerName::UI_SPECIFIC); + startMarkerVisible = false; + } + + // Otherwise, move it + else { + + Sample* sample = getSample(); + if (sample) { + + // Ok, move the marker! + int32_t newStartPos = + x * currentSong->xZoom[NAVIGATION_CLIP] + currentSong->xScroll[NAVIGATION_CLIP]; + int32_t oldStartPos = clip->startPos; + uint64_t oldStartPosSamples = clip->sampleHolder.getStartPos(true); + changeUnderlyingSampleStartPos(clip, sample, newStartPos, oldStartPos, oldStartPosSamples); + + goto needRendering; + } + } + } + // Or, marker not already visible else { if (x == endSquareDisplay || x == endSquareDisplay + 1) { endMarkerVisible = true; +needRendering: + uiTimerManager.setTimer(TimerName::UI_SPECIFIC, kSampleMarkerBlinkTime); + blinkOn = true; + uiNeedsRendering(this, 0xFFFFFFFF, 0); + } + else if (x == startSquareDisplay || x == startSquareDisplay + 1) { + startMarkerVisible = true; needRendering: uiTimerManager.setTimer(TimerName::UI_SPECIFIC, kSampleMarkerBlinkTime); blinkOn = true; @@ -533,6 +592,7 @@ ActionResult AudioClipView::padAction(int32_t x, int32_t y, int32_t on) { return ActionResult::DEALT_WITH; } + void AudioClipView::changeUnderlyingSampleLength(AudioClip* clip, const Sample* sample, int32_t newLength, int32_t oldLength, uint64_t oldLengthSamples) const { uint64_t* valueToChange; @@ -627,6 +687,100 @@ void AudioClipView::changeUnderlyingSampleLength(AudioClip* clip, const Sample* } } +void AudioClipView::changeUnderlyingSampleStartPos(AudioClip* clip, const Sample* sample, int32_t newStartPos, + int32_t oldStartPos, uint64_t oldStartPosSamples) const { + uint64_t* valueToChange; + int64_t newStartPosSamples; + + uint64_t newStartPosSamplesValue = + (uint64_t)(oldStartPosSamples * newStartPos + (oldStartPos >> 1)) / (uint32_t)oldStartPos; // Rounded + // AudioClip reversed + if (clip->sampleControls.reversed) { + + newStartPosSamples = clip->sampleHolder.getEndPos(true) - newStartPosSamplesValue; + + // If the start pos is very close to the start pos marked in the audio file, assume some + // rounding happened along the way and just go with the original + if (sample->fileLoopStartSamples) { + int64_t distanceFromFileStartMarker = newStartPosSamples - (uint64_t)sample->fileLoopStartSamples; + if (distanceFromFileStartMarker < 0) { + distanceFromFileStartMarker = -distanceFromFileStartMarker; // abs + } + if (distanceFromFileStartMarker < 10) { + newStartPosSamples = sample->fileLoopStartSamples; + } + } + + // Or if very close to actual wave start... + { + int64_t distanceFromFileStartMarker = newStartPosSamples; + if (distanceFromFileStartMarker < 0) { + distanceFromFileStartMarker = -distanceFromFileStartMarker; // abs + } + if (distanceFromFileStartMarker < 10) { + newStartPosSamples = 0; + } + } + + // If start pos less than 0, not allowed + if (newStartPosSamples < 0) { + newStartPosSamples = 0; + } + + valueToChange = &clip->sampleHolder.endPos; + } + + // AudioClip playing forward + else { + newStartPosSamples = newStartPosSamplesValue; + + // If the start pos is very close to the start pos marked in the audio file, assume some + // rounding happened along the way and just go with the original + if (sample->fileLoopStartSamples) { + int64_t distanceFromFileStartMarker = newStartPosSamples - (uint64_t)sample->fileLoopStartSamples; + if (distanceFromFileStartMarker < 0) { + distanceFromFileStartMarker = -distanceFromFileStartMarker; // abs + } + if (distanceFromFileStartMarker < 10) { + newStartPosSamples = sample->fileLoopStartSamples; + } + } + + // Or if very close to actual wave start... + { + int64_t distanceFromFileStartMarker = newStartPosSamples; + if (distanceFromFileStartMarker < 0) { + distanceFromFileStartMarker = -distanceFromFileStartMarker; // abs + } + if (distanceFromFileStartMarker < 10) { + newStartPosSamples = 0; + } + } + + valueToChange = &clip->sampleHolder.startPos; + } + + ActionType actionType = + (newStartPos < oldStartPos) ? ActionType::CLIP_START_DECREASE : ActionType::CLIP_START_INCREASE; + + // Change sample start-pos value. Must do this before calling setClipStartPos(), which will end + // up reading this value. + uint64_t oldValue = *valueToChange; + *valueToChange = newStartPosSamples; + + Action* action = actionLogger.getNewAction(actionType, ActionAddition::NOT_ALLOWED); + currentSong->setClipStartPos(clip, newStartPos, action); + + if (action) { + if (action->firstConsequence && action->firstConsequence->type == Consequence::CLIP_START) { + ConsequenceClipStart* consequence = (ConsequenceClipStart*)action->firstConsequence; + consequence->pointerToMarkerValue = valueToChange; + consequence->markerValueToRevertTo = oldValue; + } + actionLogger.closeAction(actionType); + } +} + void AudioClipView::playbackEnded() { // A few reasons we might want to redraw the waveform. If a Sample had only partially recorded, it will have just @@ -712,7 +866,12 @@ void AudioClipView::adjustLoopLength(int32_t newLength) { displayNumberOfBarsAndBeats(newLength, currentSong->xZoom[NAVIGATION_CLIP], false, "LONG"); if (action) { - action->xScrollClip[AFTER] = currentSong->xScroll[NAVIGATION_CLIP]; + if (action->firstConsequence && action->firstConsequence->type == Consequence::CLIP_LENGTH) { + ConsequenceClipLength* consequence = (ConsequenceClipLength*)action->firstConsequence; + consequence->pointerToMarkerValue = &getCurrentClip()->loopLength; + consequence->markerValueToRevertTo = oldLength; + } + actionLogger.closeAction(ActionType::CLIP_LENGTH); } } } diff --git a/src/deluge/gui/views/audio_clip_view.h b/src/deluge/gui/views/audio_clip_view.h index 2d89243881..ec672a4806 100644 --- a/src/deluge/gui/views/audio_clip_view.h +++ b/src/deluge/gui/views/audio_clip_view.h @@ -1,5 +1,5 @@ /* - * Copyright © 2019-2023 Synthstrom Audible Limited + * Copyright 2019-2023 Synthstrom Audible Limited * * This file is part of The Synthstrom Audible Deluge Firmware. * @@ -68,9 +68,12 @@ class AudioClipView final : public ClipView, public ClipMinder { int32_t lastTickSquare; bool mustRedrawTickSquares; bool endMarkerVisible; + bool startMarkerVisible; bool blinkOn; void changeUnderlyingSampleLength(AudioClip* clip, const Sample* sample, int32_t newLength, int32_t oldLength, uint64_t oldLengthSamples) const; + void changeUnderlyingSampleStartPos(AudioClip* clip, const Sample* sample, int32_t newStartPos, int32_t oldStartPos, + uint64_t oldStartPosSamples) const; }; extern AudioClipView audioClipView; From a4eab9af2b9e80d8b73b903e71ec319d921a8b95 Mon Sep 17 00:00:00 2001 From: Rob MacDonald Date: Wed, 15 Jan 2025 21:18:55 +0000 Subject: [PATCH 2/3] fix(audio): Improve audio clip start position handling - Fix duplicate needRendering label - Add proper bounds checking for start positions - Improve reversed clip handling - Fix action/undo system for start position changes --- src/deluge/gui/views/audio_clip_view.cpp | 79 ++++++++---------------- src/deluge/model/clip/audio_clip.h | 4 +- 2 files changed, 28 insertions(+), 55 deletions(-) diff --git a/src/deluge/gui/views/audio_clip_view.cpp b/src/deluge/gui/views/audio_clip_view.cpp index 2f93409f80..90b612f97f 100644 --- a/src/deluge/gui/views/audio_clip_view.cpp +++ b/src/deluge/gui/views/audio_clip_view.cpp @@ -144,11 +144,11 @@ bool AudioClipView::renderMainPads(uint32_t whichRows, RGB image[][kDisplayWidth } int32_t xEnd = std::min(kDisplayWidth, visibleWaveformXEnd); - int32_t xStart = std::max(0, visibleWaveformXStart); + int32_t xStart = (visibleWaveformXStart < 0) ? 0 : visibleWaveformXStart; bool success = waveformRenderer.renderFullScreen(getSample(), xScrollSamples, xZoomSamples, image, &clip->renderData, recorder, rgb, - clip->sampleControls.reversed, xEnd, xStart); + clip->sampleControls.reversed, xEnd); // If card being accessed and waveform would have to be re-examined, come back later if (!success && image == PadLEDs::image) { @@ -550,7 +550,7 @@ ActionResult AudioClipView::padAction(int32_t x, int32_t y, int32_t on) { int32_t newStartPos = x * currentSong->xZoom[NAVIGATION_CLIP] + currentSong->xScroll[NAVIGATION_CLIP]; int32_t oldStartPos = clip->startPos; - uint64_t oldStartPosSamples = clip->sampleHolder.getStartPos(true); + uint64_t oldStartPosSamples = clip->sampleHolder.startPos; changeUnderlyingSampleStartPos(clip, sample, newStartPos, oldStartPos, oldStartPosSamples); goto needRendering; @@ -562,14 +562,12 @@ ActionResult AudioClipView::padAction(int32_t x, int32_t y, int32_t on) { else { if (x == endSquareDisplay || x == endSquareDisplay + 1) { endMarkerVisible = true; -needRendering: uiTimerManager.setTimer(TimerName::UI_SPECIFIC, kSampleMarkerBlinkTime); blinkOn = true; uiNeedsRendering(this, 0xFFFFFFFF, 0); } else if (x == startSquareDisplay || x == startSquareDisplay + 1) { startMarkerVisible = true; -needRendering: uiTimerManager.setTimer(TimerName::UI_SPECIFIC, kSampleMarkerBlinkTime); blinkOn = true; uiNeedsRendering(this, 0xFFFFFFFF, 0); @@ -694,67 +692,41 @@ void AudioClipView::changeUnderlyingSampleStartPos(AudioClip* clip, const Sample uint64_t newStartPosSamplesValue = (uint64_t)(oldStartPosSamples * newStartPos + (oldStartPos >> 1)) / (uint32_t)oldStartPos; // Rounded + + // Ensure start position doesn't exceed clip length + if (newStartPosSamplesValue >= clip->sampleHolder.getDurationInSamples(true)) { + newStartPosSamplesValue = clip->sampleHolder.getDurationInSamples(true) - 1; + } + // AudioClip reversed if (clip->sampleControls.reversed) { newStartPosSamples = clip->sampleHolder.getEndPos(true) - newStartPosSamplesValue; - // If the start pos is very close to the start pos marked in the audio file, assume some - // rounding happened along the way and just go with the original - if (sample->fileLoopStartSamples) { - int64_t distanceFromFileStartMarker = newStartPosSamples - (uint64_t)sample->fileLoopStartSamples; - if (distanceFromFileStartMarker < 0) { - distanceFromFileStartMarker = -distanceFromFileStartMarker; // abs - } - if (distanceFromFileStartMarker < 10) { - newStartPosSamples = sample->fileLoopStartSamples; - } - } - - // Or if very close to actual wave start... - { - int64_t distanceFromFileStartMarker = newStartPosSamples; - if (distanceFromFileStartMarker < 0) { - distanceFromFileStartMarker = -distanceFromFileStartMarker; // abs - } - if (distanceFromFileStartMarker < 10) { - newStartPosSamples = 0; - } + // If very close to actual wave start... + if (newStartPosSamples < 10) { + newStartPosSamples = 0; } - // If start pos less than 0, not allowed + // If end pos less than 0, not allowed if (newStartPosSamples < 0) { newStartPosSamples = 0; } - valueToChange = &clip->sampleHolder.endPos; + valueToChange = &clip->sampleHolder.startPos; } - // AudioClip playing forward else { newStartPosSamples = newStartPosSamplesValue; - // If the start pos is very close to the start pos marked in the audio file, assume some - // rounding happened along the way and just go with the original - if (sample->fileLoopStartSamples) { - int64_t distanceFromFileStartMarker = newStartPosSamples - (uint64_t)sample->fileLoopStartSamples; - if (distanceFromFileStartMarker < 0) { - distanceFromFileStartMarker = -distanceFromFileStartMarker; // abs - } - if (distanceFromFileStartMarker < 10) { - newStartPosSamples = sample->fileLoopStartSamples; - } + // If very close to actual wave start... + if (newStartPosSamples < 10) { + newStartPosSamples = 0; } - // Or if very close to actual wave start... - { - int64_t distanceFromFileStartMarker = newStartPosSamples; - if (distanceFromFileStartMarker < 0) { - distanceFromFileStartMarker = -distanceFromFileStartMarker; // abs - } - if (distanceFromFileStartMarker < 10) { - newStartPosSamples = 0; - } + // If start pos greater than length, not allowed + if (newStartPosSamples >= sample->lengthInSamples) { + newStartPosSamples = sample->lengthInSamples - 1; } valueToChange = &clip->sampleHolder.startPos; @@ -763,14 +735,12 @@ void AudioClipView::changeUnderlyingSampleStartPos(AudioClip* clip, const Sample ActionType actionType = (newStartPos < oldStartPos) ? ActionType::CLIP_START_DECREASE : ActionType::CLIP_START_INCREASE; - // Change sample start-pos value. Must do this before calling setClipStartPos(), which will end - // up reading this value. + // Store old value for undo uint64_t oldValue = *valueToChange; *valueToChange = newStartPosSamples; + // Create action for undo/redo Action* action = actionLogger.getNewAction(actionType, ActionAddition::NOT_ALLOWED); - currentSong->setClipStartPos(clip, newStartPos, action); - if (action) { if (action->firstConsequence && action->firstConsequence->type == Consequence::CLIP_START) { ConsequenceClipStart* consequence = (ConsequenceClipStart*)action->firstConsequence; @@ -779,6 +749,9 @@ void AudioClipView::changeUnderlyingSampleStartPos(AudioClip* clip, const Sample } actionLogger.closeAction(actionType); } + + // Update clip length and playback bounds + clip->setupPlaybackBounds(); } void AudioClipView::playbackEnded() { @@ -868,7 +841,7 @@ void AudioClipView::adjustLoopLength(int32_t newLength) { if (action) { if (action->firstConsequence && action->firstConsequence->type == Consequence::CLIP_LENGTH) { ConsequenceClipLength* consequence = (ConsequenceClipLength*)action->firstConsequence; - consequence->pointerToMarkerValue = &getCurrentClip()->loopLength; + consequence->pointerToMarkerValue = (uint64_t*)&getCurrentClip()->loopLength; consequence->markerValueToRevertTo = oldLength; } actionLogger.closeAction(ActionType::CLIP_LENGTH); diff --git a/src/deluge/model/clip/audio_clip.h b/src/deluge/model/clip/audio_clip.h index 79dbc79ab2..57c6812ea8 100644 --- a/src/deluge/model/clip/audio_clip.h +++ b/src/deluge/model/clip/audio_clip.h @@ -1,5 +1,5 @@ /* - * Copyright © 2019-2023 Synthstrom Audible Limited + * Copyright 2019-2023 Synthstrom Audible Limited * * This file is part of The Synthstrom Audible Deluge Firmware. * @@ -84,7 +84,7 @@ class AudioClip final : public Clip { bool willCloneOutputForOverdub() override { return overdubsShouldCloneOutput; } void sampleZoneChanged(ModelStackWithTimelineCounter const* modelStack); int64_t getNumSamplesTilLoop(ModelStackWithTimelineCounter* modelStack); - void setPos(ModelStackWithTimelineCounter* modelStack, int32_t newPos, bool useActualPosForParamManagers) override; + void setPos(ModelStackWithTimelineCounter* modelStack, int32_t startPos, bool useActualPosForParamManagers) override; /// Return true if successfully shifted, as clip cannot be shifted past beginning bool shiftHorizontally(ModelStackWithTimelineCounter* modelStack, int32_t amount, bool shiftAutomation, bool shiftSequenceAndMPE) override; From ade547f72715783a2c475ba0b999c37f0b1e9c16 Mon Sep 17 00:00:00 2001 From: Rob MacDonald Date: Wed, 15 Jan 2025 22:27:34 +0000 Subject: [PATCH 3/3] Fix audio clip position handling by correcting start position access in AudioClipView. --- src/deluge/gui/views/audio_clip_view.cpp | 933 ++++++++++++----------- src/deluge/gui/views/audio_clip_view.h | 2 + 2 files changed, 474 insertions(+), 461 deletions(-) diff --git a/src/deluge/gui/views/audio_clip_view.cpp b/src/deluge/gui/views/audio_clip_view.cpp index 90b612f97f..87ceb30e77 100644 --- a/src/deluge/gui/views/audio_clip_view.cpp +++ b/src/deluge/gui/views/audio_clip_view.cpp @@ -1,9 +1,9 @@ /* - * Copyright 2019-2023 Synthstrom Audible Limited + * Copyright 2019-2023 Synthstrom Audable Limited * - * This file is part of The Synthstrom Audible Deluge Firmware. + * This file is part of The Synthstrom Audable Deluge Firmware. * - * The Synthstrom Audible Deluge Firmware is free software: you can redistribute it and/or modify it under the + * The Synthstrom Audable Deluge Firmware is free software: you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software Foundation, * either version 3 of the License, or (at your option) any later version. * @@ -62,7 +62,7 @@ inline Sample* getSample() { return getCurrentAudioClip()->recorder->sample; } else { - return (Sample*)getCurrentAudioClip()->sampleHolder.audioFile; + return static_cast(getCurrentAudioClip()->sampleHolder.audioFile); } } @@ -112,7 +112,7 @@ bool AudioClipView::renderMainPads(uint32_t whichRows, RGB image[][kDisplayWidth currentSong->xZoom[NAVIGATION_CLIP]); // Rounds it well down, so we get the "final square" kinda... int32_t startSquareDisplay = divide_round_negative( - clip->startPos - currentSong->xScroll[NAVIGATION_CLIP], + clip->sampleHolder.startPos - currentSong->xScroll[NAVIGATION_CLIP], currentSong->xZoom[NAVIGATION_CLIP]); // If no Sample, just clear display @@ -144,12 +144,11 @@ bool AudioClipView::renderMainPads(uint32_t whichRows, RGB image[][kDisplayWidth } int32_t xEnd = std::min(kDisplayWidth, visibleWaveformXEnd); - int32_t xStart = (visibleWaveformXStart < 0) ? 0 : visibleWaveformXStart; + int32_t xStart = std::max(static_cast(0), visibleWaveformXStart); bool success = waveformRenderer.renderFullScreen(getSample(), xScrollSamples, xZoomSamples, image, &clip->renderData, recorder, rgb, - clip->sampleControls.reversed, xEnd); - + clip->sampleControls.reversed); // If card being accessed and waveform would have to be re-examined, come back later if (!success && image == PadLEDs::image) { uiNeedsRendering(this, whichRows, 0); @@ -456,495 +455,507 @@ 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; - } - - // 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(); - - 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 startSquareDisplay = divide_round_negative( - clip->startPos - currentSong->xScroll[NAVIGATION_CLIP], - currentSong->xZoom[NAVIGATION_CLIP]); - - // Marker already visible - if (endMarkerVisible) { - - // If tapped on the marker itself again, make it invisible - if (x == endSquareDisplay) { - if (blinkOn) { - uiNeedsRendering(this, 0xFFFFFFFF, 0); - } - uiTimerManager.unsetTimer(TimerName::UI_SPECIFIC); - endMarkerVisible = false; - } - - // 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; - } - } - } - - // Or, start marker already visible - else if (startMarkerVisible) { - - // If tapped on the marker itself again, make it invisible - if (x == startSquareDisplay) { - if (blinkOn) { - uiNeedsRendering(this, 0xFFFFFFFF, 0); - } - uiTimerManager.unsetTimer(TimerName::UI_SPECIFIC); - startMarkerVisible = false; - } - - // Otherwise, move it - else { - - Sample* sample = getSample(); - if (sample) { - - // Ok, move the marker! - int32_t newStartPos = - x * currentSong->xZoom[NAVIGATION_CLIP] + currentSong->xScroll[NAVIGATION_CLIP]; - int32_t oldStartPos = clip->startPos; - uint64_t oldStartPosSamples = clip->sampleHolder.startPos; - changeUnderlyingSampleStartPos(clip, sample, newStartPos, oldStartPos, oldStartPosSamples); - - goto needRendering; - } - } - } - - // Or, marker not already visible - else { - if (x == endSquareDisplay || x == endSquareDisplay + 1) { - endMarkerVisible = true; - uiTimerManager.setTimer(TimerName::UI_SPECIFIC, kSampleMarkerBlinkTime); - blinkOn = true; - uiNeedsRendering(this, 0xFFFFFFFF, 0); - } - else if (x == startSquareDisplay || x == startSquareDisplay + 1) { - startMarkerVisible = true; - uiTimerManager.setTimer(TimerName::UI_SPECIFIC, kSampleMarkerBlinkTime); - blinkOn = true; - uiNeedsRendering(this, 0xFFFFFFFF, 0); - } - } - } - } - } - else if (x == kDisplayWidth) { - if (isUIModeActive(UI_MODE_HOLDING_SONG_BUTTON)) { - if (sdRoutineLock) { - return ActionResult::REMIND_ME_OUTSIDE_CARD_ROUTINE; - } - if (!on) { - view.activateMacro(y); - } - return ActionResult::DEALT_WITH; - } - } - - return ActionResult::DEALT_WITH; + // Edit pad action... + if (x < kDisplayWidth) { + if (Buttons::isButtonPressed(deluge::hid::button::TEMPO_ENC)) { + if (on) { + playbackHandler.grabTempoFromClip(getCurrentAudioClip()); + } + return ActionResult::DEALT_WITH; + } + + if (sdRoutineLock) { + return ActionResult::REMIND_ME_OUTSIDE_CARD_ROUTINE; + } + + // 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); + } + return soundEditorResult; + } + + AudioClip* clip = getCurrentAudioClip(); + if (!clip) { + return ActionResult::DEALT_WITH; + } + + Sample* sample = static_cast(clip->sampleHolder.audioFile); + if (!sample) { + return ActionResult::DEALT_WITH; + } + + // Calculate display positions + int32_t startSquareDisplay = (clip->sampleHolder.startPos - currentSong->xScroll[NAVIGATION_CLIP]) / currentSong->xZoom[NAVIGATION_CLIP]; + int32_t endSquareDisplay = (clip->sampleHolder.startPos + clip->loopLength - currentSong->xScroll[NAVIGATION_CLIP]) / currentSong->xZoom[NAVIGATION_CLIP]; + + if (on) { + if (endMarkerVisible) { + if (x == endSquareDisplay) { + if (blinkOn) { + uiNeedsRendering(this, 0xFFFFFFFF, 0); + } + uiTimerManager.unsetTimer(TimerName::UI_SPECIFIC); + endMarkerVisible = false; + } + else { + int32_t newLength = x * currentSong->xZoom[NAVIGATION_CLIP] + currentSong->xScroll[NAVIGATION_CLIP] - clip->sampleHolder.startPos; + int32_t oldLength = clip->loopLength; + uint64_t oldLengthSamples = clip->sampleHolder.getDurationInSamples(false); + changeUnderlyingSampleLength(clip, sample, newLength, oldLength, oldLengthSamples); + goto needRendering; + } + } + else if (startMarkerVisible) { + if (x == startSquareDisplay) { + if (blinkOn) { + uiNeedsRendering(this, 0xFFFFFFFF, 0); + } + uiTimerManager.unsetTimer(TimerName::UI_SPECIFIC); + startMarkerVisible = false; + } + else { + int32_t newStartPos = x * currentSong->xZoom[NAVIGATION_CLIP] + currentSong->xScroll[NAVIGATION_CLIP]; + int32_t oldStartPos = clip->sampleHolder.startPos; + uint64_t oldStartPosSamples = clip->sampleHolder.startPos; + changeUnderlyingSampleStartPos(clip, sample, newStartPos, oldStartPos, oldStartPosSamples); + goto needRendering; + } + } + else { + if (x == endSquareDisplay || x == endSquareDisplay + 1) { + endMarkerVisible = true; + goto needRendering; + } + else if (x == startSquareDisplay || x == startSquareDisplay + 1) { + startMarkerVisible = true; + goto needRendering; + } + } + } + } + + return ActionResult::DEALT_WITH; + +needRendering: + uiTimerManager.setTimer(TimerName::UI_SPECIFIC, kSampleMarkerBlinkTime); + blinkOn = true; + uiNeedsRendering(this, 0xFFFFFFFF, 0); + return ActionResult::DEALT_WITH; } -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; - } - } - - // If end pos less than 0, not allowed - if (newEndPosSamples < 0) { - newEndPosSamples = 0; - } - - valueToChange = &clip->sampleHolder.startPos; - } - - // AudioClip playing forward - else { - newEndPosSamples = clip->sampleHolder.startPos + 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->fileLoopEndSamples) { - int64_t distanceFromFileEndMarker = newEndPosSamples - (uint64_t)sample->fileLoopEndSamples; - if (distanceFromFileEndMarker < 0) { - distanceFromFileEndMarker = -distanceFromFileEndMarker; // abs - } - 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; - } - } - - valueToChange = &clip->sampleHolder.endPos; - } - - ActionType actionType = - (newLength < oldLength) ? ActionType::CLIP_LENGTH_DECREASE : ActionType::CLIP_LENGTH_INCREASE; - - // Change sample end-pos value. Must do this before calling setClipLength(), which will end - // up reading this value. - uint64_t oldValue = *valueToChange; - *valueToChange = newEndPosSamples; +ActionResult AudioClipView::horizontalEncoderAction(int32_t offset) { - Action* action = actionLogger.getNewAction(actionType, ActionAddition::NOT_ALLOWED); - currentSong->setClipLength(clip, newLength, action); + // Shift and x pressed - edit length of clip without timestretching + if (isNoUIModeActive() && Buttons::isButtonPressed(deluge::hid::button::X_ENC) && Buttons::isShiftButtonPressed()) { + return editClipLengthWithoutTimestretching(offset); + } - if (action) { - if (action->firstConsequence && action->firstConsequence->type == Consequence::CLIP_LENGTH) { - ConsequenceClipLength* consequence = (ConsequenceClipLength*)action->firstConsequence; - consequence->pointerToMarkerValue = valueToChange; - consequence->markerValueToRevertTo = oldValue; - } - actionLogger.closeAction(actionType); - } + else { + // Otherwise, let parent do scrolling and zooming + return ClipView::horizontalEncoderAction(offset); + } } -void AudioClipView::changeUnderlyingSampleStartPos(AudioClip* clip, const Sample* sample, int32_t newStartPos, - int32_t oldStartPos, uint64_t oldStartPosSamples) const { - uint64_t* valueToChange; - int64_t newStartPosSamples; - - uint64_t newStartPosSamplesValue = - (uint64_t)(oldStartPosSamples * newStartPos + (oldStartPos >> 1)) / (uint32_t)oldStartPos; // Rounded - - // Ensure start position doesn't exceed clip length - if (newStartPosSamplesValue >= clip->sampleHolder.getDurationInSamples(true)) { - newStartPosSamplesValue = clip->sampleHolder.getDurationInSamples(true) - 1; - } - - // AudioClip reversed - if (clip->sampleControls.reversed) { - - newStartPosSamples = clip->sampleHolder.getEndPos(true) - newStartPosSamplesValue; - - // If very close to actual wave start... - if (newStartPosSamples < 10) { - newStartPosSamples = 0; - } - - // If end pos less than 0, not allowed - if (newStartPosSamples < 0) { - newStartPosSamples = 0; - } - - valueToChange = &clip->sampleHolder.startPos; - } - // AudioClip playing forward - else { - newStartPosSamples = newStartPosSamplesValue; +ActionResult AudioClipView::editClipLengthWithoutTimestretching(int32_t offset) { + // If tempoless recording, don't allow + if (!getCurrentClip()->currentlyScrollableAndZoomable()) { + display->displayPopup(deluge::l10n::get(deluge::l10n::String::STRING_FOR_CANT_EDIT_LENGTH)); + 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)) { + 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; + + uint32_t newLength = changeClipLength(offset, oldLength, action); + + AudioClip* audioClip = getCurrentAudioClip(); + SamplePlaybackGuide guide = audioClip->guide; + SampleHolder* sampleHolder = (SampleHolder*)guide.audioFileHolder; + if (sampleHolder) { + Sample* sample = static_cast(sampleHolder->audioFile); + if (sample) { + changeUnderlyingSampleLength(audioClip, sample, newLength, oldLength, oldLengthSamples); + } + } + + displayNumberOfBarsAndBeats(newLength, currentSong->xZoom[NAVIGATION_CLIP], false, "LONG"); + + if (action) { + action->xScrollClip[AFTER] = currentSong->xScroll[NAVIGATION_CLIP]; + } + return ActionResult::DEALT_WITH; +} - // If very close to actual wave start... - if (newStartPosSamples < 10) { - newStartPosSamples = 0; - } +ActionResult AudioClipView::verticalEncoderAction(int32_t offset, bool inCardRoutine) { + if (!currentUIMode && Buttons::isShiftButtonPressed() && !Buttons::isButtonPressed(deluge::hid::button::Y_ENC)) { + if (inCardRoutine && !allowSomeUserActionsEvenWhenInCardRoutine) { + return ActionResult::REMIND_ME_OUTSIDE_CARD_ROUTINE; // Allow sometimes. + } + + // Shift colour spectrum + getCurrentAudioClip()->colourOffset += offset; + uiNeedsRendering(this, 0xFFFFFFFF, 0); + } + return ActionResult::DEALT_WITH; +} - // If start pos greater than length, not allowed - if (newStartPosSamples >= sample->lengthInSamples) { - newStartPosSamples = sample->lengthInSamples - 1; - } +bool AudioClipView::setupScroll(uint32_t oldScroll) { - valueToChange = &clip->sampleHolder.startPos; - } + if (!getCurrentAudioClip()->currentlyScrollableAndZoomable()) { + return false; + } - ActionType actionType = - (newStartPos < oldStartPos) ? ActionType::CLIP_START_DECREASE : ActionType::CLIP_START_INCREASE; + return ClipView::setupScroll(oldScroll); +} - // Store old value for undo - uint64_t oldValue = *valueToChange; - *valueToChange = newStartPosSamples; +void AudioClipView::tellMatrixDriverWhichRowsContainSomethingZoomable() { + memset(PadLEDs::transitionTakingPlaceOnRow, 1, sizeof(PadLEDs::transitionTakingPlaceOnRow)); +} - // Create action for undo/redo - Action* action = actionLogger.getNewAction(actionType, ActionAddition::NOT_ALLOWED); - if (action) { - if (action->firstConsequence && action->firstConsequence->type == Consequence::CLIP_START) { - ConsequenceClipStart* consequence = (ConsequenceClipStart*)action->firstConsequence; - consequence->pointerToMarkerValue = valueToChange; - consequence->markerValueToRevertTo = oldValue; - } - actionLogger.closeAction(actionType); - } +uint32_t AudioClipView::getMaxLength() { + if (endMarkerVisible) { + return getCurrentClip()->loopLength + 1; + } + else { + return getCurrentClip()->loopLength; + } +} - // Update clip length and playback bounds - clip->setupPlaybackBounds(); +uint32_t AudioClipView::getMaxZoom() { + int32_t maxZoom = getCurrentClip()->getMaxZoom(); + if (endMarkerVisible && maxZoom < 1073741824) { + maxZoom <<= 1; + } + return maxZoom; } void AudioClipView::playbackEnded() { - // 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); + // 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); } void AudioClipView::clipNeedsReRendering(Clip* clip) { - if (clip == 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) { - horizontalScrollForLinearRecording(0); - } - else { - uiNeedsRendering(this, 0xFFFFFFFF, 0); - } - } + if (clip == 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) { + horizontalScrollForLinearRecording(0); + } + else { + uiNeedsRendering(this, 0xFFFFFFFF, 0); + } + } } void AudioClipView::sampleNeedsReRendering(Sample* sample) { - if (sample == getSample()) { - uiNeedsRendering(this, 0xFFFFFFFF, 0); - } + if (sample == getSample()) { + uiNeedsRendering(this, 0xFFFFFFFF, 0); + } } void AudioClipView::selectEncoderAction(int8_t offset) { - if (currentUIMode) { - return; - } - auto ao = (AudioOutput*)getCurrentAudioClip()->output; - ao->scrollAudioOutputMode(offset); + if (currentUIMode) { + return; + } + auto ao = (AudioOutput*)getCurrentAudioClip()->output; + ao->scrollAudioOutputMode(offset); } void AudioClipView::setClipLengthEqualToSampleLength() { - AudioClip* audioClip = getCurrentAudioClip(); - SamplePlaybackGuide guide = audioClip->guide; - SampleHolder* sampleHolder = (SampleHolder*)guide.audioFileHolder; - if (sampleHolder) { - adjustLoopLength(sampleHolder->getLoopLengthAtSystemSampleRate(true)); - display->displayPopup(deluge::l10n::get(deluge::l10n::String::STRING_FOR_CLIP_LENGTH_ADJUSTED)); - } - else { - display->displayPopup(deluge::l10n::get(deluge::l10n::String::STRING_FOR_NO_SAMPLE)); - } + AudioClip* audioClip = getCurrentAudioClip(); + SamplePlaybackGuide guide = audioClip->guide; + SampleHolder* sampleHolder = (SampleHolder*)guide.audioFileHolder; + if (sampleHolder) { + Sample* sample = static_cast(sampleHolder->audioFile); + if (sample) { + adjustLoopLength(sampleHolder->getLoopLengthAtSystemSampleRate(true)); + display->displayPopup(deluge::l10n::get(deluge::l10n::String::STRING_FOR_CLIP_LENGTH_ADJUSTED)); + } + } + else { + display->displayPopup(deluge::l10n::get(deluge::l10n::String::STRING_FOR_NO_SAMPLE)); + } } void AudioClipView::adjustLoopLength(int32_t newLength) { - int32_t oldLength = getCurrentClip()->loopLength; - - if (oldLength != newLength) { - Action* action = nullptr; - - 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 - uiNeedsRendering(getRootUI(), 0xFFFFFFFF, 0); - } - } - 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 { - goto doReRender; - } - } - } - } - - displayNumberOfBarsAndBeats(newLength, currentSong->xZoom[NAVIGATION_CLIP], false, "LONG"); - - if (action) { - if (action->firstConsequence && action->firstConsequence->type == Consequence::CLIP_LENGTH) { - ConsequenceClipLength* consequence = (ConsequenceClipLength*)action->firstConsequence; - consequence->pointerToMarkerValue = (uint64_t*)&getCurrentClip()->loopLength; - consequence->markerValueToRevertTo = oldLength; - } - actionLogger.closeAction(ActionType::CLIP_LENGTH); - } - } + AudioClip* clip = getCurrentAudioClip(); + if (!clip) { + return; + } + + Sample* sample = static_cast(clip->sampleHolder.audioFile); + if (!sample) { + return; + } + + uint32_t oldLength = clip->loopLength; + uint64_t oldLengthSamples = clip->sampleHolder.getDurationInSamples(false); + + ActionType actionType = (newLength < oldLength) ? ActionType::CLIP_LENGTH_DECREASE : ActionType::CLIP_LENGTH_INCREASE; + + // Create action for undo/redo + Action* action = actionLogger.getNewAction(actionType, ActionAddition::NOT_ALLOWED); + if (action) { + action->xScrollClip[AFTER] = currentSong->xScroll[NAVIGATION_CLIP]; + if (action->firstConsequence && action->firstConsequence->type == Consequence::CLIP_LENGTH) { + ConsequenceClipLength* consequence = (ConsequenceClipLength*)action->firstConsequence; + consequence->pointerToMarkerValue = (uint64_t*)&getCurrentClip()->loopLength; + consequence->markerValueToRevertTo = oldLength; + } + actionLogger.closeAction(actionType); + } } -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); - } -} - -ActionResult AudioClipView::editClipLengthWithoutTimestretching(int32_t offset) { - // If tempoless recording, don't allow - if (!getCurrentClip()->currentlyScrollableAndZoomable()) { - display->displayPopup(deluge::l10n::get(deluge::l10n::String::STRING_FOR_CANT_EDIT_LENGTH)); - 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)) { - 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; - - uint32_t newLength = changeClipLength(offset, oldLength, action); - - AudioClip* audioClip = getCurrentAudioClip(); - SamplePlaybackGuide guide = audioClip->guide; - SampleHolder* sampleHolder = (SampleHolder*)guide.audioFileHolder; - if (sampleHolder) { - Sample* sample = static_cast(sampleHolder->audioFile); - if (sample) { - changeUnderlyingSampleLength(audioClip, sample, newLength, oldLength, oldLengthSamples); - } - } - - displayNumberOfBarsAndBeats(newLength, currentSong->xZoom[NAVIGATION_CLIP], false, "LONG"); - - if (action) { - action->xScrollClip[AFTER] = currentSong->xScroll[NAVIGATION_CLIP]; - } - return ActionResult::DEALT_WITH; -} - -ActionResult AudioClipView::verticalEncoderAction(int32_t offset, bool inCardRoutine) { - if (!currentUIMode && Buttons::isShiftButtonPressed() && !Buttons::isButtonPressed(deluge::hid::button::Y_ENC)) { - if (inCardRoutine && !allowSomeUserActionsEvenWhenInCardRoutine) { - return ActionResult::REMIND_ME_OUTSIDE_CARD_ROUTINE; // Allow sometimes. - } - - // Shift colour spectrum - getCurrentAudioClip()->colourOffset += offset; - uiNeedsRendering(this, 0xFFFFFFFF, 0); - } - return ActionResult::DEALT_WITH; -} - -bool AudioClipView::setupScroll(uint32_t oldScroll) { - - if (!getCurrentAudioClip()->currentlyScrollableAndZoomable()) { - return false; - } - - return ClipView::setupScroll(oldScroll); -} - -void AudioClipView::tellMatrixDriverWhichRowsContainSomethingZoomable() { - memset(PadLEDs::transitionTakingPlaceOnRow, 1, sizeof(PadLEDs::transitionTakingPlaceOnRow)); +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; + } + } + + // If end pos less than 0, not allowed + if (newEndPosSamples < 0) { + newEndPosSamples = 0; + } + + valueToChange = &clip->sampleHolder.startPos; + } + + // AudioClip playing forward + else { + newEndPosSamples = clip->sampleHolder.startPos + 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->fileLoopEndSamples) { + int64_t distanceFromFileEndMarker = newEndPosSamples - (uint64_t)sample->fileLoopEndSamples; + if (distanceFromFileEndMarker < 0) { + distanceFromFileEndMarker = -distanceFromFileEndMarker; // abs + } + 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; + } + } + + valueToChange = &clip->sampleHolder.endPos; + } + + ActionType actionType = + (newLength < oldLength) ? ActionType::CLIP_LENGTH_DECREASE : ActionType::CLIP_LENGTH_INCREASE; + + // Change sample end-pos value. Must do this before calling setClipLength(), which will end + // up reading this value. + uint64_t oldValue = *valueToChange; + *valueToChange = newEndPosSamples; + + Action* action = actionLogger.getNewAction(actionType, ActionAddition::NOT_ALLOWED); + currentSong->setClipLength(clip, newLength, action); + + if (action) { + if (action->firstConsequence && action->firstConsequence->type == Consequence::CLIP_LENGTH) { + ConsequenceClipLength* consequence = (ConsequenceClipLength*)action->firstConsequence; + consequence->pointerToMarkerValue = valueToChange; + consequence->markerValueToRevertTo = oldValue; + } + actionLogger.closeAction(actionType); + } } -uint32_t AudioClipView::getMaxLength() { - if (endMarkerVisible) { - return getCurrentClip()->loopLength + 1; - } - else { - return getCurrentClip()->loopLength; - } +void AudioClipView::changeUnderlyingSampleStartPos(AudioClip* clip, const Sample* sample, int32_t newStartPos, + int32_t oldStartPos, uint64_t oldStartPosSamples) const { + uint64_t* valueToChange; + int64_t newStartPosSamples; + + uint64_t newStartPosSamplesValue = + (uint64_t)(oldStartPosSamples * newStartPos + (oldStartPos >> 1)) / (uint32_t)oldStartPos; // Rounded + + // Ensure start position doesn't exceed clip length + if (newStartPosSamplesValue >= clip->sampleHolder.getDurationInSamples(true)) { + newStartPosSamplesValue = clip->sampleHolder.getDurationInSamples(true) - 1; + } + + // AudioClip reversed + if (clip->sampleControls.reversed) { + + newStartPosSamples = clip->sampleHolder.getEndPos(true) - newStartPosSamplesValue; + + // If very close to actual wave start... + if (newStartPosSamples < 10) { + newStartPosSamples = 0; + } + + // If end pos less than 0, not allowed + if (newStartPosSamples < 0) { + newStartPosSamples = 0; + } + + valueToChange = &clip->sampleHolder.startPos; + } + // AudioClip playing forward + else { + newStartPosSamples = newStartPosSamplesValue; + + // If very close to actual wave start... + if (newStartPosSamples < 10) { + newStartPosSamples = 0; + } + + // If start pos greater than length, not allowed + if (newStartPosSamples >= sample->lengthInSamples) { + newStartPosSamples = sample->lengthInSamples - 1; + } + + valueToChange = &clip->sampleHolder.startPos; + } + + ActionType actionType = + (newStartPos < oldStartPos) ? ActionType::CLIP_LENGTH_DECREASE : ActionType::CLIP_LENGTH_INCREASE; + + // Store old value for undo + uint64_t oldValue = *valueToChange; + *valueToChange = newStartPosSamples; + + // Create action for undo/redo + Action* action = actionLogger.getNewAction(actionType, ActionAddition::NOT_ALLOWED); + if (action) { + if (action->firstConsequence && action->firstConsequence->type == Consequence::CLIP_LENGTH) { + ConsequenceClipLength* consequence = (ConsequenceClipLength*)action->firstConsequence; + consequence->pointerToMarkerValue = valueToChange; + consequence->markerValueToRevertTo = oldValue; + } + actionLogger.closeAction(actionType); + } + + // Update clip length and playback bounds + clip->setupPlaybackBounds(); } -uint32_t AudioClipView::getMaxZoom() { - int32_t maxZoom = getCurrentClip()->getMaxZoom(); - if (endMarkerVisible && maxZoom < 1073741824) { - maxZoom <<= 1; - } - return maxZoom; +void AudioClipView::changeUnderlyingSampleEndPos(AudioClip* clip, const Sample* sample, int32_t newEndPos, + int32_t oldEndPos, uint64_t oldEndPosSamples) const { + uint64_t* valueToChange; + int64_t newEndPosSamples; + + uint64_t newEndPosSamplesValue = + (uint64_t)(oldEndPosSamples * newEndPos + (oldEndPos >> 1)) / (uint32_t)oldEndPos; // Rounded + + // Ensure end position doesn't exceed clip length + if (newEndPosSamplesValue >= clip->sampleHolder.getDurationInSamples(true)) { + newEndPosSamplesValue = clip->sampleHolder.getDurationInSamples(true) - 1; + } + + // AudioClip reversed + if (clip->sampleControls.reversed) { + + newEndPosSamples = clip->sampleHolder.startPos - newEndPosSamplesValue; + + // If very close to actual wave start... + if (newEndPosSamples < 10) { + newEndPosSamples = 0; + } + + // If end pos less than 0, not allowed + if (newEndPosSamples < 0) { + newEndPosSamples = 0; + } + + valueToChange = &clip->sampleHolder.endPos; + } + // AudioClip playing forward + else { + newEndPosSamples = newEndPosSamplesValue; + + // If very close to actual wave start... + if (newEndPosSamples < 10) { + newEndPosSamples = 0; + } + + // If start pos greater than length, not allowed + if (newEndPosSamples >= sample->lengthInSamples) { + newEndPosSamples = sample->lengthInSamples - 1; + } + + valueToChange = &clip->sampleHolder.endPos; + } + + ActionType actionType = + (newEndPos < oldEndPos) ? ActionType::CLIP_LENGTH_DECREASE : ActionType::CLIP_LENGTH_INCREASE; + + // Store old value for undo + uint64_t oldValue = *valueToChange; + *valueToChange = newEndPosSamples; + + // Create action for undo/redo + Action* action = actionLogger.getNewAction(actionType, ActionAddition::NOT_ALLOWED); + if (action) { + if (action->firstConsequence && action->firstConsequence->type == Consequence::CLIP_LENGTH) { + ConsequenceClipLength* consequence = (ConsequenceClipLength*)action->firstConsequence; + consequence->pointerToMarkerValue = valueToChange; + consequence->markerValueToRevertTo = oldValue; + } + actionLogger.closeAction(actionType); + } + + // Update clip length and playback bounds + clip->setupPlaybackBounds(); } diff --git a/src/deluge/gui/views/audio_clip_view.h b/src/deluge/gui/views/audio_clip_view.h index ec672a4806..2276176cda 100644 --- a/src/deluge/gui/views/audio_clip_view.h +++ b/src/deluge/gui/views/audio_clip_view.h @@ -74,6 +74,8 @@ class AudioClipView final : public ClipView, public ClipMinder { uint64_t oldLengthSamples) const; void changeUnderlyingSampleStartPos(AudioClip* clip, const Sample* sample, int32_t newStartPos, int32_t oldStartPos, uint64_t oldStartPosSamples) const; + void changeUnderlyingSampleEndPos(AudioClip* clip, const Sample* sample, int32_t newEndPos, int32_t oldEndPos, + uint64_t oldEndPosSamples) const; }; extern AudioClipView audioClipView;