diff --git a/.github/workflows/fork_build.yml b/.github/workflows/fork_build.yml new file mode 100644 index 0000000000..cbea5b31cc --- /dev/null +++ b/.github/workflows/fork_build.yml @@ -0,0 +1,46 @@ +on: + push: + branches: + - 'community-integrate' + pull_request: + branches: + - 'community-integrate' + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + build: + name: Build ${{ matrix.profile }}-${{ matrix.hardware }} + strategy: + matrix: + profile: [ 'debug', 'release' ] + hardware: [ 'oled', '7seg' ] + runs-on: ubuntu-latest + steps: + - name: check out repo + uses: actions/checkout@v3 + - name: Fetch container image + run: | + docker pull ghcr.io/bobtwinkles/deluge-ci-images:v$(cat toolchain/REQUIRED_VERSION) + - name: Run build + run: | + docker run --rm \ + --user=$(id --user):$(id --group) \ + -v $(pwd):/src \ + ghcr.io/bobtwinkles/deluge-ci-images:v$(cat toolchain/REQUIRED_VERSION) \ + --e2_target=dbt-build-${{ matrix.profile }}-${{ matrix.hardware }} + - name: Publish built firmware (bin) + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.profile }}-${{ matrix.hardware }}.bin + path: dbt-build-${{ matrix.profile }}-${{ matrix.hardware }}/Deluge-${{ matrix.profile }}-${{ matrix.hardware }}.bin + if-no-files-found: error + retention-days: 5 + - name: Publish built firmware (elf) + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.profile }}-${{ matrix.hardware }}.elf + path: dbt-build-${{ matrix.profile }}-${{ matrix.hardware }}/Deluge-${{ matrix.profile }}-${{ matrix.hardware }}.elf + if-no-files-found: error + retention-days: 5 diff --git a/src/definitions.h b/src/definitions.h index cb94f9ec91..6a87ccd31b 100644 --- a/src/definitions.h +++ b/src/definitions.h @@ -92,9 +92,10 @@ #define FIRMWARE_4P1P4_ALPHA 68 #define FIRMWARE_4P1P4_BETA 69 #define FIRMWARE_4P1P4 70 +#define FIRMWARE_4P1P4_MUPADUW_TS 71 #define FIRMWARE_TOO_NEW 255 -#define CURRENT_FIRMWARE_VERSION FIRMWARE_4P1P4_ALPHA +#define CURRENT_FIRMWARE_VERSION FIRMWARE_4P1P4_MUPADUW_TS // FIRMWARE_4P1P4_ALPHA #if DELUGE_MODEL == DELUGE_MODEL_40_PAD diff --git a/src/deluge/gui/ui/sound_editor.cpp b/src/deluge/gui/ui/sound_editor.cpp index 8efa50cd94..60c52d7928 100644 --- a/src/deluge/gui/ui/sound_editor.cpp +++ b/src/deluge/gui/ui/sound_editor.cpp @@ -1671,6 +1671,20 @@ class MenuItemAudioClipReverse final : public MenuItemSelection { } } audioClipReverseMenu; +// Class is named after the Deluge front-panel labelling, where [Sample1-Mode] is the bottom-left most pad. +// This is the Timestretch on/off menu for the current AudioClip. +class MenuItemAudioClipMode final : public MenuItemSelection { +public: + MenuItemAudioClipMode(char const* newName = NULL) : MenuItemSelection(newName) {} + void readCurrentValue() { + soundEditor.currentValue = ((AudioClip*)currentSong->currentClip)->sampleControls.timeStretchEnabled; + } + void writeCurrentValue() { + AudioClip* clip = (AudioClip*)currentSong->currentClip; + clip->sampleControls.timeStretchEnabled = soundEditor.currentValue; + } +} audioClipModeMenu; + class MenuItemAudioClipTranspose final : public MenuItemDecimal, public MenuItemWithCCLearning { public: MenuItemAudioClipTranspose(char const* newName = NULL) : MenuItemDecimal(newName) {} @@ -1828,7 +1842,7 @@ MenuItem* paramShortcutsForSounds[][8] = { {&delayRateMenu, &delaySyncMenu, &delayAnalogMenu, &delayFeedbackMenu, &delayPingPongMenu, NULL, NULL, NULL}}; MenuItem* paramShortcutsForAudioClips[][8] = { - {NULL, &audioClipReverseMenu, NULL, &samplePitchSpeedMenu, NULL, &fileSelectorMenu, + {&audioClipModeMenu, &audioClipReverseMenu, NULL, &samplePitchSpeedMenu, NULL, &fileSelectorMenu, &audioClipSampleMarkerEditorMenuEnd, &audioClipSampleMarkerEditorMenuStart}, {NULL, &audioClipReverseMenu, NULL, &samplePitchSpeedMenu, NULL, &fileSelectorMenu, &audioClipSampleMarkerEditorMenuEnd, &audioClipSampleMarkerEditorMenuStart}, @@ -2253,7 +2267,7 @@ MenuItemSubmenu coloursSubmenu; MenuItemRuntimeFeatureSetting runtimeFeatureSettingMenuItem; MenuItemRuntimeFeatureSettings runtimeFeatureSettingsMenu; -char const* firmwareString = "4.1.4-alpha3"; +char const* firmwareString = "4.1.4-MUPADUW_TS_V4F"; // this class is haunted for some reason, clang-format mangles it // clang-format off @@ -2936,12 +2950,17 @@ SoundEditor::SoundEditor() { // AudioClip menu system ------------------------------------------------------------------------------------------------------------------------------- // Sample menu + new (&audioClipModeMenu) MenuItemAudioClipMode("Stretch mode"); new (&audioClipReverseMenu) MenuItemAudioClipReverse("REVERSE"); new (&audioClipSampleMarkerEditorMenuStart) MenuItemAudioClipSampleMarkerEditor("", MARKER_START); new (&audioClipSampleMarkerEditorMenuEnd) MenuItemAudioClipSampleMarkerEditor("WAVEFORM", MARKER_END); - static MenuItem* audioClipSampleMenuItems[] = {&fileSelectorMenu, &audioClipReverseMenu, - &samplePitchSpeedMenu, &audioClipSampleMarkerEditorMenuEnd, - &interpolationMenu, NULL}; + static MenuItem* audioClipSampleMenuItems[] = {&fileSelectorMenu, + &audioClipReverseMenu, + &audioClipModeMenu, + &samplePitchSpeedMenu, + &audioClipSampleMarkerEditorMenuEnd, + &interpolationMenu, + NULL}; // LPF menu new (&audioClipLPFFreqMenu) MenuItemAudioClipLPFFreq("Frequency", PARAM_UNPATCHED_GLOBALEFFECTABLE_LPF_FREQ); diff --git a/src/deluge/gui/views/audio_clip_view.cpp b/src/deluge/gui/views/audio_clip_view.cpp index 62f6dafb86..2021da0ec6 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. * @@ -52,6 +52,9 @@ extern uint8_t currentlyAccessingCard; AudioClipView audioClipView{}; AudioClipView::AudioClipView() { + startMarkerVisible = false; + endMarkerVisible = false; + isEditingStartMarker = false; } inline AudioClip* getClip() { @@ -336,6 +339,7 @@ int AudioClipView::buttonAction(int x, int y, bool on, bool inCardRoutine) { getClip()->clear(action, modelStack); numericDriver.displayPopup(HAVE_OLED ? "Audio clip cleared" : "CLEAR"); endMarkerVisible = false; + startMarkerVisible = false; uiTimerManager.unsetTimer(TIMER_UI_SPECIFIC); uiNeedsRendering(this, 0xFFFFFFFF, 0); } @@ -349,8 +353,9 @@ int AudioClipView::buttonAction(int x, int y, bool on, bool inCardRoutine) { if (result != ACTION_RESULT_REMIND_ME_OUTSIDE_CARD_ROUTINE) { deactivateMarkerIfNecessary: - if (endMarkerVisible) { + if (endMarkerVisible || startMarkerVisible) { endMarkerVisible = false; + startMarkerVisible = false; if (getCurrentUI() == this) uiTimerManager.unsetTimer(TIMER_UI_SPECIFIC); uiNeedsRendering(this, 0xFFFFFFFF, 0); } @@ -363,169 +368,173 @@ int AudioClipView::buttonAction(int x, int y, bool on, bool inCardRoutine) { } int AudioClipView::padAction(int x, int y, int on) { - - // Edit pad action... - if (x < displayWidth) { - - if (Buttons::isButtonPressed(tempoEncButtonX, tempoEncButtonY)) { - if (on) playbackHandler.grabTempoFromClip(getClip()); - } - - else { - - if (sdRoutineLock) return ACTION_RESULT_REMIND_ME_OUTSIDE_CARD_ROUTINE; - - // Maybe go to SoundEditor - int soundEditorResult = soundEditor.potentialShortcutPadAction(x, y, on); - if (soundEditorResult != ACTION_RESULT_NOT_DEALT_WITH) { - - if (soundEditorResult == ACTION_RESULT_DEALT_WITH) { - endMarkerVisible = false; - uiTimerManager.unsetTimer(TIMER_UI_SPECIFIC); - uiNeedsRendering(this, 0xFFFFFFFF, 0); - } - - return soundEditorResult; - } - - else if (on && !currentUIMode) { - - AudioClip* clip = getClip(); - - int 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... - - // 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(TIMER_UI_SPECIFIC); - endMarkerVisible = false; - } - - // Otherwise, move it - else { - - Sample* sample = getSample(); - if (sample) { - - int oldLength = clip->loopLength; - - // Ok, move the marker! - int newLength = - (x + 1) * currentSong->xZoom[NAVIGATION_CLIP] + currentSong->xScroll[NAVIGATION_CLIP]; - - uint64_t oldLengthSamples = clip->sampleHolder.getDurationInSamples(true); - - uint64_t newLengthSamples = (uint64_t)(oldLengthSamples * newLength + (oldLength >> 1)) - / (uint32_t)oldLength; // Rounded - - uint64_t* valueToChange; - int64_t newEndPosSamples; - - // 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; - goto setTheStartPos; - } - } - - // 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) return ACTION_RESULT_DEALT_WITH; + if (x < displayWidth) { + if (Buttons::isButtonPressed(tempoEncButtonX, tempoEncButtonY)) { + if (on) playbackHandler.grabTempoFromClip(getClip()); + } + else { + if (sdRoutineLock) return ACTION_RESULT_REMIND_ME_OUTSIDE_CARD_ROUTINE; + + // Maybe go to SoundEditor + int soundEditorResult = soundEditor.potentialShortcutPadAction(x, y, on); + if (soundEditorResult != ACTION_RESULT_NOT_DEALT_WITH) { + if (soundEditorResult == ACTION_RESULT_DEALT_WITH) { + endMarkerVisible = false; + startMarkerVisible = false; + uiTimerManager.unsetTimer(TIMER_UI_SPECIFIC); + uiNeedsRendering(this, 0xFFFFFFFF, 0); + } + return soundEditorResult; + } + else if (on && !currentUIMode) { + AudioClip* clip = getClip(); + + // Calculate display positions + int endSquareDisplay = divide_round_negative( + clip->loopLength - currentSong->xScroll[NAVIGATION_CLIP] - 1, + currentSong->xZoom[NAVIGATION_CLIP]); + + int startSquareDisplay = divide_round_negative( + clip->sampleHolder.startPos - currentSong->xScroll[NAVIGATION_CLIP], + currentSong->xZoom[NAVIGATION_CLIP]); + + // Toggle between start/end editing if shift is held and tapping the same spot + if (Buttons::isShiftButtonPressed()) { + if (startMarkerVisible || endMarkerVisible) { + isEditingStartMarker = !isEditingStartMarker; + startMarkerVisible = isEditingStartMarker; + endMarkerVisible = !isEditingStartMarker; + uiNeedsRendering(this, 0xFFFFFFFF, 0); + return ACTION_RESULT_DEALT_WITH; + } + } + + // Handle start marker + if (startMarkerVisible) { + if (x == startSquareDisplay) { + if (blinkOn) uiNeedsRendering(this, 0xFFFFFFFF, 0); + uiTimerManager.unsetTimer(TIMER_UI_SPECIFIC); + startMarkerVisible = false; + } + else { + Sample* sample = getSample(); + if (sample) { + int oldLength = clip->loopLength; + int newStartPos = x * currentSong->xZoom[NAVIGATION_CLIP] + currentSong->xScroll[NAVIGATION_CLIP]; + + uint64_t oldLengthSamples = clip->sampleHolder.getDurationInSamples(true); + uint64_t newLengthSamples = oldLengthSamples - (newStartPos - clip->sampleHolder.startPos); + + uint64_t* valueToChange; + int64_t newPosSamples; + + if (clip->sampleControls.reversed) { + newPosSamples = clip->sampleHolder.getEndPos(true) - newLengthSamples; + if (sample->fileLoopStartSamples) { + int64_t distanceFromFileEndMarker = newPosSamples - (uint64_t)sample->fileLoopStartSamples; + if (distanceFromFileEndMarker < 0) distanceFromFileEndMarker = -distanceFromFileEndMarker; + if (distanceFromFileEndMarker < 10) { + newPosSamples = sample->fileLoopStartSamples; + goto setTheStartPos; + } + } + { + int64_t distanceFromFileEndMarker = newPosSamples; + if (distanceFromFileEndMarker < 0) distanceFromFileEndMarker = -distanceFromFileEndMarker; + if (distanceFromFileEndMarker < 10) newPosSamples = 0; + } + if (newPosSamples < 0) return ACTION_RESULT_DEALT_WITH; setTheStartPos: - 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; - goto setTheEndPos; - } - } - - // 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; - } -setTheEndPos: - valueToChange = &clip->sampleHolder.endPos; - } - - int actionType = - (newLength < oldLength) ? ACTION_CLIP_LENGTH_DECREASE : ACTION_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, false); - 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); - } - - goto needRendering; - } - } - } - - // Or, marker not already visible - else { - if (x == endSquareDisplay || x == endSquareDisplay + 1) { - endMarkerVisible = true; + valueToChange = &clip->sampleHolder.startPos; + } + else { + newPosSamples = newStartPos; + if (sample->fileLoopStartSamples) { + int64_t distanceFromFileStartMarker = newPosSamples - (uint64_t)sample->fileLoopStartSamples; + if (distanceFromFileStartMarker < 0) distanceFromFileStartMarker = -distanceFromFileStartMarker; + if (distanceFromFileStartMarker < 10) { + newPosSamples = sample->fileLoopStartSamples; + goto setTheStartPos2; + } + } + { + if (newPosSamples < 0) newPosSamples = 0; + if (newPosSamples >= clip->sampleHolder.endPos) return ACTION_RESULT_DEALT_WITH; + } +setTheStartPos2: + valueToChange = &clip->sampleHolder.startPos; + } + + int actionType = ACTION_CLIP_LENGTH_DECREASE; // Starting point moves right = decrease length + + uint64_t oldValue = *valueToChange; + *valueToChange = newPosSamples; + + Action* action = actionLogger.getNewAction(actionType, false); + currentSong->setClipLength(clip, clip->loopLength - (newStartPos - clip->sampleHolder.startPos), 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); + } + + goto needRendering; + } + } + } + // Handle end marker + else if (endMarkerVisible) { + if (x == endSquareDisplay) { + if (blinkOn) uiNeedsRendering(this, 0xFFFFFFFF, 0); + uiTimerManager.unsetTimer(TIMER_UI_SPECIFIC); + endMarkerVisible = false; + } + else { + Sample* sample = getSample(); + if (sample) { + int newEndPos = (x + 1) * currentSong->xZoom[NAVIGATION_CLIP] + currentSong->xScroll[NAVIGATION_CLIP]; + + // Ensure we don't move before the start + if (newEndPos <= clip->sampleHolder.startPos) { + return ACTION_RESULT_DEALT_WITH; + } + + Action* action = actionLogger.getNewAction(ACTION_AUDIO_CLIP_EDIT, false); + + if (action) { + action->recordAudioClipSampleChange(clip); + clip->sampleHolder.endPos = newEndPos; + actionLogger.closeAction(ACTION_AUDIO_CLIP_EDIT); + } + + goto needRendering; + } + } + } + // Neither marker visible - show the appropriate one + else { + if (x == startSquareDisplay || x == endSquareDisplay) { + if (x == startSquareDisplay) { + startMarkerVisible = true; + isEditingStartMarker = true; + } else { + endMarkerVisible = true; + isEditingStartMarker = false; + } needRendering: - uiTimerManager.setTimer(TIMER_UI_SPECIFIC, SAMPLE_MARKER_BLINK_TIME); - blinkOn = true; - uiNeedsRendering(this, 0xFFFFFFFF, 0); - } - } - } - } - } - - return ACTION_RESULT_DEALT_WITH; + uiTimerManager.setTimer(TIMER_UI_SPECIFIC, SAMPLE_MARKER_BLINK_TIME); + blinkOn = true; + uiNeedsRendering(this, 0xFFFFFFFF, 0); + } + } + } + } + } + return ACTION_RESULT_DEALT_WITH; } void AudioClipView::playbackEnded() { diff --git a/src/deluge/gui/views/audio_clip_view.h b/src/deluge/gui/views/audio_clip_view.h index 4027aa5c78..8cde9103e7 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. * @@ -28,10 +28,10 @@ class AudioClipView final : public ClipView, public ClipMinder { bool opened(); void focusRegained(); - bool renderMainPads(uint32_t whichRows, uint8_t image[][displayWidth + sideBarWidth][3], - uint8_t occupancyMask[][displayWidth + sideBarWidth], bool drawUndefinedArea = true); - bool renderSidebar(uint32_t whichRows, uint8_t image[][displayWidth + sideBarWidth][3], - uint8_t occupancyMask[][displayWidth + sideBarWidth]); + bool renderMainPads(uint32_t whichRows, uint8_t image[][displayWidth + sideBarWidth][3] = NULL, + uint8_t occupancyMask[][displayWidth + sideBarWidth] = NULL, bool drawUndefinedArea = true); + bool renderSidebar(uint32_t whichRows, uint8_t image[][displayWidth + sideBarWidth][3] = NULL, + uint8_t occupancyMask[][displayWidth + sideBarWidth] = NULL); bool setupScroll(uint32_t oldScroll); void transitionToSessionView(); void tellMatrixDriverWhichRowsContainSomethingZoomable(); @@ -61,6 +61,8 @@ class AudioClipView final : public ClipView, public ClipMinder { int lastTickSquare; bool mustRedrawTickSquares; bool endMarkerVisible; + bool startMarkerVisible; // New: track if start marker is visible + bool isEditingStartMarker; // New: track whether we're editing start or end bool blinkOn; }; diff --git a/src/deluge/gui/views/view.cpp b/src/deluge/gui/views/view.cpp index 8b1fa4a1cf..246ec5a7fa 100644 --- a/src/deluge/gui/views/view.cpp +++ b/src/deluge/gui/views/view.cpp @@ -288,9 +288,24 @@ int View::buttonAction(int x, int y, bool on, bool inCardRoutine) { } } - // Sync-scaling button else if (x == syncScalingButtonX && y == syncScalingButtonY) { - if (on && currentUIMode == UI_MODE_NONE) { + + // -> change song time-stretching CBC + if (on && Buttons::isShiftButtonPressed()) { + if (on) { + currentSong->timeStretchEnabled = !currentSong->timeStretchEnabled; + // show the user the new value with a PopUp message + if (currentSong->timeStretchEnabled) { + numericDriver.displayPopup(HAVE_OLED ? "Time-stretch: On" : "TSON", 2); + } + else { + numericDriver.displayPopup(HAVE_OLED ? "Time-stretch: Off" : "TSOF", 2); + } + } + } + + // legacy sync-scaling behaviour + else if (on && currentUIMode == UI_MODE_NONE) { if (playbackHandler.recording == RECORDING_ARRANGEMENT) { cant: @@ -382,7 +397,7 @@ int View::buttonAction(int x, int y, bool on, bool inCardRoutine) { if (on && currentUIMode == UI_MODE_NONE) { if (playbackHandler.recording == RECORDING_ARRANGEMENT) { - numericDriver.displayPopup(HAVE_OLED ? "Recording to arrangement" : "CANT"); + numericDriver.displayPopup(HAVE_OLED ? "Recording to arrangement" : "CANT"); //CBC Why show CANT for 7SEG?? return ACTION_RESULT_DEALT_WITH; } diff --git a/src/deluge/model/action/action.h b/src/deluge/model/action/action.h index b0e8568c22..9427d439b5 100644 --- a/src/deluge/model/action/action.h +++ b/src/deluge/model/action/action.h @@ -1,5 +1,5 @@ /* - * Copyright © 2017-2023 Synthstrom Audible Limited + * Copyright 2017-2023 Synthstrom Audible Limited * * This file is part of The Synthstrom Audible Deluge Firmware. * @@ -68,6 +68,7 @@ class ModelStack; #define ACTION_NOTEROW_ROTATE 24 #define ACTION_NOTEROW_LENGTH_EDIT 25 #define ACTION_NOTEROW_HORIZONTAL_SHIFT 26 +#define ACTION_AUDIO_CLIP_EDIT 27 class Action { public: diff --git a/src/deluge/model/clip/audio_clip.cpp b/src/deluge/model/clip/audio_clip.cpp index 34a95e7e4e..f71dbb1b67 100644 --- a/src/deluge/model/clip/audio_clip.cpp +++ b/src/deluge/model/clip/audio_clip.cpp @@ -493,6 +493,15 @@ void AudioClip::render(ModelStackWithTimelineCounter* modelStack, int32_t* outpu uint64_t requiredSpeedAdjustment = ((uint64_t)sampleLengthInSamples << 24) / clipLengthInSamples; + // Control this using shift+syncScaleButton. Not saved to song XML. + if (!modelStack->song->timeStretchEnabled) { + goto justDontTimeStretch; + } + // Per-clip timestretch is set using the + shortcut menu + if (!sampleControls.timeStretchEnabled) { + goto justDontTimeStretch; + } + // If we're squishing time... if (sampleControls.pitchAndSpeedAreIndependent) { timeStretchRatio = requiredSpeedAdjustment; @@ -923,6 +932,8 @@ void AudioClip::writeDataToFile(Song* song) { storageManager.writeAttribute("startSamplePos", sampleHolder.startPos); storageManager.writeAttribute("endSamplePos", sampleHolder.endPos); storageManager.writeAttribute("pitchSpeedIndependent", sampleControls.pitchAndSpeedAreIndependent); + storageManager.writeAttribute("timeStretchEnabled", sampleControls.timeStretchEnabled); + if (sampleControls.interpolationMode == INTERPOLATION_MODE_LINEAR) storageManager.writeAttribute("linearInterpolation", 1); if (sampleControls.reversed) storageManager.writeAttribute("reversed", "1"); @@ -986,6 +997,10 @@ int AudioClip::readFromFile(Song* song) { sampleControls.pitchAndSpeedAreIndependent = storageManager.readTagOrAttributeValueInt(); } + else if (!strcmp(tagName, "timeStretchEnabled")) { + sampleControls.timeStretchEnabled = storageManager.readTagOrAttributeValueInt(); + } + else if (!strcmp(tagName, "linearInterpolation")) { if (storageManager.readTagOrAttributeValueInt()) sampleControls.interpolationMode = INTERPOLATION_MODE_LINEAR; diff --git a/src/deluge/model/sample/sample_controls.cpp b/src/deluge/model/sample/sample_controls.cpp index 6e10ab9eae..b626667ee8 100644 --- a/src/deluge/model/sample/sample_controls.cpp +++ b/src/deluge/model/sample/sample_controls.cpp @@ -24,6 +24,7 @@ SampleControls::SampleControls() { interpolationMode = INTERPOLATION_MODE_SMOOTH; pitchAndSpeedAreIndependent = false; reversed = false; + timeStretchEnabled = true; } int SampleControls::getInterpolationBufferSize(int32_t phaseIncrement) { diff --git a/src/deluge/model/sample/sample_controls.h b/src/deluge/model/sample/sample_controls.h index 0453a10654..0fb1344cbd 100644 --- a/src/deluge/model/sample/sample_controls.h +++ b/src/deluge/model/sample/sample_controls.h @@ -28,6 +28,7 @@ class SampleControls { uint8_t interpolationMode; bool pitchAndSpeedAreIndependent; bool reversed; + bool timeStretchEnabled; }; #endif /* SAMPLECONTROLS_H_ */ diff --git a/src/deluge/model/song/song.cpp b/src/deluge/model/song/song.cpp index 0a5660c1d7..0f8dab0224 100644 --- a/src/deluge/model/song/song.cpp +++ b/src/deluge/model/song/song.cpp @@ -89,6 +89,7 @@ Song::Song() : backedUpParamManagers(sizeof(BackedUpParamManager)) { tripletsOn = false; affectEntire = false; + timeStretchEnabled = true; modeNotes[0] = 0; modeNotes[1] = 2; @@ -959,6 +960,7 @@ void Song::writeToFile() { storageManager.writeAttribute("affectEntire", affectEntire); storageManager.writeAttribute("activeModFunction", globalEffectable.modKnobMode); + storageManager.writeAttribute("timeStretchEnabled", timeStretchEnabled); globalEffectable.writeAttributesToFile(false); @@ -1325,6 +1327,11 @@ int Song::readFromFile() { storageManager.exitTag("modeNotes"); } + else if (!strcmp(tagName, "timeStretchEnabled")) { + timeStretchEnabled = storageManager.readTagOrAttributeValueInt(); + storageManager.exitTag("timeStretchEnabled"); + } + else if (!strcmp(tagName, "sections")) { // Read in all the sections while (*(tagName = storageManager.readNextTagOrAttributeName())) { diff --git a/src/deluge/model/song/song.h b/src/deluge/model/song/song.h index d2e1afa816..dd8238c46e 100644 --- a/src/deluge/model/song/song.h +++ b/src/deluge/model/song/song.h @@ -165,6 +165,7 @@ class Song final : public TimelineCounter { String name; bool affectEntire; + bool timeStretchEnabled; int songViewYScroll; int arrangementYScroll;