diff --git a/releases/67_Fragments/CMakeLists.txt b/releases/67_Fragments/CMakeLists.txt index c97e0ea83..b858f55ec 100644 --- a/releases/67_Fragments/CMakeLists.txt +++ b/releases/67_Fragments/CMakeLists.txt @@ -51,5 +51,7 @@ target_link_libraries(fragments tinyusb_board ) -pico_set_binary_type(fragments copy_to_ram) +# Experimental XIP layout: keep most code in flash, with the hot audio +# path explicitly pinned to RAM. +# pico_set_binary_type(fragments copy_to_ram) pico_add_extra_outputs(fragments) diff --git a/releases/67_Fragments/README.md b/releases/67_Fragments/README.md index b5a9a7c59..2fc55ee04 100644 --- a/releases/67_Fragments/README.md +++ b/releases/67_Fragments/README.md @@ -1,227 +1,195 @@ # Fragments -Fragments is a six-slot audio recorder and clocked fragment sequencer for the -Music Thing Modular Workshop System Computer and Workshop Computer. +Fragments is a six-slot sampler and sequencer for the Music Thing Modular +Workshop System Computer and Workshop Computer. -Each slot holds up to 250 ms of stereo audio at 48 kHz. Samples can be captured -from the audio inputs or imported with the included browser librarian. A bank of -21 patterns sequences the slots, while shift, repeat division, reverse -probability, playback mode, MIDI pitch, and configurable random CV outputs -provide variation. +Record short sounds into the six slots, then play them back with patterns, +shift, repeat division, reverse probability, playback modes, MIDI pitch, and +random CV outputs. -The two audio channels remain independent when recording from the card, so they -can be used as stereo or as two unrelated mono sources. +Each slot can hold up to 400 ms of stereo audio at 48 kHz. Samples can be +recorded from the module or imported with the included web editor. -## Quick start +## Downloads -1. Patch audio into either audio input. -2. Move Z up and use Main to select one of the six slots. -3. Send a gate to Pulse In 2 to record into that slot. -4. Return Z to the middle position. -5. Send a clock to Pulse In 1. -6. Move Main to select a pattern, X to shift its slot numbers, and Y to set the - repeat division. +- [fragments.uf2](UF2/fragments.uf2): standard 48 kHz firmware, up to 400 ms per slot. +- [fragments_24k.uf2](UF2/fragments_24k.uf2): alternate 24 kHz firmware, up to 800 ms per slot with a darker sound. -The LEDs show the selected slot while Z is up and the active playback slot while -Z is in the middle. +Use [web/fragments_librarian.html](web/fragments_librarian.html) for both +firmware versions. Select the matching firmware profile in the editor before +importing or sending samples. + +## Quick Start + +1. Patch audio into Audio In 1, Audio In 2, or both. +2. Move Z up. +3. Use Main to choose a slot. +4. Send a gate to Pulse In 2 to record into that slot. +5. Move Z to the middle. +6. Send a clock to Pulse In 1. +7. Use Main to choose a pattern, X to shift it, and Y to set repeat division. + +LEDs show the selected slot while Z is up. In playback, they show the active +slot or variation. ## Controls -### Z up: slot setup and recording +### Z Up: Slots | Control | Function | | --- | --- | -| Main | Selects recording slot 0-5 | -| X | Sets the selected slot's playback mode | -| Y | Sets the selected slot's reverse probability | -| Pulse In 2 | Records the selected slot while high | +| Main | Selects slot 0-5 | +| X | Sets playback mode | +| Y | Sets reverse probability | +| Pulse In 2 | Records while high | -X divides its travel into four playback modes: +Playback modes, from low to high on X: -| X position | Mode | Behavior | -| --- | --- | --- | -| 0-25% | Loop | Repeats until the sequencer moves to another step | -| 25-50% | One Shot | Plays its planned repeats, then becomes silent | -| 50-75% | Interrupt | Plays its planned repeats, then returns to live audio | -| 75-100% | Passthrough | Ignores the recording and passes live audio | - -X and Y use pickup behavior. Entering Z-up mode does not immediately overwrite -a slot's saved settings; the setting changes only after its knob moves. +| Mode | Behavior | +| --- | --- | +| Loop | Repeats until the next sequencer step | +| One Shot | Plays once, then rests | +| Interrupt | Plays once, then returns to live input | +| Passthrough | Plays live input instead of the recording | -On a clean card, all six slots start in One Shot mode with 0% reverse -probability. A saved kit restores its own per-slot mode and reverse settings. +New or cleared slots default to One Shot with 0% reverse probability. -### Z middle: pattern playback +### Z Middle: Patterns | Control | Function | | --- | --- | | Main | Selects one of 21 patterns | -| X | Shifts every slot number in the pattern by 0-5, wrapping around | -| Y | Selects x1, x2, x4, or x8 repeat division | -| Pulse In 1 | Advances the pattern on each rising edge | +| X | Shifts every slot number in the pattern, wrapping around | +| Y | Sets repeat division: x1, x2, x4, or x8 | +| Pulse In 1 | Advances the pattern | -Main, X, and Y respond only after they move in middle mode. This prevents stored -settings from jumping when Z moves between positions. +Main, X, and Y use pickup behavior. Changing Z position will not immediately +jump to the physical knob value; the setting changes after the knob moves. -At x2, x4, and x8, the most recently measured clock period is divided into that -many repeat windows. At x1, the fragment loops normally until the next clock. +### Z Down: Reset, Clear, Save -### Variation boot mode +| Gesture | Function | +| --- | --- | +| Tap Z down | Reset to the first pattern step | +| Hold Z down until all LEDs flash, release, tap once | Clear all samples | +| Hold Z down until all LEDs flash, release, tap twice | Save the current kit | -Hold Z down while rebooting the card with the Computer's boot/reset button to -start in Variation mode for that session. In this mode, Pulse In 2 records one -long sample of up to 72,000 samples, or 1.5 seconds at 48 kHz, using the same -memory that normally holds six shorter slots. Patterns play that one sample six -different ways instead of sequencing six different slots. Reboot normally to -return to standard slot-pattern playback. +After the LEDs flash, the card waits one second for a tap. A single tap waits +briefly before clearing so the card can tell it apart from a double tap. -| Pattern value | Variation | -| --- | --- | -| 0 | Normal speed | -| 1 | 2x speed, one octave up | -| 2 | 3x speed, one octave and a fifth up | -| 3 | Reverse | -| 4 | Reverse at 2x speed | -| 5 | 0.5x speed, one octave down | +## Recording -In Z-middle playback, X still shifts the pattern, but in Variation mode it -rotates the variation numbers instead of slot numbers. With Z up, Main controls -a +/-24 semitone pitch offset with a small no-pitch zone at the center, X sets -the global Variation playback mode, and Y sets reverse probability. Reverse -probability flips the natural direction of the variation: forward variations can -play backward, and reverse variations can play forward. MIDI pitch still applies -globally on top of the variation speed. The initial Z-down release is ignored so -the boot gesture does not accidentally reset or arm a save/clear command. - -Variation mode adds a 2 ms playback de-click at sample edges and step retriggers. -Recording stays raw, so standard mode keeps the sharper high-clock behavior. - -### Z down: reset, save, and clear - -- Tap and release Z down to reset the pattern to its first step. -- Hold Z down for two seconds, until all six LEDs flash, to arm the kit command. -- Release Z and tap once to clear all samples. Clear waits 150 ms before acting. -- Tap twice within that 150 ms window to save instead. All six LEDs light - briefly when the save completes. -- If no tap arrives within one second after release, the command is cancelled. - -The hardware save stores the same samples, slot settings, and pattern bank as -the librarian's Save command. Clear affects the current kit in memory; use the -hardware or librarian save afterward if the saved kit should also be replaced. - -## Recording behavior - -- Maximum recording length: 12,000 samples, or 250 ms at 48 kHz. Variation mode - records one long sample up to 72,000 samples, or 1.5 seconds. -- Minimum accepted recording: 10 ms. -- While Pulse In 2 is held, the audio inputs are monitored directly. -- Recordings preserve their raw edges without fades, allowing short fragments - to retain energy at high clock rates and playback speeds. -- Hot inputs are clamped cleanly when stored, preventing digital wraparound - distortion during capture. -- Playback uses linear interpolation for smoother pitch and speed changes. +- A standard slot records up to 400 ms. +- Variation mode records one longer sample up to 2.4 seconds. +- A recording must be at least 10 ms. - Only patched audio inputs are recorded. -- In standard mode, recording one channel preserves existing material on the - other channel. -- In Variation mode, recording replaces the long sample; unpatched channels are - cleared and remain silent during playback. -- If neither audio input is patched, the slot is left unchanged. -- In standard mode, an empty or unrecorded channel passes its corresponding - live input. - -Normally, Z up monitors live audio and pauses pattern playback. After Pulse In 1 -has received a clock, playback continues in Z-up mode for four seconds after the -most recent clock. This allows a slot to be selected or recorded without stopping -an externally clocked sequence. - -## CV and pulse outputs - -| Output | Function | +- If no audio input is patched, the slot is left unchanged. +- In standard mode, recording one channel preserves the other channel. +- Patched-but-silent audio can be recorded as silence. +- Recording does not stop the sequencer. + +The monitor input setting controls when live input is mixed with playback: + +| Mode | Behavior | | --- | --- | -| Pulse Out 1 | 10 ms pulse whenever the sequencer step changes | -| Pulse Out 2 | 10 ms pulse on the first index of the pattern | -| CV Out 1 | Configurable random voltage; defaults to full-range stepped random on each step | -| CV Out 2 | Configurable random voltage; defaults to full-range slewed random on each step | +| Always | Live input is always mixed with the program output | +| When Armed | Live input is mixed while Z is up or while recording | +| When Recording | Live input is mixed only while recording | -Pulse Out 2 follows the first pattern index, not slot 0, so shifting a pattern -does not change where the pattern-start pulse occurs. +The default monitor mode is When Armed. -The web librarian can configure each CV output's calibrated voltage range, -quantization, clock division, and slew time. Quantization options include -chromatic, major, minor, major pentatonic, minor pentatonic, dorian, pelog, and -whole tone. CV Out 2 can also be coupled to mirror CV Out 1 exactly. Use **Save -Current Kit To Card** afterward to keep those CV settings across power cycles. +## CV And Pulse -## CV inputs +| Jack | Function | +| --- | --- | +| CV In 1 | Pattern shift | +| CV In 2 | Reverse probability | +| Pulse Out 1 | 10 ms pulse on every sequencer step | +| Pulse Out 2 | 10 ms pulse on the first index of the pattern | +| CV Out 1 | Configurable random voltage, stepped by default | +| CV Out 2 | Configurable random voltage, slewed by default | + +CV In 1 and CV In 2 keep these assignments in every Z switch position. When a +CV input is patched, its matching knob becomes an attenuator: X for CV In 1 and +Y for CV In 2. -In middle mode, CV In 1 modulates pattern shift and CV In 2 modulates repeat -division. When a CV cable is connected, X and Y become attenuators for their -respective inputs. +The CV inputs automatically work with unipolar signals. If an input crosses +below 0 V, it switches to bipolar behavior so a roughly +/-6 V signal can sweep +the full control range. -The inputs automatically begin in unipolar mode. If a signal crosses below 0 V, -that input switches to bipolar interpretation so a roughly +/-6 V signal can -sweep the full control range. With no CV cable connected, X and Y work as direct -controls. +The web editor can configure the CV outputs' voltage range, quantization, +clock division, slew time, and CV Out 2 coupling. ## USB MIDI -Fragments appears as a USB MIDI device over USB-C. For compatibility with the -Workshop Computer MIDI examples, the USB product name may appear as -**MTMComputer** or a Music Thing MIDI card name in the operating system. +Fragments appears as a USB MIDI device over USB-C. Depending on the card and +operating system, it may appear as Fragments, MTMComputer, or a Music Thing MIDI +card name. + +| MIDI | Function | +| --- | --- | +| Notes | Change playback speed for all slots together | +| C4 / note 60 | Normal speed | +| Pitch bend | +/-12 semitones | +| CC 16 | Main knob | +| CC 17 | X knob | +| CC 18 | Y knob | + +Note Off has no assigned behavior. Moving a physical knob takes control back +from its MIDI CC value. -### Pitch and speed +## Variation Mode -- MIDI note 60 (C4) is normal playback speed. -- Notes above or below C4 change the playback speed of all slots together. -- Note On changes speed; Note Off has no assigned behavior. -- Pitch bend covers +/-12 semitones. +Hold Z down while rebooting the Computer with its boot/reset button to start in +Variation mode for that session. Reboot normally to return to standard mode. -### Knob control +Variation mode uses the six slot buffers as one longer sample. Patterns then +play that one sample six different ways: -| MIDI CC | Control | +| Pattern Value | Variation | | --- | --- | -| CC 16 | Main | -| CC 17 | X | -| CC 18 | Y | +| 0 | Normal speed | +| 1 | 2x speed, one octave up | +| 2 | 3x speed, one octave and a fifth up | +| 3 | Reverse | +| 4 | Reverse at 2x speed | +| 5 | 0.5x speed, one octave down | -Moving a physical knob takes control back from its MIDI CC value. +In Variation mode: -## Web librarian +- Pulse In 2 records the long sample. +- Main with Z up controls pitch over +/-24 semitones, with sweet spots at + useful intervals. +- X with Z up sets the global playback mode. +- Y with Z up sets reverse probability. +- CV In 1 still controls variation shift. +- CV In 2 still controls reverse probability. -Open [web/fragments_librarian.html](web/fragments_librarian.html) in a browser -with Web MIDI and SysEx support, such as Chrome or Edge. +## Web Editor + +Open [web/fragments_librarian.html](web/fragments_librarian.html) in Chrome or +Edge. 1. Select **Connect MIDI** and allow SysEx access. -2. Choose Fragments, MTMComputer, or the Music Thing MIDI card name for both - the MIDI input and output. -3. Select **Ping Card** to verify two-way communication. - -The librarian provides: - -- Global playback-speed control from 0.125x to 8x. -- MIDI control of Main, X, and Y. -- Per-output calibrated CV random-voltage range, quantization, clock division, - slew, and CV Out 2 coupling. -- Six sample slots with waveform trimming and audio preview. -- Copy and paste between sample slots for reusing one loaded file with different - start and end points. -- Per-slot waveform zoom up to 1024x, horizontal navigation, and Fit control. -- A maximum 250 ms selection window with editable start, end, and length values. -- Per-slot Audio 1, Audio 2, or Both destination selection. -- Optional normalization to -0.25 dBFS with up to 32x gain. -- WAV, AIFF/AIFC PCM, MP3, and M4A import when supported by the browser. -- CSV pattern import and factory-pattern restoration. -- Kit save and load commands. - -Imported stereo files are mixed to mono before being sent to the selected card -channel or channels. Preview plays the same 8-bit sample data that will be sent -to the card. - -## Pattern CSV format - -Each CSV row is one pattern. Each value is a slot number from 0 through 5, and a -row may contain 1-16 steps. The row number determines the pattern number; there -is no header or pattern-number column. +2. Choose the Fragments MIDI input and output. +3. Select **Ping Card** to confirm the connection. + +The web editor can: + +- Load, trim, preview, normalize, copy, and paste samples. +- Send one slot or all loaded samples to the card. +- Save and load kits. +- Edit and send pattern banks from CSV. +- Control playback speed and the Main, X, and Y knobs over MIDI. +- Configure monitor input and CV output behavior. + +Supported audio import depends on the browser, but WAV, AIFF/AIFC PCM, MP3, and +M4A are supported in common browsers. + +## Pattern CSV + +Each CSV row is one pattern. Each value is a slot number from 0 through 5. Rows +may contain 1-16 steps. The row number determines the pattern number. ```csv 0 @@ -230,39 +198,33 @@ is no header or pattern-number column. 5,4,3,2,1,0 ``` -The librarian accepts up to 21 rows. Sending fewer rows replaces only those -patterns. Use **Save Current Kit To Card** afterward to keep the updated pattern -bank across power cycles. +The web editor accepts up to 21 rows. Sending fewer rows replaces only those +patterns. Save the kit afterward to keep the new pattern bank across power +cycles. -The current default bank is available at +The factory bank is available at [web/fragments_factory_patterns.csv](web/fragments_factory_patterns.csv). -## Saved kits - -**Save Current Kit To Card** stores: +## Saved Kits -- All six audio slots and their channel assignments. -- Per-slot playback mode and reverse probability. -- The complete 21-pattern bank. -- CV output range, quantization, clock division, slew, and coupling settings. +A saved kit includes: -The card loads the saved kit automatically at boot. Pattern selection, shift, -division, MIDI note, pitch bend, and current sequencer position are performance -controls and are not saved. +- Audio slots and channel assignments. +- Slot playback modes and reverse probability. +- The 21-pattern bank. +- CV output settings. +- Monitor input mode. -Older saved kits remain compatible. If an older kit has no saved pattern bank, -the firmware uses its built-in factory patterns until the kit is saved again. +Pattern selection, shift, division, MIDI note, pitch bend, and current +sequencer position are performance controls and are not saved. ## Building -The ready-to-flash release file is included at -[UF2/fragments.uf2](UF2/fragments.uf2). - Requirements: - Raspberry Pi Pico SDK - CMake -- An ARM embedded GCC toolchain +- ARM embedded GCC toolchain ```sh export PICO_SDK_PATH=/path/to/pico-sdk @@ -272,39 +234,15 @@ cmake --build build_local -j4 The flashable file is `build_local/fragments.uf2`. -To flash the card, hold BOOTSEL while connecting USB-C, then copy -`fragments.uf2` to the mounted `RPI-RP2` drive. - -The firmware is compiled with size optimization and copied to RAM at boot. USB -MIDI, web communication, flash storage, LED updates, and other slower -control work run on the second RP2040 core so the 48 kHz audio path stays lean. - -## Memory +The included source builds the standard 48 kHz firmware. The 24 kHz source +variant is included as `src/fragments_24k.cpp` and +`src/usb_descriptors_24k.c` for reference. -The six stereo buffers use 144,000 bytes: - -`6 slots x 12,000 samples x 2 channels x 1 byte` - -The current build uses 264,080 bytes across code, static data, and working RAM. -The fixed buffer length should not be increased without checking the linker -memory report. +To flash the card, hold BOOTSEL while connecting USB-C, then copy the UF2 file +to the mounted `RPI-RP2` drive. ## License Fragments is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. See [LICENSE.md](LICENSE.md). - -## Project files - -| Path | Contents | -| --- | --- | -| `UF2/fragments.uf2` | Ready-to-flash firmware | -| `src/fragments.cpp` | Sequencer, recorder, controls, MIDI, and flash storage | -| `src/usb_descriptors.c` | USB MIDI device descriptors | -| `src/tusb_config.h` | TinyUSB configuration | -| `lib/ComputerCard.h` | Workshop Computer hardware helper | -| `web/fragments_librarian.html` | Self-contained browser librarian | -| `web/fragments_factory_patterns.csv` | Default 21-pattern bank | -| `CMakeLists.txt` | Pico SDK build configuration | -| `LICENSE.md` | CC BY-NC-SA 4.0 license notice | diff --git a/releases/67_Fragments/UF2/fragments.uf2 b/releases/67_Fragments/UF2/fragments.uf2 index 46cf275eb..c380079aa 100644 Binary files a/releases/67_Fragments/UF2/fragments.uf2 and b/releases/67_Fragments/UF2/fragments.uf2 differ diff --git a/releases/67_Fragments/UF2/fragments_24k.uf2 b/releases/67_Fragments/UF2/fragments_24k.uf2 new file mode 100644 index 000000000..4d60dce95 Binary files /dev/null and b/releases/67_Fragments/UF2/fragments_24k.uf2 differ diff --git a/releases/67_Fragments/info.yaml b/releases/67_Fragments/info.yaml index 2df51dfa9..231e44e19 100644 --- a/releases/67_Fragments/info.yaml +++ b/releases/67_Fragments/info.yaml @@ -1,9 +1,9 @@ -short-description: Six-slot sampler and clocked sequencer with MIDI pitch control, random CV outputs, an alternate long-sample variation mode, and web editor. -summary: Six-slot sampler and clocked sequencer with MIDI pitch control, random CV outputs, an alternate long-sample variation mode, and web editor. +draft: false Name: Fragments +short-description: Six-slot sampler and sequencer with MIDI pitch control, configurable random CV outputs, saved kits, alternate variation mode, web editor, and included 24 kHz long-sample firmware. Language: C++ (RPi Pico SDK) Creator: Max Harnishfeger -Version: 1 +Version: 1.1 Status: Released Editor: web web-entry: fragments_librarian.html @@ -15,5 +15,102 @@ tags: - sampler - sequencer - microlooper + - midi + - random-cv + +summary: Six-slot sampler and sequencer with MIDI pitch control, configurable random CV outputs, saved kits, alternate variation mode, web editor, and included 24 kHz long-sample firmware. + +panel: + inputs: + - id: AudioIn1 + name: Audio In 1 + description: Audio input for channel 1 + + - id: AudioIn2 + name: Audio In 2 + description: Audio input for channel 2 + + - id: CVIn1 + name: Pattern Shift CV + description: Controls pattern shift. When patched, X attenuates the incoming CV. + + - id: CVIn2 + name: Reverse Probability CV + description: Controls reverse probability. When patched, Y attenuates the incoming CV. + + - id: PulseIn1 + name: Clock In + description: Advances the sequencer. + + - id: PulseIn2 + name: Record Gate + description: Records into the selected slot while high. + + outputs: + - id: AudioOut1 + name: Audio Out 1 + description: Audio output for channel 1 + + - id: AudioOut2 + name: Audio Out 2 + description: Audio output for channel 2 + + - id: CVOut1 + name: Random CV 1 + description: Configurable random voltage, stepped by default. + + - id: CVOut2 + name: Random CV 2 + description: Configurable random voltage, slewed by default. Can be coupled to CV Out 1 from the web editor. + + - id: PulseOut1 + name: Step Pulse + description: Outputs a 10 ms pulse on each sequencer step. + + - id: PulseOut2 + name: Pattern Start Pulse + description: Outputs a 10 ms pulse on the first index of the current pattern. + +controls: + switch: + up: + name: Slot Settings + description: Select a slot with Main, set playback mode with X, and set reverse probability with Y. + + middle: + name: Sequencer Settings + description: Select a pattern with Main, shift the pattern with X, and set repeat division with Y. + + down: + name: Reset, Clear, Save + description: Tap to reset. Hold until the LEDs flash, then tap once to clear or twice to save. + + knobs: + - when: { z: up } + main: + name: Slot Select + description: Selects slots 0-5. + + x: + name: Playback Mode + description: Loop, one shot, interrupt, or passthrough. + + y: + name: Reverse Probability + description: Sets reverse probability from 0% to 100%. + + - when: { z: middle } + main: + name: Pattern Select + description: Selects one of 21 patterns. + + x: + name: Pattern Shift + description: Shifts pattern slot numbers, wrapping around the six slots. + + y: + name: Repeat Division + description: Sets repeat division to x1, x2, x4, or x8. + contact: email: watermonstermusic@gmail.com diff --git a/releases/67_Fragments/src/fragments.cpp b/releases/67_Fragments/src/fragments.cpp index 402ff8877..27cf50327 100644 --- a/releases/67_Fragments/src/fragments.cpp +++ b/releases/67_Fragments/src/fragments.cpp @@ -7,8 +7,10 @@ // Controls: // - Switch up: choose the recording slot with Main. X chooses that // slot's playback mode; Y sets its chance of playing backward. -// - Switch middle: Main chooses the pattern. X/CV1 shifts the pattern, -// and Y/CV2 chooses the repeat division. +// - Switch middle: Main chooses the pattern. X shifts the pattern, +// and Y chooses the repeat division. +// - CV In 1 always controls pattern shift. CV In 2 always controls +// reverse probability. X/Y become attenuators for patched CV inputs. // - Switch down: tap to reset to the first step. Hold for two seconds, // release, then tap once to clear or twice to save the kit. // @@ -27,11 +29,12 @@ #include "hardware/clocks.h" #include "hardware/sync.h" #include "pico/multicore.h" +#include "pico/platform.h" #include "pico/time.h" #include "tusb.h" -// Buffer size: 0.25 seconds at 48kHz -constexpr int kBufferLength = 12000; +// Buffer size: 0.4 seconds at 48kHz +constexpr int kBufferLength = 19200; constexpr int kNumSlots = 6; constexpr int kVariationBufferLength = kBufferLength * kNumSlots; constexpr int kNumPatterns = 21; @@ -47,6 +50,7 @@ constexpr int32_t kBootModeLatchSamples = 2400; // 50ms at 48kHz constexpr int32_t kBootModeWindowSamples = 24000; // 0.5s at 48kHz constexpr int32_t kVariationModeFeedbackSamples = 36000; // 0.75s at 48kHz constexpr int32_t kVariationFadeSamples = 96; // 2ms at 48kHz +constexpr int32_t kCVJackDebounceSamples = 480; // 10ms at 48kHz constexpr int kClearSamplesPerCore1Tick = 256; constexpr int32_t kKnobEditMoveThreshold = 32; constexpr int32_t kCVBipolarDetectThreshold = -64; @@ -54,6 +58,7 @@ constexpr int kPlaybackFracBits = 12; constexpr int32_t kPlaybackStepNormal = 1 << kPlaybackFracBits; constexpr int32_t kSamplePlaybackGainNum = 1; // Unity playback gain constexpr int32_t kSamplePlaybackGainDen = 1; +constexpr int32_t kRecordingSilenceThreshold = 32; constexpr int32_t kPitchBendRangeSemitones = 12; constexpr int32_t kPitchBendScale = (kPitchBendRangeSemitones * (1 << kPlaybackFracBits)) / 8192; @@ -70,6 +75,7 @@ constexpr uint8_t kSysExPing = 0x40; constexpr uint8_t kSysExPatternData = 0x50; constexpr uint8_t kSysExFactoryPatterns = 0x51; constexpr uint8_t kSysExCVConfig = 0x52; +constexpr uint8_t kSysExMonitorConfig = 0x53; constexpr uint8_t kSysExAck = 0x7E; constexpr uint8_t kAckPing = 0x00; constexpr uint8_t kAckImportBegin = 0x01; @@ -80,12 +86,27 @@ constexpr uint8_t kAckPatternLoaded = 0x05; constexpr uint8_t kAckPatternReject = 0x06; constexpr uint8_t kAckCVConfigLoaded = 0x07; constexpr uint8_t kAckCVConfigReject = 0x08; +constexpr uint8_t kAckMonitorConfigLoaded = 0x09; +constexpr uint8_t kAckMonitorConfigReject = 0x0A; constexpr uint32_t kFlashMagic = 0x31475246; // "FRG1" constexpr uint32_t kPatternFlashMagic = 0x31544150; // "PAT1" constexpr uint32_t kCVConfigFlashMagic = 0x31535643; // "CVS1" +constexpr uint32_t kMonitorConfigFlashMagic = 0x314E4F4D; // "MON1" constexpr uint32_t kFlashVersion = 1; -constexpr uint32_t kFlashStorageSize = 160 * 1024; +constexpr uint32_t kFlashStorageSize = 256 * 1024; constexpr uint32_t kFlashStorageOffset = PICO_FLASH_SIZE_BYTES - kFlashStorageSize; +constexpr int kLegacy350BufferLength = 16800; +constexpr uint32_t kLegacy350FlashStorageSize = 224 * 1024; +constexpr uint32_t kLegacy350FlashStorageOffset = + PICO_FLASH_SIZE_BYTES - kLegacy350FlashStorageSize; +constexpr int kLegacy300BufferLength = 14400; +constexpr uint32_t kLegacy300FlashStorageSize = 192 * 1024; +constexpr uint32_t kLegacy300FlashStorageOffset = + PICO_FLASH_SIZE_BYTES - kLegacy300FlashStorageSize; +constexpr int kLegacyV11BufferLength = 12000; +constexpr uint32_t kLegacyV11FlashStorageSize = 160 * 1024; +constexpr uint32_t kLegacyV11FlashStorageOffset = + PICO_FLASH_SIZE_BYTES - kLegacyV11FlashStorageSize; constexpr uint32_t kFlashProgramPageSize = FLASH_PAGE_SIZE; constexpr uint32_t kSysExBufferSize = 96; constexpr uint32_t kFlashKitBytes = @@ -96,6 +117,8 @@ constexpr uint32_t kFlashKitBytes = + (kNumPatterns * (1 + kMaxPatternLen)) + sizeof(uint32_t) + (2 * 4) + + 1 + + sizeof(uint32_t) + 1; static_assert(kFlashKitBytes <= kFlashStorageSize, "Saved kit exceeds flash storage"); @@ -110,6 +133,11 @@ constexpr int kModeOneShot = 1; constexpr int kModeInterrupt = 2; constexpr int kModePassthrough = 3; +constexpr uint8_t kMonitorAlways = 0; +constexpr uint8_t kMonitorWhenArmed = 1; +constexpr uint8_t kMonitorWhenRecording = 2; +constexpr uint8_t kMonitorModeCount = 3; + constexpr int kVariationNormal = 0; constexpr int kVariationOctaveUp = 1; constexpr int kVariationOctaveFifth = 2; @@ -122,7 +150,8 @@ constexpr uint8_t kCVRangeBipolar3V = 1; constexpr uint8_t kCVRangeBipolar15V = 2; constexpr uint8_t kCVRangeUnipolar6V = 3; constexpr uint8_t kCVRangeUnipolar3V = 4; -constexpr uint8_t kCVRangeCount = 5; +constexpr uint8_t kCVRangeUnipolar15V = 5; +constexpr uint8_t kCVRangeCount = 6; constexpr int32_t kCVRawFullScale = 2048; constexpr int32_t kCVMillivoltFullScale = 6000; constexpr int kCVSlewFracBits = 12; @@ -136,7 +165,10 @@ constexpr uint8_t kCVQuantMinorPentatonic = 5; constexpr uint8_t kCVQuantDorian = 6; constexpr uint8_t kCVQuantPelog = 7; constexpr uint8_t kCVQuantWholeTone = 8; -constexpr uint8_t kCVQuantCount = 9; +constexpr uint8_t kCVQuantOctaves = 9; +constexpr uint8_t kCVQuantFifthsOctaves = 10; +constexpr uint8_t kCVQuantFourthsFifthsOctaves = 11; +constexpr uint8_t kCVQuantCount = 12; constexpr uint8_t kCVClockDivCount = 5; // step, /2, /4, /8, /16 constexpr uint8_t kCVSlewCount = 6; // off, then progressively slower @@ -309,7 +341,7 @@ class Fragments : public ComputerCard } } - virtual void ProcessSample() + virtual void __not_in_flash_func(ProcessSample)() { sampleCounter_++; updatePulseOutputs(); @@ -342,6 +374,8 @@ class Fragments : public ComputerCard knobY = resolveKnobControl(2, knobY); bool audio1Connected = Connected(Input::Audio1); bool audio2Connected = Connected(Input::Audio2); + bool cv1Connected = debouncedCVConnected(0, Connected(Input::CV1)); + bool cv2Connected = debouncedCVConnected(1, Connected(Input::CV2)); Switch sw = SwitchVal(); bool switchDown = (sw == Switch::Down); @@ -349,6 +383,13 @@ class Fragments : public ComputerCard bool recordHeld = PulseIn2(); lastSwitchUp_ = (sw == Switch::Up); ZCommand zCommand = updateZCommand(switchDown); + monitorThisSample_ = shouldMonitorInput(sw, recordHeld); + monitorInputA_ = audio1Connected ? inA : 0; + monitorInputB_ = audio2Connected ? inB : 0; + monitorConnectedA_ = audio1Connected; + monitorConnectedB_ = audio2Connected; + updateAlwaysOnCVControls( + knobX, knobY, cv1in, cv2in, cv1Connected, cv2Connected); if (zCommand == kZCommandReset) { @@ -370,14 +411,16 @@ class Fragments : public ComputerCard if (variationMode_) { updateVariationPitchEditing(knobMain); - updateVariationSwitchUpEditing(knobX, knobY); + updateVariationSwitchUpEditing( + knobX, knobY, cv1Connected, cv2Connected); } else { updateSwitchUpSlotSelection(knobMain); // X/Y use pickup behavior, so changing switch position does // not overwrite a slot until the knob actually moves. - updateSwitchUpKnobEditing(selectedSlot_, knobX, knobY); + updateSwitchUpKnobEditing( + selectedSlot_, knobX, knobY, cv1Connected, cv2Connected); } } else @@ -498,8 +541,6 @@ class Fragments : public ComputerCard } } - setAudioOut(inA, inB); - // While recording, show the slot/tape being written. for (int i = 0; i < kNumSlots; i++) { @@ -510,12 +551,13 @@ class Fragments : public ComputerCard } if (recordHeld) { - // Recording monitors live input and skips playback. + // Recording keeps playback running, with the dry input mixed in + // by the monitor mixer below. } - else if (sw == Switch::Up && !clockFollowMode) + if (sw == Switch::Up && !clockFollowMode) { // Without an active clock, switch-up is live passthrough. - setAudioOut(inA, inB); + setAudioOutWithMonitor(inA, inB, true, true); } else { @@ -524,7 +566,8 @@ class Fragments : public ComputerCard if (sw == Switch::Middle) { updateMiddlePatternEditing(knobMain); - updateMiddleShiftDivideEditing(knobX, knobY, cv1in, cv2in); + updateMiddleShiftDivideEditing( + knobX, knobY, cv1Connected, cv2Connected); } else { @@ -584,7 +627,7 @@ class Fragments : public ComputerCard else if (mode == kModePassthrough) { // Passthrough mode ignores this slot's recording. - setAudioOut(inA, inB); + setAudioOutWithMonitor(inA, inB, true, true); } else if (stepFinished_) { @@ -592,7 +635,7 @@ class Fragments : public ComputerCard // returns to live audio after it finishes. if (mode == kModeInterrupt) { - setAudioOut(inA, inB); + setAudioOutWithMonitor(inA, inB, true, true); } else // kModeOneShot { @@ -716,7 +759,7 @@ class Fragments : public ComputerCard else { // Empty slots pass live audio. - setAudioOut(inA, inB); + setAudioOutWithMonitor(inA, inB, true, true); } } @@ -736,7 +779,8 @@ class Fragments : public ComputerCard activeVariation_ = shiftedValue; activeSlot_ = 0; bool flipReverse = - (int32_t)(nextRandom() & 0x0FFF) < variationReverseProb_; + (int32_t)(nextRandom() & 0x0FFF) + < activeVariationReverseProbability(); playReverse_ = variationIsReverse(shiftedValue) != flipReverse; } else @@ -835,7 +879,29 @@ class Fragments : public ComputerCard selectedSlot_ = led; } - void updateSwitchUpSlotSelection(int32_t knobMain) + bool __not_in_flash_func(debouncedCVConnected)(int input, bool rawConnected) + { + if (rawConnected == cvConnectedStable_[input]) + { + cvConnectedDebounceSamples_[input] = 0; + return cvConnectedStable_[input]; + } + + if (cvConnectedDebounceSamples_[input] < kCVJackDebounceSamples) + { + cvConnectedDebounceSamples_[input]++; + } + + if (cvConnectedDebounceSamples_[input] >= kCVJackDebounceSamples) + { + cvConnectedStable_[input] = rawConnected; + cvConnectedDebounceSamples_[input] = 0; + } + + return cvConnectedStable_[input]; + } + + void __not_in_flash_func(updateSwitchUpSlotSelection)(int32_t knobMain) { if (!switchUpMainPrimed_) { @@ -918,7 +984,7 @@ class Fragments : public ComputerCard } // Patched CV starts unipolar. If it dips below 0V, treat it as bipolar. - static int32_t attenuatedAutoCVControl( + static int32_t __not_in_flash_func(attenuatedAutoCVControl)( int32_t cv, int32_t amount, bool &seenNegative, @@ -940,7 +1006,7 @@ class Fragments : public ComputerCard : attenuatedUnipolarCVControl(cv, amount); } - static int32_t attenuatedBipolarCVControl(int32_t cv, int32_t amount) + static int32_t __not_in_flash_func(attenuatedBipolarCVControl)(int32_t cv, int32_t amount) { if (cv < -2048) cv = -2048; if (cv > 2047) cv = 2047; @@ -953,7 +1019,7 @@ class Fragments : public ComputerCard return control; } - static int32_t attenuatedUnipolarCVControl(int32_t cv, int32_t amount) + static int32_t __not_in_flash_func(attenuatedUnipolarCVControl)(int32_t cv, int32_t amount) { if (cv < 0) cv = 0; if (cv > 2047) cv = 2047; @@ -966,7 +1032,44 @@ class Fragments : public ComputerCard return control; } - void updateSwitchUpKnobEditing(int slot, int32_t knobX, int32_t knobY) + void __not_in_flash_func(updateAlwaysOnCVControls)( + int32_t knobX, + int32_t knobY, + int32_t cv1in, + int32_t cv2in, + bool cv1Connected, + bool cv2Connected) + { + if (cv1Connected) + { + int32_t shiftControl = attenuatedAutoCVControl( + cv1in, knobX, cv1SeenNegative_, true); + setShiftFromControl(shiftControl); + } + else + { + cv1SeenNegative_ = false; + } + + if (cv2Connected) + { + cvReverseProb_ = attenuatedAutoCVControl( + cv2in, knobY, cv2SeenNegative_, true); + cvReverseOverrideActive_ = true; + } + else + { + cv2SeenNegative_ = false; + cvReverseOverrideActive_ = false; + } + } + + void __not_in_flash_func(updateSwitchUpKnobEditing)( + int slot, + int32_t knobX, + int32_t knobY, + bool cv1Connected, + bool cv2Connected) { if (!switchUpKnobsPrimed_) { @@ -979,7 +1082,11 @@ class Fragments : public ComputerCard int32_t dx = knobX - lastKnobX_; if (dx < 0) dx = -dx; - if (dx >= kKnobEditMoveThreshold) + if (cv1Connected) + { + lastKnobX_ = knobX; + } + else if (dx >= kKnobEditMoveThreshold) { int mode = (int)(knobX >> 10); // 0-3 if (mode < 0) mode = 0; @@ -990,14 +1097,22 @@ class Fragments : public ComputerCard int32_t dy = knobY - lastKnobY_; if (dy < 0) dy = -dy; - if (dy >= kKnobEditMoveThreshold) + if (cv2Connected) + { + lastKnobY_ = knobY; + } + else if (dy >= kKnobEditMoveThreshold) { reverseProb_[slot] = knobY; lastKnobY_ = knobY; } } - void updateVariationSwitchUpEditing(int32_t knobX, int32_t knobY) + void __not_in_flash_func(updateVariationSwitchUpEditing)( + int32_t knobX, + int32_t knobY, + bool cv1Connected, + bool cv2Connected) { if (!switchUpKnobsPrimed_) { @@ -1010,7 +1125,11 @@ class Fragments : public ComputerCard int32_t dx = knobX - lastKnobX_; if (dx < 0) dx = -dx; - if (dx >= kKnobEditMoveThreshold) + if (cv1Connected) + { + lastKnobX_ = knobX; + } + else if (dx >= kKnobEditMoveThreshold) { int mode = (int)(knobX >> 10); // 0-3 if (mode < 0) mode = 0; @@ -1021,14 +1140,18 @@ class Fragments : public ComputerCard int32_t dy = knobY - lastKnobY_; if (dy < 0) dy = -dy; - if (dy >= kKnobEditMoveThreshold) + if (cv2Connected) + { + lastKnobY_ = knobY; + } + else if (dy >= kKnobEditMoveThreshold) { variationReverseProb_ = knobY; lastKnobY_ = knobY; } } - void updateMiddlePatternEditing(int32_t knobMain) + void __not_in_flash_func(updateMiddlePatternEditing)(int32_t knobMain) { if (!middleMainPrimed_) { @@ -1051,15 +1174,12 @@ class Fragments : public ComputerCard lastMiddleMain_ = knobMain; } - void updateMiddleShiftDivideEditing( + void __not_in_flash_func(updateMiddleShiftDivideEditing)( int32_t knobX, int32_t knobY, - int32_t cv1in, - int32_t cv2in) + bool cv1Connected, + bool cv2Connected) { - bool cv1Connected = Connected(Input::CV1); - bool cv2Connected = Connected(Input::CV2); - if (!middleXYPrimed_) { lastMiddleKnobX_ = knobX; @@ -1070,9 +1190,6 @@ class Fragments : public ComputerCard if (cv1Connected) { - int32_t shiftControl = attenuatedAutoCVControl( - cv1in, knobX, cv1SeenNegative_, true); - setShiftFromControl(shiftControl); lastMiddleKnobX_ = knobX; } else @@ -1080,7 +1197,6 @@ class Fragments : public ComputerCard // No CV patched: X is direct shift control, but only after // it moves in middle mode. This prevents a mode jump when // returning from switch-up slot selection. - cv1SeenNegative_ = false; int32_t dx = knobX - lastMiddleKnobX_; if (dx < 0) dx = -dx; if (dx >= kKnobEditMoveThreshold) @@ -1092,9 +1208,6 @@ class Fragments : public ComputerCard if (cv2Connected) { - int32_t divideControl = attenuatedAutoCVControl( - cv2in, knobY, cv2SeenNegative_, true); - setSubdivisionFromControl(divideControl); lastMiddleKnobY_ = knobY; } else @@ -1102,7 +1215,6 @@ class Fragments : public ComputerCard // No CV patched: Y is direct divide control, but only // after movement. This avoids accidental repeat/divide // jumps when moving between switch positions. - cv2SeenNegative_ = false; int32_t dy = knobY - lastMiddleKnobY_; if (dy < 0) dy = -dy; if (dy >= kKnobEditMoveThreshold) @@ -1113,7 +1225,7 @@ class Fragments : public ComputerCard } } - void setShiftFromControl(int32_t control) + void __not_in_flash_func(setShiftFromControl)(int32_t control) { int shift = (int)((control * kNumSlots) >> 12); if (shift < 0) shift = 0; @@ -1121,7 +1233,7 @@ class Fragments : public ComputerCard shiftAmount_ = shift; } - void setSubdivisionFromControl(int32_t control) + void __not_in_flash_func(setSubdivisionFromControl)(int32_t control) { int subShift = (int)(control >> 10); if (subShift < 0) subShift = 0; @@ -1140,7 +1252,7 @@ class Fragments : public ComputerCard } } - void updatePulseOutputs() + void __not_in_flash_func(updatePulseOutputs)() { if (pulseOut1Samples_ > 0) { @@ -1190,12 +1302,12 @@ class Fragments : public ComputerCard } } - int32_t randomSigned12Bit() + int32_t __not_in_flash_func(randomSigned12Bit)() { return (int32_t)(nextRandom() & 0x0FFF) - 2048; } - int32_t makeRandomCVValue(int output) + int32_t __not_in_flash_func(makeRandomCVValue)(int output) { int32_t value = applyCVRange(randomSigned12Bit(), cvRange_[output]); value = quantizeCV(value, cvQuant_[output]); @@ -1218,6 +1330,9 @@ class Fragments : public ComputerCard case kCVRangeUnipolar3V: return (value + 2048) >> 2; + case kCVRangeUnipolar15V: + return (value + 2048) >> 3; + case kCVRangeBipolar6V: default: return value; @@ -1246,6 +1361,9 @@ class Fragments : public ComputerCard static const int dorianScale[7] = {0, 2, 3, 5, 7, 9, 10}; static const int pelogScale[5] = {0, 1, 3, 7, 8}; static const int wholeToneScale[6] = {0, 2, 4, 6, 8, 10}; + static const int octaveScale[1] = {0}; + static const int fifthsOctavesScale[2] = {0, 7}; + static const int fourthsFifthsOctavesScale[3] = {0, 5, 7}; const int *scale = majorScale; int length = 7; @@ -1277,6 +1395,21 @@ class Fragments : public ComputerCard scale = wholeToneScale; length = 6; } + else if (quant == kCVQuantOctaves) + { + scale = octaveScale; + length = 1; + } + else if (quant == kCVQuantFifthsOctaves) + { + scale = fifthsOctavesScale; + length = 2; + } + else if (quant == kCVQuantFourthsFifthsOctaves) + { + scale = fourthsFifthsOctavesScale; + length = 3; + } int best = semitone; int bestDistance = 128; @@ -1299,7 +1432,7 @@ class Fragments : public ComputerCard return best; } - int32_t divRound(int32_t numerator, int32_t denominator) + int32_t __not_in_flash_func(divRound)(int32_t numerator, int32_t denominator) { if (numerator >= 0) { @@ -1308,14 +1441,14 @@ class Fragments : public ComputerCard return -((-numerator + denominator / 2) / denominator); } - int32_t clampSigned12Bit(int32_t value) + int32_t __not_in_flash_func(clampSigned12Bit)(int32_t value) { if (value < -2048) return -2048; if (value > 2047) return 2047; return value; } - int32_t randomCVToMillivolts(int32_t value) + int32_t __not_in_flash_func(randomCVToMillivolts)(int32_t value) { value = clampSigned12Bit(value); int32_t millivolts = divRound(value * kCVMillivoltFullScale, kCVRawFullScale); @@ -1331,7 +1464,7 @@ class Fragments : public ComputerCard return millivolts; } - void writeRandomCVOutput(int output, int32_t value) + void __not_in_flash_func(writeRandomCVOutput)(int output, int32_t value) { value = clampSigned12Bit(value); if (lastRandomCVOutput_[output] == value) @@ -1352,18 +1485,25 @@ class Fragments : public ComputerCard } } - uint8_t cvClockDivisor(int output) + uint8_t __not_in_flash_func(cvClockDivisor)(int output) { return 1u << cvClockDiv_[output]; } - uint8_t cvSlewShift(int output) + uint8_t __not_in_flash_func(cvSlewShift)(int output) { - static const uint8_t shifts[kCVSlewCount] = {0, 9, 10, 11, 12, 14}; - return shifts[cvSlew_[output]]; + switch (cvSlew_[output]) + { + case 1: return 9; + case 2: return 10; + case 3: return 11; + case 4: return 12; + case 5: return 14; + default: return 0; + } } - void updateRandomCVOutputs() + void __not_in_flash_func(updateRandomCVOutputs)() { for (int output = 0; output < 2; output++) { @@ -1598,6 +1738,10 @@ class Fragments : public ComputerCard handleCVConfig(payload, size); break; + case kSysExMonitorConfig: + handleMonitorConfig(payload, size); + break; + default: break; } @@ -1732,6 +1876,18 @@ class Fragments : public ComputerCard (uint16_t)(range | (quant << 3) | (div << 7) | (slew << 10))); } + void handleMonitorConfig(uint8_t *data, uint32_t size) + { + if (size < 1 || data[0] >= kMonitorModeCount) + { + sendSysExAck(kAckMonitorConfigReject, 0, 0); + return; + } + + monitorMode_ = data[0]; + sendSysExAck(kAckMonitorConfigLoaded, monitorMode_, 0); + } + void handleImportBegin(uint8_t *data, uint32_t size) { if (size < 4) @@ -2167,7 +2323,7 @@ class Fragments : public ComputerCard return total; } - void outputSlotSampleOrLive( + void __not_in_flash_func(outputSlotSampleOrLive)( int slot, int32_t inA, int32_t inB, @@ -2201,11 +2357,19 @@ class Fragments : public ComputerCard } } - setAudioOut(outA, outB); + bool includesLiveInputA = !variationMode_ && !channelA; + bool includesLiveInputB = !variationMode_ && !channelB; + setAudioOutWithMonitor(outA, outB, includesLiveInputA, includesLiveInputB); } - int8_t audioToStoredSample(int32_t sample) + int8_t __not_in_flash_func(audioToStoredSample)(int32_t sample) { + if (sample > -kRecordingSilenceThreshold + && sample < kRecordingSilenceThreshold) + { + return 0; + } + int32_t stored = sample >> 4; if (stored < -128) return -128; if (stored > 127) return 127; @@ -2269,7 +2433,7 @@ class Fragments : public ComputerCard : interpolatedBufferSample(bufferB_[slot], recordedLengthB_[slot]); } - int32_t interpolatedBufferSample(int8_t *buffer, int32_t length) + int32_t __not_in_flash_func(interpolatedBufferSample)(int8_t *buffer, int32_t length) { if (length <= 0) { @@ -2292,7 +2456,7 @@ class Fragments : public ComputerCard return sample + (((nextSample - sample) * frac) >> kPlaybackFracBits); } - int32_t interpolatedVariationSample( + int32_t __not_in_flash_func(interpolatedVariationSample)( int8_t buffer[kNumSlots][kBufferLength], int32_t length) { @@ -2342,7 +2506,7 @@ class Fragments : public ComputerCard buffer[slot][offset] = sample; } - int32_t smoothVariationEdge(int slot, int32_t sample, bool recordedChannel) + int32_t __not_in_flash_func(smoothVariationEdge)(int slot, int32_t sample, bool recordedChannel) { if (!recordedChannel) { @@ -2381,7 +2545,7 @@ class Fragments : public ComputerCard return (sample * gain) / fade; } - int32_t crossfadeVariationRetrigger(int32_t target, int32_t start) + int32_t __not_in_flash_func(crossfadeVariationRetrigger)(int32_t target, int32_t start) { int32_t fade = kVariationFadeSamples; int32_t elapsed = fade - variationFadeSamples_; @@ -2398,6 +2562,53 @@ class Fragments : public ComputerCard AudioOut2(outB); } + void __not_in_flash_func(setAudioOutWithMonitor)( + int32_t outA, + int32_t outB, + bool outputAlreadyIncludesInputA, + bool outputAlreadyIncludesInputB) + { + if (monitorThisSample_) + { + if (monitorConnectedA_ && !outputAlreadyIncludesInputA) + { + outA = mixMonitorInput(outA, monitorInputA_); + } + if (monitorConnectedB_ && !outputAlreadyIncludesInputB) + { + outB = mixMonitorInput(outB, monitorInputB_); + } + } + + setAudioOut(outA, outB); + } + + bool shouldMonitorInput(Switch sw, bool recordHeld) const + { + switch (monitorMode_) + { + case kMonitorAlways: + return true; + case kMonitorWhenRecording: + return recordHeld; + case kMonitorWhenArmed: + default: + return sw == Switch::Up || recordHeld; + } + } + + static int32_t clampAudioOutput(int32_t sample) + { + if (sample < -2048) return -2048; + if (sample > 2047) return 2047; + return sample; + } + + static int32_t mixMonitorInput(int32_t program, int32_t monitor) + { + return clampAudioOutput((program + monitor) >> 1); + } + void outputSlotSilenceOrLive(int slot, int32_t inA, int32_t inB) { outputSlotSampleOrLive(slot, inA, inB, 0, 0); @@ -2463,12 +2674,46 @@ class Fragments : public ComputerCard } writeFlashByte(cv2CoupledToCV1_ ? 1 : 0); + writeFlashU32(kMonitorConfigFlashMagic); + writeFlashByte(monitorMode_); + flushFlashPage(); } void loadKitFromFlash() { - const uint8_t *read = (const uint8_t *)(XIP_BASE + kFlashStorageOffset); + if (loadKitFromFlashOffset(kFlashStorageOffset, kBufferLength)) + { + return; + } + + if (kBufferLength != kLegacy350BufferLength + && loadKitFromFlashOffset( + kLegacy350FlashStorageOffset, + kLegacy350BufferLength)) + { + return; + } + + if (kBufferLength != kLegacy300BufferLength + && loadKitFromFlashOffset( + kLegacy300FlashStorageOffset, + kLegacy300BufferLength)) + { + return; + } + + if (kBufferLength != kLegacyV11BufferLength) + { + loadKitFromFlashOffset( + kLegacyV11FlashStorageOffset, + kLegacyV11BufferLength); + } + } + + bool loadKitFromFlashOffset(uint32_t storageOffset, uint32_t expectedBufferLength) + { + const uint8_t *read = (const uint8_t *)(XIP_BASE + storageOffset); uint32_t magic = readFlashU32(read); uint32_t version = readFlashU32(read); @@ -2477,10 +2722,16 @@ class Fragments : public ComputerCard if (magic != kFlashMagic || version != kFlashVersion - || bufferLength != (uint32_t)kBufferLength + || bufferLength != expectedBufferLength || numSlots != (uint32_t)kNumSlots) { - return; + return false; + } + + int32_t copyLength = (int32_t)bufferLength; + if (copyLength > kBufferLength) + { + copyLength = kBufferLength; } for (int slot = 0; slot < kNumSlots; slot++) @@ -2497,14 +2748,25 @@ class Fragments : public ComputerCard for (int slot = 0; slot < kNumSlots; slot++) { - for (int i = 0; i < kBufferLength; i++) + for (int i = 0; i < copyLength; i++) { bufferA_[slot][i] = (int8_t)*read++; } - for (int i = 0; i < kBufferLength; i++) + read += bufferLength - copyLength; + for (int i = copyLength; i < kBufferLength; i++) + { + bufferA_[slot][i] = 0; + } + + for (int i = 0; i < copyLength; i++) { bufferB_[slot][i] = (int8_t)*read++; } + read += bufferLength - copyLength; + for (int i = copyLength; i < kBufferLength; i++) + { + bufferB_[slot][i] = 0; + } } if (readFlashU32(read) == kPatternFlashMagic) @@ -2572,6 +2834,15 @@ class Fragments : public ComputerCard } } + if (readFlashU32(read) == kMonitorConfigFlashMagic) + { + uint8_t monitor = *read++; + if (monitor < kMonitorModeCount) + { + monitorMode_ = monitor; + } + } + writeIndex_ = 0; playIndex_ = 0; playPositionQ_ = 0; @@ -2581,6 +2852,7 @@ class Fragments : public ComputerCard inSilenceFill_ = false; recordingActive_ = false; recomputeVariationRecordingState(); + return true; } void writeFlashU32(uint32_t value) @@ -2689,11 +2961,21 @@ class Fragments : public ComputerCard void rollReverse() { uint32_t roll = nextRandom() & 0x0FFF; // 0-4095 - playReverse_ = (int32_t)roll < reverseProb_[activeSlot_]; + playReverse_ = (int32_t)roll < activeSlotReverseProbability(activeSlot_); + } + + int32_t activeSlotReverseProbability(int slot) const + { + return cvReverseOverrideActive_ ? cvReverseProb_ : reverseProb_[slot]; + } + + int32_t activeVariationReverseProbability() const + { + return cvReverseOverrideActive_ ? cvReverseProb_ : variationReverseProb_; } // Small, fast random-number generator for musical variation. - uint32_t nextRandom() + uint32_t __not_in_flash_func(nextRandom)() { uint32_t x = rngState_; x ^= x << 13; @@ -2808,6 +3090,8 @@ class Fragments : public ComputerCard bool bufferFull_ = false; bool cv1SeenNegative_ = false; bool cv2SeenNegative_ = false; + bool cvConnectedStable_[2] = {false, false}; + int32_t cvConnectedDebounceSamples_[2] = {0, 0}; int32_t zHoldSamples_ = 0; volatile bool zCommandArmed_ = false; bool zCommandNeedsRelease_ = false; @@ -2842,6 +3126,12 @@ class Fragments : public ComputerCard volatile uint8_t cvClockDiv_[2] = {0, 0}; volatile uint8_t cvSlew_[2] = {0, 3}; volatile bool cv2CoupledToCV1_ = false; + volatile uint8_t monitorMode_ = kMonitorWhenArmed; + bool monitorThisSample_ = false; + bool monitorConnectedA_ = false; + bool monitorConnectedB_ = false; + int32_t monitorInputA_ = 0; + int32_t monitorInputB_ = 0; volatile uint32_t playbackStepQ_ = kPlaybackStepNormal; Pattern patterns_[kNumPatterns] = {}; volatile bool midiKnobActive_[3] = {false, false, false}; @@ -2910,6 +3200,8 @@ class Fragments : public ComputerCard volatile int reverseProb_[kNumSlots] = {0}; int variationPlaybackMode_ = kModeOneShot; volatile int variationReverseProb_ = 0; + volatile bool cvReverseOverrideActive_ = false; + volatile int32_t cvReverseProb_ = 0; // xorshift32 must not be seeded with zero. uint32_t rngState_ = 0x9F3779B9u; diff --git a/releases/67_Fragments/src/fragments_24k.cpp b/releases/67_Fragments/src/fragments_24k.cpp new file mode 100644 index 000000000..82754917f --- /dev/null +++ b/releases/67_Fragments/src/fragments_24k.cpp @@ -0,0 +1,3327 @@ +// Fragments +// +// A six-slot audio fragment recorder and sequencer for the Workshop +// Computer. Pulse In 1 clocks the pattern, Pulse In 2 records into the +// selected slot, and the Z switch moves between performance modes. +// +// Controls: +// - Switch up: choose the recording slot with Main. X chooses that +// slot's playback mode; Y sets its chance of playing backward. +// - Switch middle: Main chooses the pattern. X shifts the pattern, +// and Y chooses the repeat division. +// - CV In 1 always controls pattern shift. CV In 2 always controls +// reverse probability. X/Y become attenuators for patched CV inputs. +// - Switch down: tap to reset to the first step. Hold for two seconds, +// release, then tap once to clear or twice to save the kit. +// +// Playback modes per slot: +// - Looping: repeat the fragment for the whole step. +// - One Shot: play once, then go silent until the next step. +// - Interrupt: play once, then return to live audio. +// - Passthrough: ignore the recording and pass live audio. +// +// The fast audio work stays in ProcessSample() on core 0. Core 1 handles +// slower jobs such as LED updates, step decisions, recording cleanup, +// and other work that should not interrupt the audio loop. + +#include "ComputerCard.h" +#include "hardware/flash.h" +#include "hardware/clocks.h" +#include "hardware/sync.h" +#include "pico/multicore.h" +#include "pico/platform.h" +#include "pico/time.h" +#include "tusb.h" + +constexpr int kAudioSampleRate = 48000; +constexpr int kStoredSampleRate = 24000; +constexpr int kStoredSamplesPerAudioSample = kAudioSampleRate / kStoredSampleRate; +static_assert( + kAudioSampleRate % kStoredSampleRate == 0, + "Stored sample rate must divide the audio interrupt rate"); + +// Buffer size: 0.8 seconds at 24kHz +constexpr int kBufferLength = 19200; +constexpr int kNumSlots = 6; +constexpr int kVariationBufferLength = kBufferLength * kNumSlots; +constexpr int kNumPatterns = 21; +constexpr int kMaxPatternLen = 16; +constexpr int kPulseTriggerSamples = 480; // 10ms at 48kHz +constexpr int32_t kMinRecordingSamples = kStoredSampleRate / 100; // 10ms +constexpr int32_t kClockActivityHoldSamples = 192000; // 4s at 48kHz +constexpr int32_t kSaveFeedbackSamples = 24000; // 0.5s at 48kHz +constexpr int32_t kClearHoldSamples = 96000; // 2s at 48kHz +constexpr int32_t kCommandTimeoutSamples = 48000; // 1s at 48kHz +constexpr int32_t kDoubleTapSamples = 7200; // 150ms at 48kHz +constexpr int32_t kBootModeLatchSamples = 2400; // 50ms at 48kHz +constexpr int32_t kBootModeWindowSamples = 24000; // 0.5s at 48kHz +constexpr int32_t kVariationModeFeedbackSamples = 36000; // 0.75s at 48kHz +constexpr int32_t kVariationFadeSamples = 96; // 2ms at 48kHz +constexpr int32_t kCVJackDebounceSamples = 480; // 10ms at 48kHz +constexpr int kClearSamplesPerCore1Tick = 256; +constexpr int32_t kKnobEditMoveThreshold = 32; +constexpr int32_t kCVBipolarDetectThreshold = -64; +constexpr int kPlaybackFracBits = 12; +constexpr int32_t kPlaybackRatioUnity = 1 << kPlaybackFracBits; +constexpr int32_t kPlaybackStepNormal = + kPlaybackRatioUnity / kStoredSamplesPerAudioSample; +constexpr int32_t kSamplePlaybackGainNum = 1; // Unity playback gain +constexpr int32_t kSamplePlaybackGainDen = 1; +constexpr int32_t kRecordingSilenceThreshold = 32; +constexpr int32_t kPitchBendRangeSemitones = 12; +constexpr int32_t kPitchBendScale = + (kPitchBendRangeSemitones * (1 << kPlaybackFracBits)) / 8192; +constexpr int32_t kVariationPitchRangeSemitones = 24; +constexpr int32_t kVariationPitchDeadband = 160; +constexpr int32_t kVariationPitchSnapWindowQ = 2 * (1 << (kPlaybackFracBits - 2)); // 0.5 semitones +constexpr uint8_t kSysExManufacturer = 0x7D; +constexpr uint8_t kSysExImportBegin = 0x20; +constexpr uint8_t kSysExImportData = 0x21; +constexpr uint8_t kSysExImportEnd = 0x22; +constexpr uint8_t kSysExSaveKit = 0x30; +constexpr uint8_t kSysExLoadKit = 0x31; +constexpr uint8_t kSysExPing = 0x40; +constexpr uint8_t kSysExPatternData = 0x50; +constexpr uint8_t kSysExFactoryPatterns = 0x51; +constexpr uint8_t kSysExCVConfig = 0x52; +constexpr uint8_t kSysExMonitorConfig = 0x53; +constexpr uint8_t kSysExAck = 0x7E; +constexpr uint8_t kAckPing = 0x00; +constexpr uint8_t kAckImportBegin = 0x01; +constexpr uint8_t kAckImportReject = 0x02; +constexpr uint8_t kAckImportEnd = 0x03; +constexpr uint8_t kAckImportShort = 0x04; +constexpr uint8_t kAckPatternLoaded = 0x05; +constexpr uint8_t kAckPatternReject = 0x06; +constexpr uint8_t kAckCVConfigLoaded = 0x07; +constexpr uint8_t kAckCVConfigReject = 0x08; +constexpr uint8_t kAckMonitorConfigLoaded = 0x09; +constexpr uint8_t kAckMonitorConfigReject = 0x0A; +constexpr uint32_t kFlashMagic = 0x31475246; // "FRG1" +constexpr uint32_t kPatternFlashMagic = 0x31544150; // "PAT1" +constexpr uint32_t kCVConfigFlashMagic = 0x31535643; // "CVS1" +constexpr uint32_t kMonitorConfigFlashMagic = 0x314E4F4D; // "MON1" +constexpr uint32_t kFlashVersion = 2; +constexpr uint32_t kFlashStorageSize = 256 * 1024; +constexpr uint32_t kFlashStorageOffset = PICO_FLASH_SIZE_BYTES - kFlashStorageSize; +constexpr int kLegacy350BufferLength = 16800; +constexpr uint32_t kLegacy350FlashStorageSize = 224 * 1024; +constexpr uint32_t kLegacy350FlashStorageOffset = + PICO_FLASH_SIZE_BYTES - kLegacy350FlashStorageSize; +constexpr int kLegacy300BufferLength = 14400; +constexpr uint32_t kLegacy300FlashStorageSize = 192 * 1024; +constexpr uint32_t kLegacy300FlashStorageOffset = + PICO_FLASH_SIZE_BYTES - kLegacy300FlashStorageSize; +constexpr int kLegacyV11BufferLength = 12000; +constexpr uint32_t kLegacyV11FlashStorageSize = 160 * 1024; +constexpr uint32_t kLegacyV11FlashStorageOffset = + PICO_FLASH_SIZE_BYTES - kLegacyV11FlashStorageSize; +constexpr uint32_t kFlashProgramPageSize = FLASH_PAGE_SIZE; +constexpr uint32_t kSysExBufferSize = 96; +constexpr uint32_t kFlashKitBytes = + 16 + + (kNumSlots * 7 * sizeof(uint32_t)) + + (kNumSlots * 2 * kBufferLength) + + sizeof(uint32_t) + + (kNumPatterns * (1 + kMaxPatternLen)) + + sizeof(uint32_t) + + (2 * 4) + + 1 + + sizeof(uint32_t) + + 1; +static_assert(kFlashKitBytes <= kFlashStorageSize, "Saved kit exceeds flash storage"); + +constexpr uint32_t kSemitoneSteps[12] = { + 4096, 4339, 4598, 4871, 5161, 5468, + 5793, 6137, 6502, 6889, 7298, 7732 +}; + +// Per-slot playback modes. X chooses one of these while the switch is up. +constexpr int kModeLooping = 0; +constexpr int kModeOneShot = 1; +constexpr int kModeInterrupt = 2; +constexpr int kModePassthrough = 3; + +constexpr uint8_t kMonitorAlways = 0; +constexpr uint8_t kMonitorWhenArmed = 1; +constexpr uint8_t kMonitorWhenRecording = 2; +constexpr uint8_t kMonitorModeCount = 3; + +constexpr int kVariationNormal = 0; +constexpr int kVariationOctaveUp = 1; +constexpr int kVariationOctaveFifth = 2; +constexpr int kVariationReverse = 3; +constexpr int kVariationReverseOctaveUp = 4; +constexpr int kVariationOctaveDown = 5; + +constexpr uint8_t kCVRangeBipolar6V = 0; +constexpr uint8_t kCVRangeBipolar3V = 1; +constexpr uint8_t kCVRangeBipolar15V = 2; +constexpr uint8_t kCVRangeUnipolar6V = 3; +constexpr uint8_t kCVRangeUnipolar3V = 4; +constexpr uint8_t kCVRangeUnipolar15V = 5; +constexpr uint8_t kCVRangeCount = 6; +constexpr int32_t kCVRawFullScale = 2048; +constexpr int32_t kCVMillivoltFullScale = 6000; +constexpr int kCVSlewFracBits = 12; + +constexpr uint8_t kCVQuantOff = 0; +constexpr uint8_t kCVQuantChromatic = 1; +constexpr uint8_t kCVQuantMajor = 2; +constexpr uint8_t kCVQuantMinor = 3; +constexpr uint8_t kCVQuantMajorPentatonic = 4; +constexpr uint8_t kCVQuantMinorPentatonic = 5; +constexpr uint8_t kCVQuantDorian = 6; +constexpr uint8_t kCVQuantPelog = 7; +constexpr uint8_t kCVQuantWholeTone = 8; +constexpr uint8_t kCVQuantOctaves = 9; +constexpr uint8_t kCVQuantFifthsOctaves = 10; +constexpr uint8_t kCVQuantFourthsFifthsOctaves = 11; +constexpr uint8_t kCVQuantCount = 12; + +constexpr uint8_t kCVClockDivCount = 5; // step, /2, /4, /8, /16 +constexpr uint8_t kCVSlewCount = 6; // off, then progressively slower + +enum ZCommand +{ + kZCommandNone, + kZCommandReset, + kZCommandSave, + kZCommandClear +}; + +// A pattern is a sequence of slot numbers. Length says how many entries +// in steps[] are active; the rest are just padding. +struct Pattern +{ + uint8_t steps[kMaxPatternLen]; + uint8_t length; +}; + +static const Pattern factoryPatterns[kNumPatterns] = { + /* 0 */ {{0}, 1}, + /* 1 */ {{0, 1}, 2}, + /* 2 */ {{0, 1, 2}, 3}, + /* 3 */ {{0, 1, 2, 3}, 4}, + /* 4 */ {{0, 1, 2, 4}, 4}, + /* 5 */ {{0, 1, 2, 5}, 4}, + /* 6 */ {{0, 1, 2, 3, 4, 5}, 6}, + /* 7 */ {{5, 4, 3, 2, 1, 0}, 6}, + /* 8 */ {{0, 0, 2, 0}, 4}, + /* 9 */ {{0, 1, 2, 0}, 4}, + /* 10 */ {{0, 1, 2, 2}, 4}, + /* 11 */ {{0, 1, 2, 2, 0, 2, 3, 1}, 8}, + /* 12 */ {{0, 0, 1, 1}, 4}, + /* 13 */ {{0, 0, 2, 2}, 4}, + /* 14 */ {{0, 2, 4, 2}, 4}, + /* 15 */ {{1, 3, 5, 3}, 4}, + /* 16 */ {{0, 2, 4, 1, 3, 5}, 6}, + /* 17 */ {{0, 1, 2, 1, 2, 3, 2, 3, 4, 3, 4, 5}, 12}, + /* 18 */ {{5, 4, 3, 4, 3, 2, 3, 2, 1, 2, 1, 0}, 12}, + /* 19 */ {{0, 1, 2, 3, 4, 5, 1}, 7}, + /* 20 */ {{0, 1, 2, 3, 4, 5, 5, 4, 3, 2, 1, 0}, 12}, +}; + +class Fragments : public ComputerCard +{ +public: + Fragments() + { + loadFactoryPatterns(); + loadKitFromFlash(); + + // The probe lets us tell which inputs are patched, and also + // keeps the probe signal aligned with ComputerCard's ADC reads. + EnableNormalisationProbe(); + + CVOut1Millivolts(0); + CVOut2Millivolts(0); + } + + // Core 1 handles slow control work so ProcessSample() can stay lean. + void Core1() + { + sleep_us(150000); + + USBPowerState_t powerState = USBPowerState(); + isUSBMIDIHost_ = (powerState == DFP); + if (isUSBMIDIHost_) + { + tuh_init(TUH_OPT_RHPORT); + } + else + { + tud_init(TUD_OPT_RHPORT); + } + + while (true) + { + if (isUSBMIDIHost_) + { + tuh_task(); + } + else + { + tud_task(); + serviceUSBMIDI(); + } + serviceClearRequest(); + serviceRecordingFinalize(); + serviceFlashSave(); + + // Step/reset requests are set by core 0 when it sees pulses. + if (resetEdge_) + { + resetEdge_ = false; + stepIndex_ = 0; + applyStepChange(); + triggerStepPulses(true); + } + else if (clockEdge_) + { + clockEdge_ = false; + + // Measure the clock period in samples for repeat divisions. + uint32_t now = sampleCounter_; + if (haveClockPeriod_) + { + clockPeriodSamples_ = now - lastClockSample_; + updateRepeatLength(); + } + else + { + haveClockPeriod_ = true; + } + lastClockSample_ = now; + + stepIndex_++; + if (stepIndex_ >= patterns_[selectedPattern_].length) + { + stepIndex_ = 0; + } + applyStepChange(); + triggerStepPulses(stepIndex_ == 0); + } + + // Pattern changes keep the current step position when possible. + { + int currentPattern = selectedPattern_; + if (currentPattern != lastSeenPattern_) + { + lastSeenPattern_ = currentPattern; + + uint8_t newLen = patterns_[currentPattern].length; + if (newLen > 0) + { + stepIndex_ = stepIndex_ % newLen; + } + else + { + stepIndex_ = 0; + } + + applyStepChange(); + } + } + + // Shift changes remap the current step immediately. + { + int currentShift = shiftAmount_; + if (currentShift != lastSeenShift_) + { + lastSeenShift_ = currentShift; + applyStepChange(); + } + } + + // Division changes affect repeat length, not the selected slot. + { + int currentSubShift = subdivisionShift_; + if (currentSubShift != lastSeenSubShift_) + { + lastSeenSubShift_ = currentSubShift; + updateRepeatLength(); + } + } + + updateLeds(); + + sleep_us(100); + } + } + + virtual void __not_in_flash_func(ProcessSample)() + { + sampleCounter_++; + updatePulseOutputs(); + updateRandomCVOutputs(); + if (clockActivitySamples_ > 0) + { + clockActivitySamples_--; + } + if (saveFeedbackSamples_ > 0) + { + saveFeedbackSamples_--; + } + if (variationModeFeedbackSamples_ > 0) + { + variationModeFeedbackSamples_--; + } + + // Read each hardware input once per sample. ComputerCard's ADC + // mux expects a steady read pattern, so we cache everything here + // and use these local values below. + int32_t inA = AudioIn1(); + int32_t inB = AudioIn2(); + int32_t cv1in = CVIn1(); + int32_t cv2in = CVIn2(); + int32_t knobMain = KnobVal(Knob::Main); + int32_t knobX = KnobVal(Knob::X); + int32_t knobY = KnobVal(Knob::Y); + knobMain = resolveKnobControl(0, knobMain); + knobX = resolveKnobControl(1, knobX); + knobY = resolveKnobControl(2, knobY); + bool audio1Connected = Connected(Input::Audio1); + bool audio2Connected = Connected(Input::Audio2); + bool cv1Connected = debouncedCVConnected(0, Connected(Input::CV1)); + bool cv2Connected = debouncedCVConnected(1, Connected(Input::CV2)); + + Switch sw = SwitchVal(); + bool switchDown = (sw == Switch::Down); + updateBootModeLatch(switchDown); + bool recordHeld = PulseIn2(); + lastSwitchUp_ = (sw == Switch::Up); + ZCommand zCommand = updateZCommand(switchDown); + monitorThisSample_ = shouldMonitorInput(sw, recordHeld); + monitorInputA_ = audio1Connected ? inA : 0; + monitorInputB_ = audio2Connected ? inB : 0; + monitorConnectedA_ = audio1Connected; + monitorConnectedB_ = audio2Connected; + updateAlwaysOnCVControls( + knobX, knobY, cv1in, cv2in, cv1Connected, cv2Connected); + + if (zCommand == kZCommandReset) + { + resetEdge_ = true; + } + else if (zCommand == kZCommandSave && !recordHeld) + { + __mem_fence_release(); + flashSaveRequested_ = true; + } + else if (zCommand == kZCommandClear) + { + beginClearAll(); + } + + // Switch up: choose a slot and optionally edit its settings. + if (sw == Switch::Up) + { + if (variationMode_) + { + updateVariationPitchEditing(knobMain); + updateVariationSwitchUpEditing( + knobX, knobY, cv1Connected, cv2Connected); + } + else + { + updateSwitchUpSlotSelection(knobMain); + // X/Y use pickup behavior, so changing switch position does + // not overwrite a slot until the knob actually moves. + updateSwitchUpKnobEditing( + selectedSlot_, knobX, knobY, cv1Connected, cv2Connected); + } + } + else + { + switchUpMainPrimed_ = false; + switchUpKnobsPrimed_ = false; + } + + if (sw != Switch::Middle) + { + middleMainPrimed_ = false; + middleXYPrimed_ = false; + } + + // When recording ends, core 1 publishes the completed slot. + if (!recordHeld && wasRecording_) + { + int slot = recordingSlot_; + if (recordingHadInput_) + { + int32_t finalLength = bufferFull_ + ? (variationMode_ ? kVariationBufferLength : kBufferLength) + : writeIndex_; + if (finalLength >= kMinRecordingSamples) + { + pendingRecordSlot_ = slot; + pendingRecordLength_ = finalLength; + pendingRecordChannelA_ = recordingWriteA_; + pendingRecordChannelB_ = recordingWriteB_; + pendingRecordVariation_ = variationMode_; + __mem_fence_release(); + pendingRecordFinalize_ = true; + } + } + __mem_fence_release(); + recordingActive_ = false; + } + + // Pulse In 1 is the clock. Core 1 turns this flag into a step. + if (PulseIn1RisingEdge()) + { + clockEdge_ = true; + clockActivitySamples_ = kClockActivityHoldSamples; + } + + // After recent clocks, switch-up selects slots without stopping playback. + bool clockFollowMode = (clockActivitySamples_ > 0); + + if (recordHeld) + { + // Pulse In 2 records a slot in normal mode, or the long + // one-sample tape when booted into Variation mode. + int slot = variationMode_ ? 0 : selectedSlot_; + + // A defensive guard: this should never happen during normal use. + if (slot < 0 || slot >= kNumSlots) + { + for (int i = 0; i < 6; i++) LedOn(i); + setAudioOut(0, 0); + wasRecording_ = recordHeld; + lastSwitchDown_ = switchDown; + return; + } + + if (!wasRecording_) + { + recordingHadInput_ = audio1Connected || audio2Connected; + + if (recordingHadInput_) + { + recordingSlot_ = slot; + writeIndex_ = 0; + bufferFull_ = false; + recordDecimationPhase_ = 0; + recordAccumA_ = 0; + recordAccumB_ = 0; + recordingWriteA_ = audio1Connected; + recordingWriteB_ = audio2Connected; + } + } + + if (recordingHadInput_) + { + int32_t maxRecordLength = variationMode_ + ? kVariationBufferLength + : kBufferLength; + + if (!bufferFull_) + { + recordAccumA_ += inA; + recordAccumB_ += inB; + recordDecimationPhase_++; + + if (recordDecimationPhase_ >= kStoredSamplesPerAudioSample) + { + int32_t storedInputA = + recordAccumA_ / kStoredSamplesPerAudioSample; + int32_t storedInputB = + recordAccumB_ / kStoredSamplesPerAudioSample; + + if (writeIndex_ >= 0 && writeIndex_ < maxRecordLength) + { + if (recordingWriteA_) + { + if (variationMode_) + { + writeVariationSample( + bufferA_, + writeIndex_, + audioToStoredSample(storedInputA)); + } + else + { + bufferA_[slot][writeIndex_] = + audioToStoredSample(storedInputA); + } + } + if (recordingWriteB_) + { + if (variationMode_) + { + writeVariationSample( + bufferB_, + writeIndex_, + audioToStoredSample(storedInputB)); + } + else + { + bufferB_[slot][writeIndex_] = + audioToStoredSample(storedInputB); + } + } + } + + recordAccumA_ = 0; + recordAccumB_ = 0; + recordDecimationPhase_ = 0; + + if (writeIndex_ < maxRecordLength - 1) + { + writeIndex_++; + } + else + { + bufferFull_ = true; + } + } + } + } + + // While recording, show the slot/tape being written. + for (int i = 0; i < kNumSlots; i++) + { + if (i == slot) LedOn(i); else LedOff(i); + } + recordingActive_ = recordingHadInput_; + + } + if (recordHeld) + { + // Recording keeps playback running, with the dry input mixed in + // by the monitor mixer below. + } + if (sw == Switch::Up && !clockFollowMode) + { + // Without an active clock, switch-up is live passthrough. + setAudioOutWithMonitor(inA, inB, true, true); + } + else + { + // Pattern playback. In clock-follow switch-up, keep playing + // while the up-position controls select/edit the record slot. + if (sw == Switch::Middle) + { + updateMiddlePatternEditing(knobMain); + updateMiddleShiftDivideEditing( + knobX, knobY, cv1Connected, cv2Connected); + } + else + { + middleMainPrimed_ = false; + middleXYPrimed_ = false; + } + + int slot = activeSlot_; + + // A new step restarts playback and latches direction. + if (restartPlayback_) + { + restartPlayback_ = false; + if (variationMode_) + { + variationFadeSamples_ = kVariationFadeSamples; + variationFadeStartA_ = lastOutputA_; + variationFadeStartB_ = lastOutputB_; + } + int32_t currentLength = playbackLength(slot); + playDirReverse_ = playReverse_; + playIndex_ = playDirReverse_ + ? (currentLength > 0 ? currentLength - 1 : 0) + : 0; + playPositionQ_ = playIndex_ << kPlaybackFracBits; + repeatPosition_ = 0; + inSilenceFill_ = false; + stepFinished_ = false; + repeatsCompleted_ = 0; + } + + int32_t currentLength = playbackLength(slot); + int mode = playbackModeForSlot(slot); + + // Keep the playback head inside the active slot. + if (currentLength == 0) + { + playIndex_ = 0; + playPositionQ_ = 0; + } + else if (playIndex_ >= currentLength) + { + playIndex_ = playDirReverse_ ? (currentLength - 1) : 0; + playPositionQ_ = playIndex_ << kPlaybackFracBits; + } + else if (playIndex_ < 0) + { + playIndex_ = playDirReverse_ ? (currentLength - 1) : 0; + playPositionQ_ = playIndex_ << kPlaybackFracBits; + } + + if (recordingActive_ && slot == recordingSlot_) + { + // Avoid reading a slot while recording into it. + outputSlotSilenceOrLive(slot, inA, inB); + } + else if (mode == kModePassthrough) + { + // Passthrough mode ignores this slot's recording. + setAudioOutWithMonitor(inA, inB, true, true); + } + else if (stepFinished_) + { + // One Shot goes quiet after it finishes. Interrupt + // returns to live audio after it finishes. + if (mode == kModeInterrupt) + { + setAudioOutWithMonitor(inA, inB, true, true); + } + else // kModeOneShot + { + outputSlotSilenceOrLive(slot, inA, inB); + } + } + else if (hasPlaybackRecording(slot)) + { + // Division repeats only apply above x1. At x1, the + // fragment simply loops until the next sequencer step. + bool subdivisionActive = (subdivisionShift_ > 0) + && (repeatLengthSamples_ > 0); + + int32_t effectiveRepeatLength = subdivisionActive + ? repeatLengthSamples_ + : currentLength; + + if (effectiveRepeatLength < 1) + { + effectiveRepeatLength = 1; + } + + // One pass of the fragment may end naturally, or at a + // division boundary if the repeat is shorter than the recording. + bool fragmentCompleted = false; + + if (inSilenceFill_) + { + // The recording ended before this repeat slot did. + outputSlotSilenceOrLive(slot, inA, inB); + } + else + { + outputSlotSampleOrLive( + slot, + inA, + inB, + playbackSampleA(slot), + playbackSampleB(slot)); + + int32_t playbackStepThisSample = playbackStepForCurrentVariation(); + + if (playDirReverse_) + { + playPositionQ_ -= playbackStepThisSample; + if (playPositionQ_ < 0) + { + if (subdivisionActive + && fragmentShorterThanRepeat( + currentLength, + effectiveRepeatLength, + playbackStepThisSample)) + { + inSilenceFill_ = true; + } + else + { + playIndex_ = currentLength - 1; + playPositionQ_ = playIndex_ << kPlaybackFracBits; + fragmentCompleted = true; + } + } + else + { + playIndex_ = playPositionQ_ >> kPlaybackFracBits; + } + } + else + { + playPositionQ_ += playbackStepThisSample; + if (playPositionQ_ >= (currentLength << kPlaybackFracBits)) + { + if (subdivisionActive + && fragmentShorterThanRepeat( + currentLength, + effectiveRepeatLength, + playbackStepThisSample)) + { + inSilenceFill_ = true; + } + else + { + playIndex_ = 0; + playPositionQ_ = 0; + fragmentCompleted = true; + } + } + else + { + playIndex_ = playPositionQ_ >> kPlaybackFracBits; + } + } + } + + // Division boundary: restart or reverse-restart the fragment. + if (subdivisionActive) + { + repeatPosition_++; + if (repeatPosition_ >= effectiveRepeatLength) + { + repeatPosition_ = 0; + inSilenceFill_ = false; + playIndex_ = playDirReverse_ + ? (currentLength > 0 ? currentLength - 1 : 0) + : 0; + playPositionQ_ = playIndex_ << kPlaybackFracBits; + fragmentCompleted = true; + } + } + + // One Shot and Interrupt finish after their planned repeats. + if (fragmentCompleted + && mode != kModeLooping) + { + repeatsCompleted_++; + + int32_t targetRepeats = subdivisionActive + ? (1 << subdivisionShift_) + : 1; + + if (repeatsCompleted_ >= targetRepeats) + { + stepFinished_ = true; + } + } + } + else + { + // Empty slots pass live audio. + setAudioOutWithMonitor(inA, inB, true, true); + } + } + + wasRecording_ = recordHeld; + lastSwitchDown_ = switchDown; + } + +private: + // Move to the current pattern step, including slot shift. + void applyStepChange() + { + uint8_t rawValue = patterns_[selectedPattern_].steps[stepIndex_]; + int shiftedValue = (rawValue + shiftAmount_) % kNumSlots; + + if (variationMode_) + { + activeVariation_ = shiftedValue; + activeSlot_ = 0; + bool flipReverse = + (int32_t)(nextRandom() & 0x0FFF) + < activeVariationReverseProbability(); + playReverse_ = variationIsReverse(shiftedValue) != flipReverse; + } + else + { + activeSlot_ = shiftedValue; + rollReverse(); + } + + rollRandomCVOutputs(); + restartPlayback_ = true; + } + + void updateBootModeLatch(bool switchDown) + { + if (bootModeWindowSamples_ <= 0) + { + return; + } + + bootModeWindowSamples_--; + + if (switchDown) + { + if (bootModeDownSamples_ < kBootModeLatchSamples) + { + bootModeDownSamples_++; + } + + if (bootModeDownSamples_ >= kBootModeLatchSamples) + { + variationMode_ = true; + variationModeFeedbackSamples_ = kVariationModeFeedbackSamples; + zIgnoreRelease_ = true; + bootModeWindowSamples_ = 0; + } + } + else + { + bootModeDownSamples_ = 0; + } + } + + static bool variationIsReverse(int variation) + { + return variation == kVariationReverse + || variation == kVariationReverseOctaveUp; + } + + static int32_t snapVariationPitchSemitoneOffset(int32_t semitoneOffsetQ) + { + static const int8_t snapPoints[] = { + -24, -19, -17, -12, -7, -5, 0, 5, 7, 12, 17, 19, 24 + }; + + for (uint32_t i = 0; i < sizeof(snapPoints) / sizeof(snapPoints[0]); ++i) + { + int32_t snapQ = ((int32_t)snapPoints[i]) << kPlaybackFracBits; + int32_t distance = semitoneOffsetQ - snapQ; + if (distance < 0) distance = -distance; + + if (distance <= kVariationPitchSnapWindowQ) + { + return snapQ; + } + } + + return semitoneOffsetQ; + } + + void updateVariationPitchControl(int32_t knobMain) + { + int32_t centered = knobMain - 2048; + int32_t magnitude = centered < 0 ? -centered : centered; + int32_t semitoneOffsetQ = 0; + + if (magnitude > kVariationPitchDeadband) + { + int32_t travel = magnitude - kVariationPitchDeadband; + int32_t range = 2047 - kVariationPitchDeadband; + semitoneOffsetQ = + (travel * (kVariationPitchRangeSemitones << kPlaybackFracBits)) + / range; + + if (centered < 0) + { + semitoneOffsetQ = -semitoneOffsetQ; + } + } + + semitoneOffsetQ = snapVariationPitchSemitoneOffset(semitoneOffsetQ); + variationPitchStepQ_ = semitoneOffsetToPlaybackRatio(semitoneOffsetQ); + + int led = (int)((knobMain * kNumSlots) >> 12); + if (led < 0) led = 0; + if (led >= kNumSlots) led = kNumSlots - 1; + selectedSlot_ = led; + } + + bool __not_in_flash_func(debouncedCVConnected)(int input, bool rawConnected) + { + if (rawConnected == cvConnectedStable_[input]) + { + cvConnectedDebounceSamples_[input] = 0; + return cvConnectedStable_[input]; + } + + if (cvConnectedDebounceSamples_[input] < kCVJackDebounceSamples) + { + cvConnectedDebounceSamples_[input]++; + } + + if (cvConnectedDebounceSamples_[input] >= kCVJackDebounceSamples) + { + cvConnectedStable_[input] = rawConnected; + cvConnectedDebounceSamples_[input] = 0; + } + + return cvConnectedStable_[input]; + } + + void __not_in_flash_func(updateSwitchUpSlotSelection)(int32_t knobMain) + { + if (!switchUpMainPrimed_) + { + lastSwitchUpMain_ = knobMain; + switchUpMainPrimed_ = true; + return; + } + + int32_t dm = knobMain - lastSwitchUpMain_; + if (dm < 0) dm = -dm; + if (dm < kKnobEditMoveThreshold) + { + return; + } + + int sel = (int)((knobMain * kNumSlots) >> 12); + if (sel < 0) sel = 0; + if (sel >= kNumSlots) sel = kNumSlots - 1; + + selectedSlot_ = sel; + lastSwitchUpMain_ = knobMain; + + if (sel != lastEditSlot_) + { + switchUpKnobsPrimed_ = false; + } + } + + void updateVariationPitchEditing(int32_t knobMain) + { + if (!switchUpMainPrimed_) + { + lastSwitchUpMain_ = knobMain; + switchUpMainPrimed_ = true; + return; + } + + int32_t dm = knobMain - lastSwitchUpMain_; + if (dm < 0) dm = -dm; + if (dm < kKnobEditMoveThreshold) + { + return; + } + + updateVariationPitchControl(knobMain); + lastSwitchUpMain_ = knobMain; + } + + int32_t playbackStepForVariation(int variation) + { + int32_t step = (int32_t)playbackStepQ_; + + if (variationMode_) + { + switch (variation) + { + case kVariationOctaveUp: + case kVariationReverseOctaveUp: + step *= 2; + break; + case kVariationOctaveFifth: + step *= 3; + break; + case kVariationOctaveDown: + step /= 2; + break; + default: + break; + } + + step = (step * (int32_t)variationPitchStepQ_) >> kPlaybackFracBits; + } + + return step > 0 ? step : 1; + } + + int32_t playbackStepForCurrentVariation() + { + return playbackStepForVariation(activeVariation_); + } + + bool __not_in_flash_func(fragmentShorterThanRepeat)( + int32_t storedLength, + int32_t repeatAudioSamples, + int32_t playbackStep) + { + if (storedLength <= 0) + { + return true; + } + if (playbackStep < 1) + { + playbackStep = 1; + } + + return (((int64_t)storedLength) << kPlaybackFracBits) + < ((int64_t)repeatAudioSamples * playbackStep); + } + + // Patched CV starts unipolar. If it dips below 0V, treat it as bipolar. + static int32_t __not_in_flash_func(attenuatedAutoCVControl)( + int32_t cv, + int32_t amount, + bool &seenNegative, + bool connected) + { + if (!connected) + { + seenNegative = false; + return amount; + } + + if (cv < kCVBipolarDetectThreshold) + { + seenNegative = true; + } + + return seenNegative + ? attenuatedBipolarCVControl(cv, amount) + : attenuatedUnipolarCVControl(cv, amount); + } + + static int32_t __not_in_flash_func(attenuatedBipolarCVControl)(int32_t cv, int32_t amount) + { + if (cv < -2048) cv = -2048; + if (cv > 2047) cv = 2047; + if (amount < 0) amount = 0; + if (amount > 4095) amount = 4095; + + int32_t control = 2048 + ((cv * amount) >> 12); + if (control < 0) control = 0; + if (control > 4095) control = 4095; + return control; + } + + static int32_t __not_in_flash_func(attenuatedUnipolarCVControl)(int32_t cv, int32_t amount) + { + if (cv < 0) cv = 0; + if (cv > 2047) cv = 2047; + if (amount < 0) amount = 0; + if (amount > 4095) amount = 4095; + + int32_t control = (cv * amount) >> 11; + if (control < 0) control = 0; + if (control > 4095) control = 4095; + return control; + } + + void __not_in_flash_func(updateAlwaysOnCVControls)( + int32_t knobX, + int32_t knobY, + int32_t cv1in, + int32_t cv2in, + bool cv1Connected, + bool cv2Connected) + { + if (cv1Connected) + { + int32_t shiftControl = attenuatedAutoCVControl( + cv1in, knobX, cv1SeenNegative_, true); + setShiftFromControl(shiftControl); + } + else + { + cv1SeenNegative_ = false; + } + + if (cv2Connected) + { + cvReverseProb_ = attenuatedAutoCVControl( + cv2in, knobY, cv2SeenNegative_, true); + cvReverseOverrideActive_ = true; + } + else + { + cv2SeenNegative_ = false; + cvReverseOverrideActive_ = false; + } + } + + void __not_in_flash_func(updateSwitchUpKnobEditing)( + int slot, + int32_t knobX, + int32_t knobY, + bool cv1Connected, + bool cv2Connected) + { + if (!switchUpKnobsPrimed_) + { + lastEditSlot_ = slot; + lastKnobX_ = knobX; + lastKnobY_ = knobY; + switchUpKnobsPrimed_ = true; + return; + } + + int32_t dx = knobX - lastKnobX_; + if (dx < 0) dx = -dx; + if (cv1Connected) + { + lastKnobX_ = knobX; + } + else if (dx >= kKnobEditMoveThreshold) + { + int mode = (int)(knobX >> 10); // 0-3 + if (mode < 0) mode = 0; + if (mode > 3) mode = 3; + playbackMode_[slot] = mode; + lastKnobX_ = knobX; + } + + int32_t dy = knobY - lastKnobY_; + if (dy < 0) dy = -dy; + if (cv2Connected) + { + lastKnobY_ = knobY; + } + else if (dy >= kKnobEditMoveThreshold) + { + reverseProb_[slot] = knobY; + lastKnobY_ = knobY; + } + } + + void __not_in_flash_func(updateVariationSwitchUpEditing)( + int32_t knobX, + int32_t knobY, + bool cv1Connected, + bool cv2Connected) + { + if (!switchUpKnobsPrimed_) + { + lastEditSlot_ = -1; + lastKnobX_ = knobX; + lastKnobY_ = knobY; + switchUpKnobsPrimed_ = true; + return; + } + + int32_t dx = knobX - lastKnobX_; + if (dx < 0) dx = -dx; + if (cv1Connected) + { + lastKnobX_ = knobX; + } + else if (dx >= kKnobEditMoveThreshold) + { + int mode = (int)(knobX >> 10); // 0-3 + if (mode < 0) mode = 0; + if (mode > 3) mode = 3; + variationPlaybackMode_ = mode; + lastKnobX_ = knobX; + } + + int32_t dy = knobY - lastKnobY_; + if (dy < 0) dy = -dy; + if (cv2Connected) + { + lastKnobY_ = knobY; + } + else if (dy >= kKnobEditMoveThreshold) + { + variationReverseProb_ = knobY; + lastKnobY_ = knobY; + } + } + + void __not_in_flash_func(updateMiddlePatternEditing)(int32_t knobMain) + { + if (!middleMainPrimed_) + { + lastMiddleMain_ = knobMain; + middleMainPrimed_ = true; + return; + } + + int32_t dm = knobMain - lastMiddleMain_; + if (dm < 0) dm = -dm; + if (dm < kKnobEditMoveThreshold) + { + return; + } + + int pat = (int)((knobMain * kNumPatterns) >> 12); + if (pat < 0) pat = 0; + if (pat >= kNumPatterns) pat = kNumPatterns - 1; + selectedPattern_ = pat; + lastMiddleMain_ = knobMain; + } + + void __not_in_flash_func(updateMiddleShiftDivideEditing)( + int32_t knobX, + int32_t knobY, + bool cv1Connected, + bool cv2Connected) + { + if (!middleXYPrimed_) + { + lastMiddleKnobX_ = knobX; + lastMiddleKnobY_ = knobY; + middleXYPrimed_ = true; + return; + } + + if (cv1Connected) + { + lastMiddleKnobX_ = knobX; + } + else + { + // No CV patched: X is direct shift control, but only after + // it moves in middle mode. This prevents a mode jump when + // returning from switch-up slot selection. + int32_t dx = knobX - lastMiddleKnobX_; + if (dx < 0) dx = -dx; + if (dx >= kKnobEditMoveThreshold) + { + setShiftFromControl(knobX); + lastMiddleKnobX_ = knobX; + } + } + + if (cv2Connected) + { + lastMiddleKnobY_ = knobY; + } + else + { + // No CV patched: Y is direct divide control, but only + // after movement. This avoids accidental repeat/divide + // jumps when moving between switch positions. + int32_t dy = knobY - lastMiddleKnobY_; + if (dy < 0) dy = -dy; + if (dy >= kKnobEditMoveThreshold) + { + setSubdivisionFromControl(knobY); + lastMiddleKnobY_ = knobY; + } + } + } + + void __not_in_flash_func(setShiftFromControl)(int32_t control) + { + int shift = (int)((control * kNumSlots) >> 12); + if (shift < 0) shift = 0; + if (shift >= kNumSlots) shift = kNumSlots - 1; + shiftAmount_ = shift; + } + + void __not_in_flash_func(setSubdivisionFromControl)(int32_t control) + { + int subShift = (int)(control >> 10); + if (subShift < 0) subShift = 0; + if (subShift > 3) subShift = 3; + subdivisionShift_ = subShift; + } + + // Core 1 requests trigger pulses; core 0 owns the actual hardware + // writes so the output duration is sample-accurate. + void triggerStepPulses(bool firstStep) + { + pulseOut1Samples_ = kPulseTriggerSamples; + if (firstStep) + { + pulseOut2Samples_ = kPulseTriggerSamples; + } + } + + void __not_in_flash_func(updatePulseOutputs)() + { + if (pulseOut1Samples_ > 0) + { + PulseOut1(true); + pulseOut1Samples_--; + } + else + { + PulseOut1(false); + } + + if (pulseOut2Samples_ > 0) + { + PulseOut2(true); + pulseOut2Samples_--; + } + else + { + PulseOut2(false); + } + } + + // Random CV targets refresh on step divisions. + void rollRandomCVOutputs() + { + for (int output = 0; output < 2; output++) + { + if (output == 1 && cv2CoupledToCV1_) + { + continue; + } + + if (cvRandomStepCount_[output] == 0) + { + randomCVTarget_[output] = makeRandomCVValue(output); + if (cvSlewShift(output) == 0) + { + randomCVCurrent_[output] = randomCVTarget_[output]; + randomCVCurrentQ_[output] = randomCVTarget_[output] << kCVSlewFracBits; + } + cvRandomStepCount_[output] = cvClockDivisor(output) - 1; + } + else + { + cvRandomStepCount_[output]--; + } + } + } + + int32_t __not_in_flash_func(randomSigned12Bit)() + { + return (int32_t)(nextRandom() & 0x0FFF) - 2048; + } + + int32_t __not_in_flash_func(makeRandomCVValue)(int output) + { + int32_t value = applyCVRange(randomSigned12Bit(), cvRange_[output]); + value = quantizeCV(value, cvQuant_[output]); + return clampSigned12Bit(value); + } + + int32_t applyCVRange(int32_t value, uint8_t range) + { + switch (range) + { + case kCVRangeBipolar3V: + return value >> 1; + + case kCVRangeBipolar15V: + return value >> 2; + + case kCVRangeUnipolar6V: + return (value + 2048) >> 1; + + case kCVRangeUnipolar3V: + return (value + 2048) >> 2; + + case kCVRangeUnipolar15V: + return (value + 2048) >> 3; + + case kCVRangeBipolar6V: + default: + return value; + } + } + + int32_t quantizeCV(int32_t value, uint8_t quant) + { + if (quant == kCVQuantOff) + { + return value; + } + + int semitone = divRound(value * 72, 2048); + semitone = nearestQuantizedSemitone(semitone, quant); + + return divRound(semitone * 2048, 72); + } + + int nearestQuantizedSemitone(int semitone, uint8_t quant) + { + static const int majorScale[7] = {0, 2, 4, 5, 7, 9, 11}; + static const int minorScale[7] = {0, 2, 3, 5, 7, 8, 10}; + static const int majorPentatonicScale[5] = {0, 2, 4, 7, 9}; + static const int minorPentatonicScale[5] = {0, 3, 5, 7, 10}; + static const int dorianScale[7] = {0, 2, 3, 5, 7, 9, 10}; + static const int pelogScale[5] = {0, 1, 3, 7, 8}; + static const int wholeToneScale[6] = {0, 2, 4, 6, 8, 10}; + static const int octaveScale[1] = {0}; + static const int fifthsOctavesScale[2] = {0, 7}; + static const int fourthsFifthsOctavesScale[3] = {0, 5, 7}; + const int *scale = majorScale; + int length = 7; + + if (quant == kCVQuantMinor) + { + scale = minorScale; + } + else if (quant == kCVQuantMajorPentatonic) + { + scale = majorPentatonicScale; + length = 5; + } + else if (quant == kCVQuantMinorPentatonic) + { + scale = minorPentatonicScale; + length = 5; + } + else if (quant == kCVQuantDorian) + { + scale = dorianScale; + } + else if (quant == kCVQuantPelog) + { + scale = pelogScale; + length = 5; + } + else if (quant == kCVQuantWholeTone) + { + scale = wholeToneScale; + length = 6; + } + else if (quant == kCVQuantOctaves) + { + scale = octaveScale; + length = 1; + } + else if (quant == kCVQuantFifthsOctaves) + { + scale = fifthsOctavesScale; + length = 2; + } + else if (quant == kCVQuantFourthsFifthsOctaves) + { + scale = fourthsFifthsOctavesScale; + length = 3; + } + + int best = semitone; + int bestDistance = 128; + + for (int octave = -7; octave <= 7; octave++) + { + for (int i = 0; i < length; i++) + { + int candidate = octave * 12 + scale[i]; + int distance = candidate - semitone; + if (distance < 0) distance = -distance; + if (distance < bestDistance) + { + bestDistance = distance; + best = candidate; + } + } + } + + return best; + } + + int32_t __not_in_flash_func(divRound)(int32_t numerator, int32_t denominator) + { + if (numerator >= 0) + { + return (numerator + denominator / 2) / denominator; + } + return -((-numerator + denominator / 2) / denominator); + } + + int32_t __not_in_flash_func(clampSigned12Bit)(int32_t value) + { + if (value < -2048) return -2048; + if (value > 2047) return 2047; + return value; + } + + int32_t __not_in_flash_func(randomCVToMillivolts)(int32_t value) + { + value = clampSigned12Bit(value); + int32_t millivolts = divRound(value * kCVMillivoltFullScale, kCVRawFullScale); + + if (millivolts < -kCVMillivoltFullScale) + { + return -kCVMillivoltFullScale; + } + if (millivolts > kCVMillivoltFullScale) + { + return kCVMillivoltFullScale; + } + return millivolts; + } + + void __not_in_flash_func(writeRandomCVOutput)(int output, int32_t value) + { + value = clampSigned12Bit(value); + if (lastRandomCVOutput_[output] == value) + { + return; + } + + lastRandomCVOutput_[output] = value; + int32_t millivolts = randomCVToMillivolts(value); + + if (output == 0) + { + CVOut1Millivolts(millivolts); + } + else + { + CVOut2Millivolts(millivolts); + } + } + + uint8_t __not_in_flash_func(cvClockDivisor)(int output) + { + return 1u << cvClockDiv_[output]; + } + + uint8_t __not_in_flash_func(cvSlewShift)(int output) + { + switch (cvSlew_[output]) + { + case 1: return 9; + case 2: return 10; + case 3: return 11; + case 4: return 12; + case 5: return 14; + default: return 0; + } + } + + void __not_in_flash_func(updateRandomCVOutputs)() + { + for (int output = 0; output < 2; output++) + { + if (output == 1 && cv2CoupledToCV1_) + { + continue; + } + + int32_t target = randomCVTarget_[output]; + uint8_t slewShift = cvSlewShift(output); + + if (slewShift == 0) + { + randomCVCurrent_[output] = target; + randomCVCurrentQ_[output] = target << kCVSlewFracBits; + } + else + { + int32_t targetQ = target << kCVSlewFracBits; + int32_t delta = targetQ - randomCVCurrentQ_[output]; + int32_t step = delta >> slewShift; + + if (step == 0 && delta != 0) + { + step = delta > 0 ? 1 : -1; + } + + randomCVCurrentQ_[output] += step; + + if ((delta > 0 && randomCVCurrentQ_[output] >= targetQ) + || (delta < 0 && randomCVCurrentQ_[output] <= targetQ)) + { + randomCVCurrentQ_[output] = targetQ; + randomCVCurrent_[output] = target; + } + else + { + randomCVCurrent_[output] = + randomCVCurrentQ_[output] >> kCVSlewFracBits; + } + } + } + + if (cv2CoupledToCV1_) + { + randomCVTarget_[1] = randomCVTarget_[0]; + randomCVCurrent_[1] = randomCVCurrent_[0]; + randomCVCurrentQ_[1] = randomCVCurrentQ_[0]; + cvRandomStepCount_[1] = cvRandomStepCount_[0]; + } + + writeRandomCVOutput(0, randomCVCurrent_[0]); + writeRandomCVOutput(1, randomCVCurrent_[1]); + } + + void serviceUSBMIDI() + { + uint8_t buffer[64]; + + while (tud_midi_available()) + { + uint32_t count = tud_midi_stream_read(buffer, sizeof(buffer)); + + for (uint32_t i = 0; i < count; i++) + { + handleMIDIByte(buffer[i]); + } + } + } + + void handleMIDIByte(uint8_t byte) + { + if (byte == 0xF0) + { + sysexActive_ = true; + sysexLength_ = 0; + return; + } + + if (sysexActive_) + { + if (byte == 0xF7) + { + processSysEx(); + sysexActive_ = false; + sysexLength_ = 0; + } + else if (byte < 0x80 && sysexLength_ < kSysExBufferSize) + { + sysexBuffer_[sysexLength_] = byte; + sysexLength_++; + } + return; + } + + if (byte & 0x80) + { + midiStatus_ = byte; + midiDataCount_ = 0; + return; + } + + uint8_t command = midiStatus_ & 0xF0; + if (command != 0x80 && command != 0x90 && command != 0xB0 && command != 0xE0) + { + return; + } + + midiData_[midiDataCount_] = byte; + midiDataCount_++; + + if (midiDataCount_ < 2) + { + return; + } + + if (command == 0x90 && midiData_[1] > 0) + { + midiNote_ = midiData_[0]; + updateMIDIPlaybackStep(); + } + else if (command == 0xB0) + { + handleMIDIControlChange(midiData_[0], midiData_[1]); + } + else if (command == 0xE0) + { + int32_t bend = ((int32_t)midiData_[0] | ((int32_t)midiData_[1] << 7)) - 8192; + pitchBendSemitoneQ_ = bend * kPitchBendScale; + updateMIDIPlaybackStep(); + } + + midiDataCount_ = 0; + } + + void updateMIDIPlaybackStep() + { + int32_t semitoneOffsetQ = + (((int32_t)midiNote_ - 60) << kPlaybackFracBits) + + pitchBendSemitoneQ_; + + playbackStepQ_ = + playbackRatioToStep(semitoneOffsetToPlaybackRatio(semitoneOffsetQ)); + } + + uint32_t playbackRatioToStep(uint32_t ratioQ) + { + uint32_t step = (ratioQ * kPlaybackStepNormal) >> kPlaybackFracBits; + return step > 0 ? step : 1; + } + + uint32_t semitoneOffsetToPlaybackRatio(int32_t semitoneOffsetQ) + { + int octave = 0; + const int32_t octaveQ = 12 << kPlaybackFracBits; + + while (semitoneOffsetQ < 0) + { + semitoneOffsetQ += octaveQ; + octave--; + } + + while (semitoneOffsetQ >= octaveQ) + { + semitoneOffsetQ -= octaveQ; + octave++; + } + + int semitone = semitoneOffsetQ >> kPlaybackFracBits; + int32_t frac = semitoneOffsetQ & ((1 << kPlaybackFracBits) - 1); + uint32_t step = kSemitoneSteps[semitone]; + + uint32_t nextStep = (semitone == 11) + ? (kSemitoneSteps[0] << 1) + : kSemitoneSteps[semitone + 1]; + step += ((nextStep - step) * (uint32_t)frac) >> kPlaybackFracBits; + + if (octave > 0) + { + step <<= octave; + } + else if (octave < 0) + { + int shift = -octave; + step >>= shift; + if (step < 1) step = 1; + } + + return step; + } + + void processSysEx() + { + if (sysexLength_ < 2 || sysexBuffer_[0] != kSysExManufacturer) + { + return; + } + + uint8_t command = sysexBuffer_[1]; + uint8_t *payload = &sysexBuffer_[2]; + uint32_t size = sysexLength_ - 2; + + switch (command) + { + case kSysExImportBegin: + handleImportBegin(payload, size); + break; + + case kSysExImportData: + handleImportData(payload, size); + break; + + case kSysExImportEnd: + handleImportEnd(); + break; + + case kSysExSaveKit: + flashSaveRequested_ = true; + break; + + case kSysExLoadKit: + loadKitFromFlash(); + break; + + case kSysExPing: + sendSysExAck(kAckPing, 0, 0); + break; + + case kSysExPatternData: + handlePatternData(payload, size); + break; + + case kSysExFactoryPatterns: + loadFactoryPatterns(); + sendSysExAck(kAckPatternLoaded, 127, kNumPatterns); + break; + + case kSysExCVConfig: + handleCVConfig(payload, size); + break; + + case kSysExMonitorConfig: + handleMonitorConfig(payload, size); + break; + + default: + break; + } + } + + void handleMIDIControlChange(uint8_t cc, uint8_t value) + { + int knob = -1; + if (cc == 16) knob = 0; // Main + else if (cc == 17) knob = 1; // X + else if (cc == 18) knob = 2; // Y + + if (knob < 0) + { + return; + } + + midiKnobValue_[knob] = ((int32_t)value * 4095) / 127; + midiKnobActive_[knob] = true; + } + + int32_t resolveKnobControl(int knob, int32_t physical) + { + if (midiKnobActive_[knob]) + { + int32_t delta = physical - lastPhysicalKnob_[knob]; + if (delta < 0) delta = -delta; + if (delta >= kKnobEditMoveThreshold) + { + midiKnobActive_[knob] = false; + lastPhysicalKnob_[knob] = physical; + return physical; + } + + return midiKnobValue_[knob]; + } + + lastPhysicalKnob_[knob] = physical; + return physical; + } + + void loadFactoryPatterns() + { + for (int i = 0; i < kNumPatterns; i++) + { + patterns_[i] = factoryPatterns[i]; + } + } + + void handlePatternData(uint8_t *data, uint32_t size) + { + if (size < 2) + { + sendSysExAck(kAckPatternReject, 0, 0); + return; + } + + int pattern = data[0]; + int length = data[1]; + + if (pattern < 0 || pattern >= kNumPatterns || length < 1 + || length > kMaxPatternLen || size < (uint32_t)(2 + length)) + { + sendSysExAck(kAckPatternReject, (uint8_t)pattern, (uint16_t)length); + return; + } + + for (int i = 0; i < length; i++) + { + if (data[2 + i] >= kNumSlots) + { + sendSysExAck(kAckPatternReject, (uint8_t)pattern, (uint16_t)i); + return; + } + } + + patterns_[pattern].length = (uint8_t)length; + for (int i = 0; i < kMaxPatternLen; i++) + { + patterns_[pattern].steps[i] = i < length ? data[2 + i] : 0; + } + + if (selectedPattern_ == pattern) + { + if (stepIndex_ >= length) + { + stepIndex_ = 0; + } + applyStepChange(); + } + + sendSysExAck(kAckPatternLoaded, (uint8_t)pattern, (uint16_t)length); + } + + void handleCVConfig(uint8_t *data, uint32_t size) + { + if (size < 5) + { + sendSysExAck(kAckCVConfigReject, 0, 0); + return; + } + + int output = data[0]; + uint8_t range = data[1]; + uint8_t quant = data[2]; + uint8_t div = data[3]; + uint8_t slew = data[4]; + bool coupled = output == 1 && size >= 6 && data[5] != 0; + + if (output < 0 || output >= 2 + || range >= kCVRangeCount + || quant >= kCVQuantCount + || div >= kCVClockDivCount + || slew >= kCVSlewCount) + { + sendSysExAck(kAckCVConfigReject, (uint8_t)output, 0); + return; + } + + cvRange_[output] = range; + cvQuant_[output] = quant; + cvClockDiv_[output] = div; + cvSlew_[output] = slew; + if (output == 1) + { + cv2CoupledToCV1_ = coupled; + } + cvRandomStepCount_[output] = 0; + randomCVTarget_[output] = makeRandomCVValue(output); + + sendSysExAck(kAckCVConfigLoaded, (uint8_t)output, + (uint16_t)(range | (quant << 3) | (div << 7) | (slew << 10))); + } + + void handleMonitorConfig(uint8_t *data, uint32_t size) + { + if (size < 1 || data[0] >= kMonitorModeCount) + { + sendSysExAck(kAckMonitorConfigReject, 0, 0); + return; + } + + monitorMode_ = data[0]; + sendSysExAck(kAckMonitorConfigLoaded, monitorMode_, 0); + } + + void handleImportBegin(uint8_t *data, uint32_t size) + { + if (size < 4) + { + return; + } + + int slot = data[0]; + uint8_t mask = data[1] & 0x03; + int32_t length = (int32_t)data[2] | ((int32_t)data[3] << 7); + + if (slot < 0 || slot >= kNumSlots || mask == 0 || length <= 0) + { + importActive_ = false; + sendSysExAck(kAckImportReject, 0, 0); + return; + } + + if (length > kBufferLength) + { + length = kBufferLength; + } + + importActive_ = true; + importSlot_ = slot; + importChannelMask_ = mask; + importExpectedLength_ = length; + importIndex_ = 0; + sendSysExAck(kAckImportBegin, (uint8_t)slot, (uint16_t)length); + } + + void handleImportData(uint8_t *data, uint32_t size) + { + if (!importActive_) + { + sendSysExAck(kAckImportReject, 0, 0); + return; + } + + int slot = importSlot_; + + for (uint32_t i = 0; i + 1 < size && importIndex_ < importExpectedLength_; i += 2) + { + uint8_t packed = (data[i] & 0x7F) | ((data[i + 1] & 0x01) << 7); + int8_t sample = (int8_t)packed; + + if (importChannelMask_ & 0x01) + { + bufferA_[slot][importIndex_] = sample; + } + if (importChannelMask_ & 0x02) + { + bufferB_[slot][importIndex_] = sample; + } + + importIndex_++; + } + } + + void handleImportEnd() + { + if (!importActive_) + { + return; + } + + int slot = importSlot_; + int32_t length = importIndex_; + + if (length >= kMinRecordingSamples) + { + if (importChannelMask_ & 0x01) + { + recordedLengthA_[slot] = length; + recordingChannelA_[slot] = true; + } + if (importChannelMask_ & 0x02) + { + recordedLengthB_[slot] = length; + recordingChannelB_[slot] = true; + } + + recordedLength_[slot] = + recordedLengthA_[slot] > recordedLengthB_[slot] + ? recordedLengthA_[slot] + : recordedLengthB_[slot]; + hasRecording_[slot] = recordingChannelA_[slot] || recordingChannelB_[slot]; + reverseProb_[slot] = 0; + recomputeVariationRecordingState(); + sendSysExAck(kAckImportEnd, (uint8_t)slot, (uint16_t)length); + } + else + { + sendSysExAck(kAckImportShort, (uint8_t)slot, (uint16_t)length); + } + + importActive_ = false; + } + + void sendSysExAck(uint8_t code, uint8_t slot, uint16_t value) + { + uint8_t message[] = { + 0xF0, + kSysExManufacturer, + kSysExAck, + code, + slot, + (uint8_t)(value & 0x7F), + (uint8_t)((value >> 7) & 0x7F), + 0xF7 + }; + tud_midi_stream_write(0, message, sizeof(message)); + } + + void closeZCommand() + { + zCommandArmed_ = false; + zCommandNeedsRelease_ = false; + zCommandTimeoutSamples_ = 0; + zDoubleTapSamples_ = 0; + zFirstTapDown_ = false; + } + + // Hold Z to arm, then single-tap Clear or double-tap Save. + ZCommand updateZCommand(bool switchDown) + { + bool pressed = switchDown && !lastSwitchDown_; + bool released = !switchDown && lastSwitchDown_; + + if (zCommandArmed_) + { + if (zCommandNeedsRelease_) + { + if (!switchDown) + { + zCommandNeedsRelease_ = false; + zCommandTimeoutSamples_ = kCommandTimeoutSamples; + } + return kZCommandNone; + } + + if (zFirstTapDown_) + { + if (released) + { + zFirstTapDown_ = false; + zDoubleTapSamples_ = kDoubleTapSamples; + } + return kZCommandNone; + } + + if (zDoubleTapSamples_ > 0) + { + if (pressed) + { + zIgnoreRelease_ = true; + closeZCommand(); + return kZCommandSave; + } + + zDoubleTapSamples_--; + if (zDoubleTapSamples_ == 0) + { + closeZCommand(); + return kZCommandClear; + } + return kZCommandNone; + } + + if (zCommandTimeoutSamples_ > 0) + { + if (pressed) + { + zFirstTapDown_ = true; + zCommandTimeoutSamples_ = 0; + return kZCommandNone; + } + + zCommandTimeoutSamples_--; + if (zCommandTimeoutSamples_ == 0) + { + closeZCommand(); + } + return kZCommandNone; + } + + closeZCommand(); + return kZCommandNone; + } + + if (zIgnoreRelease_) + { + if (!switchDown) + { + zIgnoreRelease_ = false; + } + zHoldSamples_ = 0; + return kZCommandNone; + } + + if (switchDown) + { + if (zHoldSamples_ < kClearHoldSamples) + { + zHoldSamples_++; + } + + if (zHoldSamples_ >= kClearHoldSamples) + { + zCommandArmed_ = true; + zCommandNeedsRelease_ = true; + zHoldSamples_ = 0; + } + return kZCommandNone; + } + + if (released) + { + zHoldSamples_ = 0; + return kZCommandReset; + } + + zHoldSamples_ = 0; + return kZCommandNone; + } + + void beginClearAll() + { + for (int i = 0; i < kNumSlots; i++) + { + hasRecording_[i] = false; + recordedLength_[i] = 0; + recordedLengthA_[i] = 0; + recordedLengthB_[i] = 0; + recordingChannelA_[i] = false; + recordingChannelB_[i] = false; + reverseProb_[i] = 0; + } + + writeIndex_ = 0; + playIndex_ = 0; + playPositionQ_ = 0; + variationRecordedLength_ = 0; + variationRecordedLengthA_ = 0; + variationRecordedLengthB_ = 0; + variationHasRecording_ = false; + variationChannelA_ = false; + variationChannelB_ = false; + repeatPosition_ = 0; + repeatsCompleted_ = 0; + stepFinished_ = false; + inSilenceFill_ = false; + bufferFull_ = false; + recordingActive_ = false; + + __mem_fence_release(); + pendingRecordFinalize_ = false; + pendingRecordVariation_ = false; + clearRequested_ = true; + } + + void serviceClearRequest() + { + if (clearRequested_ && !clearInProgress_) + { + clearRequested_ = false; + clearInProgress_ = true; + clearSlot_ = 0; + clearIndex_ = 0; + } + + if (!clearInProgress_) + { + return; + } + + for (int i = 0; i < kClearSamplesPerCore1Tick; i++) + { + bufferA_[clearSlot_][clearIndex_] = 0; + bufferB_[clearSlot_][clearIndex_] = 0; + + clearSlot_++; + if (clearSlot_ >= kNumSlots) + { + clearSlot_ = 0; + clearIndex_++; + + if (clearIndex_ >= kBufferLength) + { + clearInProgress_ = false; + return; + } + } + } + } + + void serviceRecordingFinalize() + { + if (!pendingRecordFinalize_) + { + return; + } + + __mem_fence_acquire(); + pendingRecordFinalize_ = false; + int slot = pendingRecordSlot_; + int32_t length = pendingRecordLength_; + + if (pendingRecordVariation_) + { + finalizeVariationRecording( + length, + pendingRecordChannelA_, + pendingRecordChannelB_); + __mem_fence_release(); + return; + } + + if (pendingRecordChannelA_) + { + recordedLengthA_[slot] = length; + recordingChannelA_[slot] = true; + } + if (pendingRecordChannelB_) + { + recordedLengthB_[slot] = length; + recordingChannelB_[slot] = true; + } + + recordedLength_[slot] = + recordedLengthA_[slot] > recordedLengthB_[slot] + ? recordedLengthA_[slot] + : recordedLengthB_[slot]; + hasRecording_[slot] = recordingChannelA_[slot] || recordingChannelB_[slot]; + reverseProb_[slot] = 0; + recomputeVariationRecordingState(); + __mem_fence_release(); + } + + void finalizeVariationRecording(int32_t length, bool channelA, bool channelB) + { + if (length < 0) length = 0; + if (length > kVariationBufferLength) length = kVariationBufferLength; + + if (channelA) + { + setVariationSegmentLengths(recordedLengthA_, recordingChannelA_, length); + } + else + { + setVariationSegmentLengths(recordedLengthA_, recordingChannelA_, 0); + } + + if (channelB) + { + setVariationSegmentLengths(recordedLengthB_, recordingChannelB_, length); + } + else + { + setVariationSegmentLengths(recordedLengthB_, recordingChannelB_, 0); + } + + for (int slot = 0; slot < kNumSlots; slot++) + { + recordedLength_[slot] = + recordedLengthA_[slot] > recordedLengthB_[slot] + ? recordedLengthA_[slot] + : recordedLengthB_[slot]; + hasRecording_[slot] = recordingChannelA_[slot] || recordingChannelB_[slot]; + reverseProb_[slot] = 0; + } + + recomputeVariationRecordingState(); + } + + void setVariationSegmentLengths( + int32_t *lengths, + bool *channels, + int32_t totalLength) + { + for (int slot = 0; slot < kNumSlots; slot++) + { + int32_t segmentLength = totalLength; + if (segmentLength > kBufferLength) + { + segmentLength = kBufferLength; + } + if (segmentLength < 0) + { + segmentLength = 0; + } + + lengths[slot] = segmentLength; + channels[slot] = segmentLength > 0; + totalLength -= segmentLength; + } + } + + void recomputeVariationRecordingState() + { + variationRecordedLengthA_ = combinedVariationLength(recordedLengthA_); + variationRecordedLengthB_ = combinedVariationLength(recordedLengthB_); + variationRecordedLength_ = + variationRecordedLengthA_ > variationRecordedLengthB_ + ? variationRecordedLengthA_ + : variationRecordedLengthB_; + variationChannelA_ = variationRecordedLengthA_ > 0; + variationChannelB_ = variationRecordedLengthB_ > 0; + variationHasRecording_ = variationChannelA_ || variationChannelB_; + } + + int32_t combinedVariationLength(int32_t *lengths) + { + int32_t total = 0; + for (int slot = 0; slot < kNumSlots; slot++) + { + int32_t length = lengths[slot]; + if (length <= 0) + { + break; + } + if (length > kBufferLength) + { + length = kBufferLength; + } + + total += length; + if (length < kBufferLength) + { + break; + } + } + return total; + } + + void __not_in_flash_func(outputSlotSampleOrLive)( + int slot, + int32_t inA, + int32_t inB, + int32_t sampleA, + int32_t sampleB) + { + bool channelA = playbackChannelA(slot); + bool channelB = playbackChannelB(slot); + int32_t lengthA = playbackLengthA(slot); + int32_t lengthB = playbackLengthB(slot); + + int32_t unrecordedA = variationMode_ ? 0 : inA; + int32_t unrecordedB = variationMode_ ? 0 : inB; + int32_t outA = channelA + ? (playIndex_ < lengthA ? gainRecordedSample(sampleA) : 0) + : unrecordedA; + int32_t outB = channelB + ? (playIndex_ < lengthB ? gainRecordedSample(sampleB) : 0) + : unrecordedB; + + if (variationMode_) + { + outA = smoothVariationEdge(slot, outA, channelA); + outB = smoothVariationEdge(slot, outB, channelB); + + if (variationFadeSamples_ > 0) + { + outA = crossfadeVariationRetrigger(outA, variationFadeStartA_); + outB = crossfadeVariationRetrigger(outB, variationFadeStartB_); + variationFadeSamples_--; + } + } + + bool includesLiveInputA = !variationMode_ && !channelA; + bool includesLiveInputB = !variationMode_ && !channelB; + setAudioOutWithMonitor(outA, outB, includesLiveInputA, includesLiveInputB); + } + + int8_t __not_in_flash_func(audioToStoredSample)(int32_t sample) + { + if (sample > -kRecordingSilenceThreshold + && sample < kRecordingSilenceThreshold) + { + return 0; + } + + int32_t stored = sample >> 4; + if (stored < -128) return -128; + if (stored > 127) return 127; + return (int8_t)stored; + } + + int32_t gainRecordedSample(int32_t sample) + { + int32_t gained = (sample * kSamplePlaybackGainNum) / kSamplePlaybackGainDen; + if (gained < -2048) return -2048; + if (gained > 2047) return 2047; + return gained; + } + + int playbackModeForSlot(int slot) + { + return variationMode_ ? variationPlaybackMode_ : playbackMode_[slot]; + } + + bool hasPlaybackRecording(int slot) + { + return variationMode_ ? variationHasRecording_ : hasRecording_[slot]; + } + + bool playbackChannelA(int slot) + { + return variationMode_ ? variationChannelA_ : recordingChannelA_[slot]; + } + + bool playbackChannelB(int slot) + { + return variationMode_ ? variationChannelB_ : recordingChannelB_[slot]; + } + + int32_t playbackLength(int slot) + { + return variationMode_ ? variationRecordedLength_ : recordedLength_[slot]; + } + + int32_t playbackLengthA(int slot) + { + return variationMode_ ? variationRecordedLengthA_ : recordedLengthA_[slot]; + } + + int32_t playbackLengthB(int slot) + { + return variationMode_ ? variationRecordedLengthB_ : recordedLengthB_[slot]; + } + + int32_t playbackSampleA(int slot) + { + return variationMode_ + ? interpolatedVariationSample(bufferA_, variationRecordedLengthA_) + : interpolatedBufferSample(bufferA_[slot], recordedLengthA_[slot]); + } + + int32_t playbackSampleB(int slot) + { + return variationMode_ + ? interpolatedVariationSample(bufferB_, variationRecordedLengthB_) + : interpolatedBufferSample(bufferB_[slot], recordedLengthB_[slot]); + } + + int32_t __not_in_flash_func(interpolatedBufferSample)(int8_t *buffer, int32_t length) + { + if (length <= 0) + { + return 0; + } + + int32_t index = playPositionQ_ >> kPlaybackFracBits; + if (index < 0) index = 0; + if (index >= length) index = length - 1; + + int32_t nextIndex = index + 1; + if (nextIndex >= length) + { + nextIndex = index; + } + + int32_t frac = playPositionQ_ & ((1 << kPlaybackFracBits) - 1); + int32_t sample = (int32_t)buffer[index] << 4; + int32_t nextSample = (int32_t)buffer[nextIndex] << 4; + return sample + (((nextSample - sample) * frac) >> kPlaybackFracBits); + } + + int32_t __not_in_flash_func(interpolatedVariationSample)( + int8_t buffer[kNumSlots][kBufferLength], + int32_t length) + { + if (length <= 0) + { + return 0; + } + + int32_t index = playPositionQ_ >> kPlaybackFracBits; + if (index < 0) index = 0; + if (index >= length) index = length - 1; + + int32_t nextIndex = index + 1; + if (nextIndex >= length) + { + nextIndex = index; + } + + int32_t frac = playPositionQ_ & ((1 << kPlaybackFracBits) - 1); + int32_t sample = (int32_t)readVariationSample(buffer, index) << 4; + int32_t nextSample = (int32_t)readVariationSample(buffer, nextIndex) << 4; + return sample + (((nextSample - sample) * frac) >> kPlaybackFracBits); + } + + int8_t readVariationSample( + int8_t buffer[kNumSlots][kBufferLength], + int32_t index) + { + if (index < 0) index = 0; + if (index >= kVariationBufferLength) index = kVariationBufferLength - 1; + int slot = index / kBufferLength; + int offset = index - (slot * kBufferLength); + return buffer[slot][offset]; + } + + void writeVariationSample( + int8_t buffer[kNumSlots][kBufferLength], + int32_t index, + int8_t sample) + { + if (index < 0 || index >= kVariationBufferLength) + { + return; + } + int slot = index / kBufferLength; + int offset = index - (slot * kBufferLength); + buffer[slot][offset] = sample; + } + + int32_t __not_in_flash_func(smoothVariationEdge)(int slot, int32_t sample, bool recordedChannel) + { + if (!recordedChannel) + { + return sample; + } + + int32_t length = playbackLength(slot); + int32_t fade = kVariationFadeSamples; + if (length <= 1) + { + return 0; + } + if (fade * 2 > length) + { + fade = length / 2; + } + if (fade <= 0) + { + return sample; + } + + int32_t fromStart = playIndex_; + int32_t fromEnd = length - 1 - playIndex_; + int32_t gain = fade; + if (fromStart < gain) gain = fromStart; + if (fromEnd < gain) gain = fromEnd; + + if (gain >= fade) + { + return sample; + } + if (gain <= 0) + { + return 0; + } + return (sample * gain) / fade; + } + + int32_t __not_in_flash_func(crossfadeVariationRetrigger)(int32_t target, int32_t start) + { + int32_t fade = kVariationFadeSamples; + int32_t elapsed = fade - variationFadeSamples_; + if (elapsed < 0) elapsed = 0; + if (elapsed > fade) elapsed = fade; + return ((start * (fade - elapsed)) + (target * elapsed)) / fade; + } + + void setAudioOut(int32_t outA, int32_t outB) + { + lastOutputA_ = outA; + lastOutputB_ = outB; + AudioOut1(outA); + AudioOut2(outB); + } + + void __not_in_flash_func(setAudioOutWithMonitor)( + int32_t outA, + int32_t outB, + bool outputAlreadyIncludesInputA, + bool outputAlreadyIncludesInputB) + { + if (monitorThisSample_) + { + if (monitorConnectedA_ && !outputAlreadyIncludesInputA) + { + outA = mixMonitorInput(outA, monitorInputA_); + } + if (monitorConnectedB_ && !outputAlreadyIncludesInputB) + { + outB = mixMonitorInput(outB, monitorInputB_); + } + } + + setAudioOut(outA, outB); + } + + bool shouldMonitorInput(Switch sw, bool recordHeld) const + { + switch (monitorMode_) + { + case kMonitorAlways: + return true; + case kMonitorWhenRecording: + return recordHeld; + case kMonitorWhenArmed: + default: + return sw == Switch::Up || recordHeld; + } + } + + static int32_t clampAudioOutput(int32_t sample) + { + if (sample < -2048) return -2048; + if (sample > 2047) return 2047; + return sample; + } + + static int32_t mixMonitorInput(int32_t program, int32_t monitor) + { + return clampAudioOutput((program + monitor) >> 1); + } + + void outputSlotSilenceOrLive(int slot, int32_t inA, int32_t inB) + { + outputSlotSampleOrLive(slot, inA, inB, 0, 0); + } + + void serviceFlashSave() + { + if (!flashSaveRequested_) + { + return; + } + + flashSaveRequested_ = false; + saveKitToFlash(); + saveFeedbackSamples_ = kSaveFeedbackSamples; + } + + void saveKitToFlash() + { + flashWriteOffset_ = 0; + flashPageFill_ = 0; + + uint32_t ints = save_and_disable_interrupts(); + flash_range_erase(kFlashStorageOffset, kFlashStorageSize); + restore_interrupts(ints); + + writeFlashU32(kFlashMagic); + writeFlashU32(kFlashVersion); + writeFlashU32((uint32_t)kBufferLength); + writeFlashU32((uint32_t)kNumSlots); + + for (int slot = 0; slot < kNumSlots; slot++) + { + writeFlashU32((uint32_t)recordedLength_[slot]); + writeFlashU32((uint32_t)recordedLengthA_[slot]); + writeFlashU32((uint32_t)recordedLengthB_[slot]); + writeFlashU32(recordingChannelA_[slot] ? 1u : 0u); + writeFlashU32(recordingChannelB_[slot] ? 1u : 0u); + writeFlashU32((uint32_t)playbackMode_[slot]); + writeFlashU32((uint32_t)reverseProb_[slot]); + } + + for (int slot = 0; slot < kNumSlots; slot++) + { + writeFlashBytes((uint8_t *)bufferA_[slot], kBufferLength); + writeFlashBytes((uint8_t *)bufferB_[slot], kBufferLength); + } + + writeFlashU32(kPatternFlashMagic); + for (int pattern = 0; pattern < kNumPatterns; pattern++) + { + writeFlashByte(patterns_[pattern].length); + writeFlashBytes(patterns_[pattern].steps, kMaxPatternLen); + } + + writeFlashU32(kCVConfigFlashMagic); + for (int output = 0; output < 2; output++) + { + writeFlashByte(cvRange_[output]); + writeFlashByte(cvQuant_[output]); + writeFlashByte(cvClockDiv_[output]); + writeFlashByte(cvSlew_[output]); + } + writeFlashByte(cv2CoupledToCV1_ ? 1 : 0); + + writeFlashU32(kMonitorConfigFlashMagic); + writeFlashByte(monitorMode_); + + flushFlashPage(); + } + + void loadKitFromFlash() + { + if (loadKitFromFlashOffset(kFlashStorageOffset, kBufferLength)) + { + return; + } + + if (kBufferLength != kLegacy350BufferLength + && loadKitFromFlashOffset( + kLegacy350FlashStorageOffset, + kLegacy350BufferLength)) + { + return; + } + + if (kBufferLength != kLegacy300BufferLength + && loadKitFromFlashOffset( + kLegacy300FlashStorageOffset, + kLegacy300BufferLength)) + { + return; + } + + if (kBufferLength != kLegacyV11BufferLength) + { + loadKitFromFlashOffset( + kLegacyV11FlashStorageOffset, + kLegacyV11BufferLength); + } + } + + bool loadKitFromFlashOffset(uint32_t storageOffset, uint32_t expectedBufferLength) + { + const uint8_t *read = (const uint8_t *)(XIP_BASE + storageOffset); + + uint32_t magic = readFlashU32(read); + uint32_t version = readFlashU32(read); + uint32_t bufferLength = readFlashU32(read); + uint32_t numSlots = readFlashU32(read); + + if (magic != kFlashMagic + || version != kFlashVersion + || bufferLength != expectedBufferLength + || numSlots != (uint32_t)kNumSlots) + { + return false; + } + + int32_t copyLength = (int32_t)bufferLength; + if (copyLength > kBufferLength) + { + copyLength = kBufferLength; + } + + for (int slot = 0; slot < kNumSlots; slot++) + { + recordedLength_[slot] = clampStoredLength((int32_t)readFlashU32(read)); + recordedLengthA_[slot] = clampStoredLength((int32_t)readFlashU32(read)); + recordedLengthB_[slot] = clampStoredLength((int32_t)readFlashU32(read)); + recordingChannelA_[slot] = readFlashU32(read) != 0; + recordingChannelB_[slot] = readFlashU32(read) != 0; + playbackMode_[slot] = clampPlaybackMode((int32_t)readFlashU32(read)); + reverseProb_[slot] = clamp12Bit((int32_t)readFlashU32(read)); + hasRecording_[slot] = recordingChannelA_[slot] || recordingChannelB_[slot]; + } + + for (int slot = 0; slot < kNumSlots; slot++) + { + for (int i = 0; i < copyLength; i++) + { + bufferA_[slot][i] = (int8_t)*read++; + } + read += bufferLength - copyLength; + for (int i = copyLength; i < kBufferLength; i++) + { + bufferA_[slot][i] = 0; + } + + for (int i = 0; i < copyLength; i++) + { + bufferB_[slot][i] = (int8_t)*read++; + } + read += bufferLength - copyLength; + for (int i = copyLength; i < kBufferLength; i++) + { + bufferB_[slot][i] = 0; + } + } + + if (readFlashU32(read) == kPatternFlashMagic) + { + const uint8_t *check = read; + bool valid = true; + + for (int pattern = 0; pattern < kNumPatterns; pattern++) + { + uint8_t length = *check++; + if (length < 1 || length > kMaxPatternLen) + { + valid = false; + } + + for (int step = 0; step < kMaxPatternLen; step++) + { + uint8_t slot = *check++; + if (step < length && slot >= kNumSlots) + { + valid = false; + } + } + } + + if (valid) + { + for (int pattern = 0; pattern < kNumPatterns; pattern++) + { + patterns_[pattern].length = *read++; + for (int step = 0; step < kMaxPatternLen; step++) + { + patterns_[pattern].steps[step] = *read++; + } + } + } + } + + if (readFlashU32(read) == kCVConfigFlashMagic) + { + for (int output = 0; output < 2; output++) + { + uint8_t range = *read++; + uint8_t quant = *read++; + uint8_t div = *read++; + uint8_t slew = *read++; + + if (range < kCVRangeCount + && quant < kCVQuantCount + && div < kCVClockDivCount + && slew < kCVSlewCount) + { + cvRange_[output] = range; + cvQuant_[output] = quant; + cvClockDiv_[output] = div; + cvSlew_[output] = slew; + cvRandomStepCount_[output] = 0; + } + } + + uint8_t coupled = *read++; + if (coupled <= 1) + { + cv2CoupledToCV1_ = coupled != 0; + } + } + + if (readFlashU32(read) == kMonitorConfigFlashMagic) + { + uint8_t monitor = *read++; + if (monitor < kMonitorModeCount) + { + monitorMode_ = monitor; + } + } + + writeIndex_ = 0; + playIndex_ = 0; + playPositionQ_ = 0; + repeatPosition_ = 0; + repeatsCompleted_ = 0; + stepFinished_ = false; + inSilenceFill_ = false; + recordingActive_ = false; + recomputeVariationRecordingState(); + return true; + } + + void writeFlashU32(uint32_t value) + { + writeFlashByte((uint8_t)(value & 0xFF)); + writeFlashByte((uint8_t)((value >> 8) & 0xFF)); + writeFlashByte((uint8_t)((value >> 16) & 0xFF)); + writeFlashByte((uint8_t)((value >> 24) & 0xFF)); + } + + void writeFlashBytes(uint8_t *data, uint32_t count) + { + for (uint32_t i = 0; i < count; i++) + { + writeFlashByte(data[i]); + } + } + + void writeFlashByte(uint8_t value) + { + flashPage_[flashPageFill_] = value; + flashPageFill_++; + + if (flashPageFill_ >= kFlashProgramPageSize) + { + programFlashPage(); + } + } + + void flushFlashPage() + { + if (flashPageFill_ == 0) + { + return; + } + + while (flashPageFill_ < kFlashProgramPageSize) + { + flashPage_[flashPageFill_] = 0xFF; + flashPageFill_++; + } + + programFlashPage(); + } + + void programFlashPage() + { + uint32_t ints = save_and_disable_interrupts(); + flash_range_program( + kFlashStorageOffset + flashWriteOffset_, + flashPage_, + kFlashProgramPageSize); + restore_interrupts(ints); + + flashWriteOffset_ += kFlashProgramPageSize; + flashPageFill_ = 0; + } + + uint32_t readFlashU32(const uint8_t *&read) + { + uint32_t value = (uint32_t)read[0] + | ((uint32_t)read[1] << 8) + | ((uint32_t)read[2] << 16) + | ((uint32_t)read[3] << 24); + read += 4; + return value; + } + + int32_t clampStoredLength(int32_t value) + { + if (value < 0) return 0; + if (value > kBufferLength) return kBufferLength; + return value; + } + + int32_t clamp12Bit(int32_t value) + { + if (value < 0) return 0; + if (value > 4095) return 4095; + return value; + } + + int clampPlaybackMode(int value) + { + if (value < kModeLooping || value > kModePassthrough) + { + return kModeOneShot; + } + return value; + } + + // Convert the latest clock period into the current repeat length. + void updateRepeatLength() + { + if (haveClockPeriod_) + { + repeatLengthSamples_ = clockPeriodSamples_ >> subdivisionShift_; + } + else + { + repeatLengthSamples_ = 0; + } + } + + // Decide once per step whether this slot should play backward. + void rollReverse() + { + uint32_t roll = nextRandom() & 0x0FFF; // 0-4095 + playReverse_ = (int32_t)roll < activeSlotReverseProbability(activeSlot_); + } + + int32_t activeSlotReverseProbability(int slot) const + { + return cvReverseOverrideActive_ ? cvReverseProb_ : reverseProb_[slot]; + } + + int32_t activeVariationReverseProbability() const + { + return cvReverseOverrideActive_ ? cvReverseProb_ : variationReverseProb_; + } + + // Small, fast random-number generator for musical variation. + uint32_t __not_in_flash_func(nextRandom)() + { + uint32_t x = rngState_; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + rngState_ = x; + return x; + } + + // LEDs show slot selection, the active step, or command status. + void updateLeds() + { + __mem_fence_acquire(); + + if (zCommandArmed_ || clearInProgress_) + { + bool flashOn = ((sampleCounter_ >> 13) & 1) == 0; + for (int i = 0; i < kNumSlots; i++) + { + LedOn(i, flashOn); + } + return; + } + + if (variationModeFeedbackSamples_ > 0) + { + bool flashOn = ((sampleCounter_ >> 12) & 1) == 0; + for (int i = 0; i < kNumSlots; i++) + { + LedOn(i, flashOn); + } + return; + } + + if (saveFeedbackSamples_ > 0) + { + for (int i = 0; i < kNumSlots; i++) + { + LedOn(i); + } + return; + } + + if (recordingActive_) + { + return; + } + + if (lastSwitchUp_) + { + // Switch up: selected recording slot. + for (int i = 0; i < kNumSlots; i++) + { + if (i == selectedSlot_) LedOn(i); else LedOff(i); + } + } + else + { + // Switch middle: currently playing slot, or active variation + // when booted into one-sample variation mode. + int litLed = variationMode_ ? activeVariation_ : activeSlot_; + for (int i = 0; i < kNumSlots; i++) + { + if (i == litLed) LedOn(i); else LedOff(i); + } + } + } + + int8_t bufferA_[kNumSlots][kBufferLength] = {{0}}; + int8_t bufferB_[kNumSlots][kBufferLength] = {{0}}; + + int32_t writeIndex_ = 0; + int32_t playIndex_ = 0; + int32_t playPositionQ_ = 0; + bool playDirReverse_ = false; + int32_t lastOutputA_ = 0; + int32_t lastOutputB_ = 0; + int32_t variationFadeSamples_ = 0; + int32_t variationFadeStartA_ = 0; + int32_t variationFadeStartB_ = 0; + + // Playback position inside the current divided repeat. + int32_t repeatPosition_ = 0; + bool inSilenceFill_ = false; + + // One Shot and Interrupt use these to know when their step is done. + int32_t repeatsCompleted_ = 0; + bool stepFinished_ = false; + int32_t recordedLength_[kNumSlots] = {0}; + int32_t recordedLengthA_[kNumSlots] = {0}; + int32_t recordedLengthB_[kNumSlots] = {0}; + int32_t variationRecordedLength_ = 0; + int32_t variationRecordedLengthA_ = 0; + int32_t variationRecordedLengthB_ = 0; + + bool hasRecording_[kNumSlots] = {false}; + bool recordingChannelA_[kNumSlots] = {false}; + bool recordingChannelB_[kNumSlots] = {false}; + bool variationHasRecording_ = false; + bool variationChannelA_ = false; + bool variationChannelB_ = false; + bool wasRecording_ = false; + bool recordingHadInput_ = false; + bool recordingWriteA_ = false; + bool recordingWriteB_ = false; + volatile bool pendingRecordFinalize_ = false; + volatile int pendingRecordSlot_ = 0; + volatile int32_t pendingRecordLength_ = 0; + volatile bool pendingRecordChannelA_ = false; + volatile bool pendingRecordChannelB_ = false; + volatile bool pendingRecordVariation_ = false; + bool bufferFull_ = false; + bool cv1SeenNegative_ = false; + bool cv2SeenNegative_ = false; + bool cvConnectedStable_[2] = {false, false}; + int32_t cvConnectedDebounceSamples_[2] = {0, 0}; + int32_t zHoldSamples_ = 0; + volatile bool zCommandArmed_ = false; + bool zCommandNeedsRelease_ = false; + int32_t zCommandTimeoutSamples_ = 0; + int32_t zDoubleTapSamples_ = 0; + bool zFirstTapDown_ = false; + bool zIgnoreRelease_ = false; + int32_t bootModeWindowSamples_ = kBootModeWindowSamples; + int32_t bootModeDownSamples_ = 0; + volatile bool variationMode_ = false; + volatile int32_t variationModeFeedbackSamples_ = 0; + volatile uint32_t variationPitchStepQ_ = kPlaybackRatioUnity; + bool isUSBMIDIHost_ = false; + bool switchUpMainPrimed_ = false; + int32_t lastSwitchUpMain_ = 0; + bool switchUpKnobsPrimed_ = false; + int lastEditSlot_ = -1; + int32_t lastKnobX_ = 0; + int32_t lastKnobY_ = 0; + bool middleMainPrimed_ = false; + int32_t lastMiddleMain_ = 0; + bool middleXYPrimed_ = false; + int32_t lastMiddleKnobX_ = 0; + int32_t lastMiddleKnobY_ = 0; + volatile int32_t randomCVTarget_[2] = {0, 0}; + int32_t randomCVCurrent_[2] = {0, 0}; + int32_t randomCVCurrentQ_[2] = {0, 0}; + int32_t lastRandomCVOutput_[2] = {4096, 4096}; + uint8_t cvRandomStepCount_[2] = {0, 0}; + volatile uint8_t cvRange_[2] = {kCVRangeBipolar6V, kCVRangeBipolar6V}; + volatile uint8_t cvQuant_[2] = {kCVQuantOff, kCVQuantOff}; + volatile uint8_t cvClockDiv_[2] = {0, 0}; + volatile uint8_t cvSlew_[2] = {0, 3}; + volatile bool cv2CoupledToCV1_ = false; + volatile uint8_t monitorMode_ = kMonitorWhenArmed; + bool monitorThisSample_ = false; + bool monitorConnectedA_ = false; + bool monitorConnectedB_ = false; + int32_t monitorInputA_ = 0; + int32_t monitorInputB_ = 0; + volatile uint32_t playbackStepQ_ = kPlaybackStepNormal; + Pattern patterns_[kNumPatterns] = {}; + volatile bool midiKnobActive_[3] = {false, false, false}; + volatile int32_t midiKnobValue_[3] = {0, 0, 0}; + int32_t lastPhysicalKnob_[3] = {0, 0, 0}; + uint8_t midiStatus_ = 0; + uint8_t midiData_[2] = {0, 0}; + uint8_t midiDataCount_ = 0; + uint8_t midiNote_ = 60; + int32_t pitchBendSemitoneQ_ = 0; + bool sysexActive_ = false; + uint8_t sysexBuffer_[kSysExBufferSize] = {0}; + uint32_t sysexLength_ = 0; + bool importActive_ = false; + int importSlot_ = 0; + uint8_t importChannelMask_ = 0; + int32_t importExpectedLength_ = 0; + int32_t importIndex_ = 0; + volatile bool flashSaveRequested_ = false; + volatile int32_t saveFeedbackSamples_ = 0; + alignas(4) uint8_t flashPage_[kFlashProgramPageSize] = {0}; + uint32_t flashPageFill_ = 0; + uint32_t flashWriteOffset_ = 0; + + volatile int recordingSlot_ = 0; + + // Shared control state. Volatile keeps both cores seeing fresh values. + volatile int selectedSlot_ = 0; // switch-up Main knob selection + volatile int selectedPattern_ = 0; // switch-middle Main knob selection (0-20) + volatile int shiftAmount_ = 0; // switch-middle X knob shift (0-5) + volatile int subdivisionShift_ = 0; // switch-middle Y knob: 0/1/2/3 = x1/x2/x4/x8 + volatile bool recordingActive_ = false; // currently recording? + int32_t recordDecimationPhase_ = 0; + int32_t recordAccumA_ = 0; + int32_t recordAccumB_ = 0; + volatile bool clockEdge_ = false; // set by core 0, cleared by core 1 + volatile bool resetEdge_ = false; // set by core 0, cleared by core 1 + + volatile uint32_t sampleCounter_ = 0; + + volatile int activeSlot_ = 0; // which slot to play (Middle) + volatile int activeVariation_ = kVariationNormal; + volatile bool restartPlayback_ = false; // set by core1, cleared by core0 + volatile bool playReverse_ = false; // direction for the current step + volatile int stepIndex_ = 0; + + // Samples per divided repeat. 0 means "no measured clock yet". + volatile int32_t repeatLengthSamples_ = 0; + + // 10ms trigger output counters. + volatile int32_t pulseOut1Samples_ = 0; + volatile int32_t pulseOut2Samples_ = 0; + + // Recent clocks keep switch-up from stopping playback. + int32_t clockActivitySamples_ = 0; + + volatile bool clearRequested_ = false; + volatile bool clearInProgress_ = false; + int32_t clearSlot_ = 0; + int32_t clearIndex_ = 0; + + // Per-slot playback mode, set with X while the switch is up. + int playbackMode_[kNumSlots] = { + kModeOneShot, kModeOneShot, kModeOneShot, + kModeOneShot, kModeOneShot, kModeOneShot + }; + + // Per-slot chance of reverse playback, set with Y while switch is up. + volatile int reverseProb_[kNumSlots] = {0}; + int variationPlaybackMode_ = kModeOneShot; + volatile int variationReverseProb_ = 0; + volatile bool cvReverseOverrideActive_ = false; + volatile int32_t cvReverseProb_ = 0; + + // xorshift32 must not be seeded with zero. + uint32_t rngState_ = 0x9F3779B9u; + + // Local mirrors let core 1 notice control changes. + int lastSeenPattern_ = -1; + + int lastSeenShift_ = 0; + int lastSeenSubShift_ = 0; + + // Clock-period tracking for divided repeats. + uint32_t lastClockSample_ = 0; + uint32_t clockPeriodSamples_ = 0; + bool haveClockPeriod_ = false; + + // Mirrors switch-up for LED display. + volatile bool lastSwitchUp_ = false; + + // Switch edge memory for reset/clear gestures. + bool lastSwitchDown_ = false; +}; + +static Fragments *gFragments = nullptr; + +int main() +{ + // ComputerCard recommends 144MHz because it is a clean multiple of + // the 48kHz audio rate and reduces ADC-related tonal noise. + set_sys_clock_khz(144000, true); + + // Fragments contains the audio buffers, so keep the object in + // static RAM rather than on the small main stack. Recording slot 5 + // writes to the far end of those buffers, which can expose a stack + // collision if the object is automatic. + static Fragments fragments; + gFragments = &fragments; + + // Launch the sequencer/LED loop on core 1. Core 0 continues into + // fragments.Run() below, which drives ProcessSample() from the + // audio interrupt. + multicore_launch_core1([]() { + // Run core 1 on the same Fragments instance as core 0. We use + // this explicit pointer instead of ComputerCard::ThisPtr() + // because ThisPtr() is assigned inside Run(), after core 1 has + // already been launched. + gFragments->Core1(); + }); + + fragments.Run(); +} diff --git a/releases/67_Fragments/src/usb_descriptors_24k.c b/releases/67_Fragments/src/usb_descriptors_24k.c new file mode 100644 index 000000000..345ed22ea --- /dev/null +++ b/releases/67_Fragments/src/usb_descriptors_24k.c @@ -0,0 +1,120 @@ +#include "tusb.h" +#include "pico/unique_id.h" + +#include + +#define USB_VID 0x2E8A +#define USB_PID 0x10C1 +#define USB_BCD 0x0200 + +enum { + STRING_LANGID = 0, + STRING_MANUFACTURER, + STRING_PRODUCT, + STRING_SERIAL, + STRING_LAST, +}; + +char const *string_desc_arr[] = { + (const char[]){0x09, 0x04}, + "Music Thing", + "Fragments 24k", + NULL, +}; + +tusb_desc_device_t const desc_device = { + .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = USB_BCD, + .bDeviceClass = 0x00, + .bDeviceSubClass = 0x00, + .bDeviceProtocol = 0x00, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + .idVendor = USB_VID, + .idProduct = USB_PID, + .bcdDevice = 0x0100, + .iManufacturer = STRING_MANUFACTURER, + .iProduct = STRING_PRODUCT, + .iSerialNumber = STRING_SERIAL, + .bNumConfigurations = 0x01, +}; + +uint8_t const *tud_descriptor_device_cb(void) +{ + return (uint8_t const *)&desc_device; +} + +enum { + ITF_NUM_MIDI = 0, + ITF_NUM_MIDI_STREAMING, + ITF_NUM_TOTAL, +}; + +#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_MIDI_DESC_LEN) +#define EPNUM_MIDI 0x01 + +uint8_t const desc_fs_configuration[] = { + TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100), + TUD_MIDI_DESCRIPTOR(ITF_NUM_MIDI, 0, EPNUM_MIDI, 0x80 | EPNUM_MIDI, 64), +}; + +uint8_t const *tud_descriptor_configuration_cb(uint8_t index) +{ + (void)index; + return desc_fs_configuration; +} + +static uint16_t desc_str[32]; + +uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) +{ + (void)langid; + + uint8_t chr_count = 0; + + if (index == STRING_LANGID) + { + memcpy(&desc_str[1], string_desc_arr[0], 2); + chr_count = 1; + } + else if (index == STRING_SERIAL) + { + pico_unique_board_id_t id; + pico_get_unique_board_id(&id); + uint64_t raw_id = *(uint64_t *)&id.id; + int serial = (int)((raw_id + 1) % 10000000ull); + if (serial < 1000000) serial += 1000000; + + char temp[8]; + chr_count = 7; + temp[7] = '\0'; + for (int i = 6; i >= 0; i--) + { + temp[i] = (char)('0' + (serial % 10)); + serial /= 10; + } + + for (uint8_t i = 0; i < chr_count; i++) + { + desc_str[1 + i] = temp[i]; + } + } + else if (index < STRING_LAST) + { + char const *str = string_desc_arr[index]; + chr_count = (uint8_t)strlen(str); + if (chr_count > 31) chr_count = 31; + + for (uint8_t i = 0; i < chr_count; i++) + { + desc_str[1 + i] = str[i]; + } + } + else + { + return NULL; + } + + desc_str[0] = (TUSB_DESC_STRING << 8) | (2 * chr_count + 2); + return desc_str; +} diff --git a/releases/67_Fragments/web/Fragments Manual.pdf b/releases/67_Fragments/web/Fragments Manual.pdf index 6386608b1..8907e8ec7 100644 Binary files a/releases/67_Fragments/web/Fragments Manual.pdf and b/releases/67_Fragments/web/Fragments Manual.pdf differ diff --git a/releases/67_Fragments/web/fragments_librarian.html b/releases/67_Fragments/web/fragments_librarian.html index 45753999d..49c54b2ae 100644 --- a/releases/67_Fragments/web/fragments_librarian.html +++ b/releases/67_Fragments/web/fragments_librarian.html @@ -3,7 +3,7 @@ - Fragments Librarian + Fragments Web Editor