diff --git a/.gitignore b/.gitignore index 936ff75..a8bebd5 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,13 @@ # OS X crap .DS_Stor? + +# Build artifacts +build/ +*.o +*.elf +*.hex +*.bin +*.syx +*.mid +__pycache__/ diff --git a/CARCOSA.md b/CARCOSA.md new file mode 100644 index 0000000..cf7fa8a --- /dev/null +++ b/CARCOSA.md @@ -0,0 +1,438 @@ +# Carcosa v2.06 +## Custom firmware for the Ambika polysynth + +Carcosa replaces the stock Ambika firmware with a focused set of synthesis engines, expanded envelope capabilities, and a streamlined interface. + +Based on the original Ambika firmware by Emilie Gillet (Mutable Instruments). + +--- + +## Synthesis Modes + +Carcosa has two categories of sound engines: **classic** dual-oscillator modes and **special** single-voice modes. + +### Classic Modes + +Classic modes work like the original Ambika. Two independent oscillators mixed together, processed through the analog filter. + +| Waveform | Parameter knob | +|----------|---------------| +| **none** | Silence | +| **saw** | — | +| **square** | Pulse width (0 = 50% square, 1-127 = variable PWM) | +| **triangle** | Waveshaping amount | +| **sine** | — | +| **pad** | Detune spread (4 detuned saws, supersaw-style) | +| **noise** | Filter color (0-63 = low-pass, 64-127 = high-pass) | + +In classic modes, both osc1 and osc2 are active. The mixer page controls balance, crossmod, sub oscillator, noise, fuzz, and crush. + +### Special Modes + +Special modes take over the entire voice. The mixer page parameters are repurposed for that engine's controls. Each has a dedicated UI with 1-2 pages of parameters. + +#### 4-Op FM (fm4op) + +TX81Z-style frequency modulation with 4 operators. + +**Page 1:** + +| Knob | Label | Range | Description | +|------|-------|-------|-------------| +| 1 | algo | 1-8 | Algorithm (operator routing topology) | +| 2 | fbk | 0-127 | Feedback on operator 4 | +| 3 | wav1 | sin-saw | Operator 1 waveform (8 TX81Z waveforms) | +| 4 | wav2 | sin-saw | Operator 2 waveform | +| 5 | rat1 | 0.5-127 | Operator 1 frequency ratio | +| 6 | fin1 | -64/+63 | Operator 1 fine detune | +| 7 | rat2 | 0.5-127 | Operator 2 frequency ratio | +| 8 | fin2 | -64/+63 | Operator 2 fine detune | + +**Page 2:** + +| Knob | Label | Range | Description | +|------|-------|-------|-------------| +| 1 | wav3 | sin-saw | Operator 3 waveform | +| 2 | wav4 | sin-saw | Operator 4 waveform | +| 3 | lvl1 | 0-127 | Operator 1 output level | +| 4 | lvl2 | 0-127 | Operator 2 output level | +| 5 | rat3 | 0.5-127 | Operator 3 frequency ratio | +| 6 | fin3 | -64/+63 | Operator 3 fine detune | +| 7 | lvl3 | 0-127 | Operator 3 output level | +| 8 | lvl4 | 0-127 | Operator 4 output level | + +**The 8 algorithms:** + +``` +1: 4->3->2->1 Full serial chain +2: (3+4)->2->1 Two modulators into carrier chain +3: (4->3)+2->1 Parallel mod + carrier into final carrier +4: (4->3)+(2->1) Two parallel pairs +5: (4->2)+(4->3->1) Shared modulator, two paths +6: 4->(1+2+3) One modulator, three carriers +7: (4->1)+2+3 One modulated + two free carriers +8: 1+2+3+4 All carriers (additive) +``` + +**Operator waveforms:** sin, half, abs, qtr, habs, tri, pls, saw + +**Modulation depth:** Operator levels use a TX81Z-style exponential curve (0.75 dB/step). Level 0 = silence, 64 = barely audible, 100 = moderate, 127 = full. The curve is steep above ~90 — start with low values (10-30) and bring up gradually. Per-operator envelopes (env 4-7) multiply the level after the exponential curve, matching the TX81Z signal chain. + +**Feedback:** 16-bit internal state creates evolving, non-repeating oscillation (similar to the TX81Z's 14-bit feedback dynamics). Higher values add saw-like harmonics to operator 4. + +The mod matrix shows FM-specific destination names when in FM mode: `algo`, `lvl1`-`lvl4`, `rat3`, `fin3`, etc. Route envelopes/LFOs to operator levels for dynamic FM timbres. + +#### Karplus-Strong (pluck) + +Physical model of a plucked string. A noise burst excites a tuned delay line with filtered feedback. + +**Page 1:** + +| Knob | Label | Range | Description | +|------|-------|-------|-------------| +| 1 | damp | 0-127 | Damping (high = darker, faster decay) | +| 2 | colr | 0-127 | Excitation brightness | +| 3 | dcay | 0-127 | Overall decay rate | +| 4 | body | 0-127 | Body resonance (comb filter at half delay) | +| 5 | rang | -24/+24 | Pitch range (semitones) | +| 6 | tune | -64/+63 | Fine tuning | +| 7 | pos | 0-127 | Pluck position (changes harmonic content) | +| 8 | exc | nois/clic/brit/dark | Excitation type | + +**Page 2:** + +| Knob | Label | Range | Description | +|------|-------|-------|-------------| +| 1 | rate | 0-127 | Ensemble LFO speed | +| 2 | dpth | 0-127 | Ensemble depth (3 read heads from same delay) | +| 3 | sprd | 0-127 | Ensemble spread (phase offset between heads) | +| 4 | wmix | 0-127 | Ensemble wet/dry mix | +| 5 | stif | 0-127 | String stiffness (inharmonic partials, scales with pitch) | +| 6 | feed | 0-127 | Sustain (reduces damping loss for longer ring, never self-oscillates) | + +**Damping** uses an IIR low-pass filter that sweeps from bright metallic ring (0) to dark muted thud (127). The mod matrix shows KS-specific destination names: `damp`, `body`, `stif`, `emix`, `feed`, etc. + +#### West Coast (wcoast) + +Buchla-style wavefolder with built-in FM and sub-oscillator. + +**Page 1:** + +| Knob | Label | Range | Description | +|------|-------|-------|-------------| +| 1 | wave | sin/tri | Base waveform | +| 2 | fold | 0-127 | Fold depth (harmonics increase with depth) | +| 3 | sym | 0-127 | Symmetry (even/odd harmonic balance, 64=center) | +| 4 | bias | 0-127 | DC offset into folder | +| 5 | fmdp | 0-127 | FM modulation depth | +| 6 | fmrt | -24/+24 | FM modulator ratio | +| 7 | colr | 0-127 | Post-fold brightness | +| 8 | drve | 0-127 | Pre-fold drive | + +**Page 2:** + +| Knob | Label | Range | Description | +|------|-------|-------|-------------| +| 1 | flds | 0-127 | Fold character (additional harmonic shaping) | +| 2 | gain | 0-127 | Input gain before folding | +| 3 | env | 0-127 | Envelope-to-fold depth (timbral VCA) | +| 4 | sub | 0-127 | Sub-oscillator level (one octave down) | +| 5 | sync | 0-127 | Self-sync amount (metallic tones) | + +**Wavefolder** uses a quadratic gain curve (1x at fold=0, 33x at fold=127) creating up to 16 folds — matching real Buchla 259 harmonic density. The **color** filter is a 2-pole IIR (12dB/oct) that sweeps from dark fundamental-only to bright full harmonics. + +Route an envelope to `fold` (fold depth) for the classic west coast behavior where timbre and amplitude are linked. The `env` parameter on page 2 does this directly — the signature Buchla pluck sound. The mod matrix shows WC-specific destination names: `fold`, `sym`, `bias`, `colr`, `envf`, `gain`, etc. + +#### Waveshaping (wshape) + +Drives a sine wave through a nonlinear waveshaper. Works as a normal oscillator (can be used on osc1 or osc2 alongside other waveforms). + +| Knob | Description | +|------|-------------| +| prm | Drive amount (0 = clean sine, 127 = heavily shaped) | +| rng | Pitch range | +| tun | Fine tuning | + +--- + +## Navigation + +### Classic mode (osc/mixer pages) + +- **S1 press**: Toggle between oscillator page and mixer page +- **Encoder**: Scroll through parameters, click to edit + +### Entering special modes + +1. While on the osc or mixer page, **press S1** — LED 1 starts blinking (mode select active) +2. **Turn encoder** to cycle through: classic, fm4op, pluck, wcoast +3. **Press S1 again** to confirm and exit mode select + +Mode select also works from inside any special mode page. Pressing any other button exits mode select automatically. + +### Engine isolation + +Each engine has its own state slot. When you switch engines, your current settings are saved and the target engine's settings are restored. Tweak an FM patch, switch to classic, switch back — your FM tweaks are right where you left them. No parameter bleed between engines. + +### Inside special mode pages + +- **S1 press**: Toggle between page 1 and page 2 +- **Encoder**: Scroll through parameters on the current page, click to edit +- **Knobs 1-8**: Direct control of the 8 parameters shown on screen + +### Other pages + +All other pages (filter, envelope, modulation, part, arpeggiator, multi, performance, system) work exactly like the original Ambika. + +--- + +## Envelopes + +Carcosa has **7 envelopes**. Envelopes 1-3 have paired LFOs and curve selection (exponential, linear, looping). Envelopes 4-7 are simple ADSR — lightweight extra modulation sources for per-operator FM control, layered modulation, or any creative routing. + +The ENV/LFO page selector (knob 1) cycles through all 7 envelopes. When env 1-3 is selected, the top row shows LFO rate, shape, and envelope curve. When env 4-7 is selected, the top row is blank — just ADSR on the bottom row. + +All 7 envelopes are available as modulation sources in the mod matrix (env1 through env7). + +Envelopes 1-3 have a selectable curve mode (knob 4, labeled `mode`): + +| Mode | Behavior | +|------|----------| +| **exp** | Exponential ADSR (original Ambika behavior) | +| **lin** | Linear ADSR (SH-101 style, snappy) | +| **loop** | Cycling AD with exponential curves (Maths/function generator) | +| **lpln** | Cycling AD with linear curves (triangle/ramp LFO) | + +In loop modes: +- **attk** = rise time +- **dec** = fall time +- **sus** = loop floor (how low the cycle goes before retriggering) +- **rel** = release time (when key is released, looping stops and envelope falls to zero) + +Each envelope's mode is independent. Use exponential ADSR on the VCA, looping linear on the filter, etc. + +--- + +## Slop + +The **slop** parameter on the part page (knob 3) adds per-note random variation to pitch and envelope timing. This simulates the analog component drift that gives vintage polysynths their character. + +- **0**: Pristine digital (no variation) +- **Low values (10-30)**: Subtle warmth, slight detuning between voices +- **High values (60-127)**: Wobbly, unstable, heavily vintage + +Each note-on generates a fresh random pitch offset (gentle, up to ~0.5 semitone at max). Envelope attack, decay, and release also vary per voice with a stronger effect — at high slop values, each voice in a chord has noticeably different envelope timing. + +--- + +## Randomizer + +Access via the performance button group (press 3 times: performance -> knob assign -> randomizer). + +| Knob | Section | +|------|---------| +| 1 | Oscillator randomization depth | +| 2 | Filter randomization depth | +| 3 | Envelope randomization depth | +| 4 | Modulation matrix randomization depth | +| 5 | Mixer randomization depth | +| 6 | LFO randomization depth | +| 7 | Global depth (scales all others) | +| 8 | Shows "push" — click encoder to trigger | + +Set knobs to 0 to lock that section. Turn up for more variation. Click the encoder to generate a new random patch. Each click produces a different result. + +--- + +## Firmware Update + +### Via SD Card + +1. Run `./flash_sd.sh /mnt/sdcard` (or copy `AMBIKA.BIN` and `VOICE1-6.BIN` manually) +2. Insert SD card into Ambika +3. **Hold S8 during power-on** to flash the controller +4. After reboot, go to OS info page, select each voicecard port (1-6), press S4 to flash + +Flash the controller first, then all 6 voicecards. + +### EEPROM auto-reset + +When upgrading from an older firmware version, Carcosa automatically detects stale EEPROM data and resets to factory defaults on first boot. This ensures the correct init patch, voice allocation, and engine settings are loaded. No manual factory reset needed. + +### Recovery + +If the firmware crashes on boot, **hold S8 during power-on** to force the bootloader to read from SD card. This always works regardless of firmware state. + +To revert to stock Ambika firmware, place the original `AMBIKA.BIN` and `VOICE*.BIN` files on the SD card and flash. + +--- + +## Technical Notes + +### Memory Usage + +| | Voicecard (ATmega328p) | Controller (ATmega644p) | +|---|---|---| +| Flash | 32,062 / 32,768 (97.8%) | 60,822 / 65,536 (92.8%) | +| RAM | 1,521 / 2,048 (74%) | 3,706 / 4,096 (90%) | + +### Changes from Stock Ambika + +**Removed:** CZ synthesis (9 variants), old 2-op FM, 8-bit land, dirty PWM, vowel synthesis, wavetable oscillators (16 + wavequence), step sequencer. + +**Added:** 4-op FM, Karplus-Strong, west coast wavefolder, waveshaping, looping envelopes, analog slop, smart randomizer, dedicated UI pages for special modes, per-engine mod destination names, per-engine state isolation, EEPROM auto-reset, extended octave range (-4/+4). + +**Bug fixes:** MIDI channel display (showed garbage for channels 1-16), engine switch parameter bleed, voicecard frame drops on engine switch, boot page routing, note stack uninitialized variables, transient generator off-by-one, SPI bulk write bounds check, page group initialization size. + +### Patch Compatibility + +Existing Ambika patches using saw, square, triangle, sine, pad, or noise will work unchanged. Patches using removed waveforms (CZ, wavetable, etc.) will have their oscillator shape reset to `none` on first load. + +--- + +## MIDI CC Reference + +All synthesis modes respond to MIDI CCs. The special modes reuse existing patch byte offsets, so the same CC numbers control different parameters depending on the active mode. + +### Global (all modes) + +| CC | Parameter | +|----|-----------| +| 108 | FM feedback (fm4op mode) | +| 109 | Slop | +| 110 | Envelope curve (env 1-3 only) | +| 111 | Extra envelope attack (env 4-7) | +| 112 | Extra envelope decay (env 4-7) | +| 113 | Extra envelope sustain (env 4-7) | +| 114 | Extra envelope release (env 4-7) | + +### Classic Mode + +| CC | Parameter | +|----|-----------| +| 16 | Osc 1 waveform | +| 17 | Osc 1 parameter | +| 14 | Osc 1 range | +| 15 | Osc 1 detune | +| 18 | Osc 2 waveform | +| 19 | Osc 2 parameter | +| 20 | Osc 2 range | +| 21 | Osc 2 detune | +| 22 | Mix balance | +| 23 | Mix operator | +| 24 | Mix parameter | +| 25 | Sub osc shape | +| 26 | Sub osc level | +| 27 | Noise level | +| 12 | Fuzz | +| 13 | Crush | +| 3 | Filter cutoff | +| 9 | Filter resonance | + +### 4-Op FM Mode + +| CC | FM parameter | +|----|-------------| +| 17 | Algorithm | +| 108 | Feedback | +| 18 | Op 1/2 waveform (packed) | +| 19 | Op 3/4 waveform (packed) | +| 14 | Op 1 ratio | +| 15 | Op 1 fine | +| 20 | Op 2 ratio | +| 21 | Op 2 fine | +| 22 | Op 3 ratio | +| 23 | Op 3 fine | +| 24 | Op 4 ratio | +| 25 | Op 4 fine | +| 26 | Op 1 level | +| 27 | Op 2 level | +| 12 | Op 3 level | +| 13 | Op 4 level | + +### Karplus-Strong Mode + +| CC | KS parameter | +|----|-------------| +| 17 | Damping | +| 19 | Excitation color | +| 20 | Decay rate | +| 21 | Pluck position | +| 18 | Excitation type | +| 22 | Body resonance | +| 23 | Ensemble rate | +| 24 | Ensemble depth | +| 25 | Ensemble spread | +| 26 | Ensemble mix | +| 27 | Stiffness | +| 12 | Extra feedback | + +### West Coast Mode + +| CC | WC parameter | +|----|-------------| +| 18 | Base waveform | +| 17 | Fold depth | +| 19 | Symmetry | +| 22 | Bias | +| 20 | FM depth | +| 21 | FM ratio | +| 24 | Color | +| 23 | Drive | +| 25 | Fold stages | +| 26 | Input gain | +| 27 | Env to fold | +| 12 | Sub level | +| 13 | Sync | + +--- + +## FAQ + +**Can I go back to the stock Ambika firmware?** +Yes. Put the original AMBIKA.BIN and VOICE*.BIN files on the SD card and hold S8 during power-on. The bootloader is never modified — you can always reflash. + +**Will my existing patches work?** +Patches using saw, square, triangle, sine, pad, or noise will sound the same. Patches using removed waveforms (CZ, wavetables, vowel, 8-bit, etc.) will have their oscillator reset to `none` on first load. Filter settings, envelopes, and mod matrix are unchanged. + +**Do I need to flash all 6 voicecards?** +Yes. The voicecard firmware contains the new synthesis engines. If you only flash the controller, the voicecards won't understand the new waveform types and you'll get silence or wrong sounds. + +**My Ambika shows garbage on the screen after flashing. Is it bricked?** +No. Hold S8 during power-on to force the bootloader to reflash from SD card. This always works regardless of what firmware is running. + +**Does this work with all voicecard types (4P, SVF, SMR)?** +Yes. Carcosa only changes the digital oscillator code. The analog filter on each voicecard is untouched and works exactly as before. + +**Can I use the special modes (FM, pluck, wcoast) with the analog filter?** +Yes. The filter is in the analog signal path after the DAC. Every synthesis mode runs through it. FM through the SSM2164 ladder filter sounds great. + +**Why remove the wavetables? I used those.** +The 16 wavetable banks consumed 10.3 KB of flash on a 32 KB chip. Removing them freed enough space for the FM, Karplus-Strong, and west coast engines combined. The tradeoff is intentional — fewer but deeper sound sources. + +**Why remove the step sequencer?** +The step sequencer consumed nearly 2 KB of controller flash and 384 bytes of RAM. Most Ambika owners sequence externally via MIDI. The arpeggiator is still present. Removing the sequencer gave enough headroom to keep all the new UI pages stable. + +**Can I use FM on one part and subtractive on another?** +Yes. Each part selects its own synthesis mode independently. Part 1 can be FM while part 2 plays saw through the filter. The mode is stored per-patch. + +**How does the slop parameter work?** +Each note-on generates a small random pitch offset and envelope timing variation. The amount is controlled by the slop knob on the part page. In a chord, each voice gets a different random value, simulating the component drift of analog polysynths. + +**How do looping envelopes work?** +Set the envelope mode to `loop` or `lpln`. The envelope cycles attack → decay → attack → decay continuously while the key is held. The sustain knob sets the floor of the cycle. When the key is released, the envelope falls to zero at the release rate. This turns an envelope into a function generator, similar to Make Noise Maths. + +**What are the 8 FM waveforms?** +They match the TX81Z: sine, half-sine, absolute sine, quarter sine, half-absolute, triple-absolute, pulse sine, and saw sine. All are derived from the existing sine table with zero extra flash cost. + +**Is this open source?** +Yes, and it always will be. Carcosa exists because Emilie Gillet open-sourced the Ambika firmware. That firmware exists because of open-source AVR toolchains, open hardware designs, and a community that shares knowledge freely. We stand on the shoulders of giants. GPL v3.0 — fork it, modify it, improve it, share it. That's the deal. + +--- + +## License + +GPL v3.0, same as the original Ambika firmware. + +Original developer: Emilie Gillet (emilie.o.gillet@gmail.com) +Carcosa firmware: Joseph Martin Giralt diff --git a/README b/README deleted file mode 100644 index 4e2d654..0000000 --- a/README +++ /dev/null @@ -1,13 +0,0 @@ -Ambika, a hybrid MIDI polysynth and voicecard host. - -Ambika consists of a compact motherboard serving as a "host" for up to 6 sound synthesis voicecard. While this design is primarily intended to be a flexible hybrid polysynth, it could also be used as a drum module/drum machine. - -The motherboard comprises 6 audio outputs, each one connected to a voicecard ; a global mono output ; a pair of MIDI input/output ; a SD card slot ; a 5V/8V/-8V power supply capable of delivering 150mA on the 8V rails and 350mA on the 5V rail ; the master MCU and the user interface elements. The voicecards (a pair of each being attached to the 3 voicecard ports) are SPI slaves, they receive note and modulation data from the motherboard ; and output monophonic audio, ideally 1V pp. - -3 designs of voicecards implementing a refined version of the Shruthi-1 engine are provided. Each of those use a different filter (4-Pole with LM13700, 4-Pole with SSM2164, 2-Pole SVF with SSM2164). - -Original developer: Olivier Gillet (olivier@mutable-instruments.net) - -The firmware is released under a GPL3.0 license. It includes a variant of the formant synthesis algorithm used in Peter Knight's Cantarino speech synthesizer. - -The PCB layouts and schematics, documentation, analyses, simulations and 3D models are released under a Creative Commons cc-by-sa 3.0 license. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1420808 --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# Carcosa +## Alternative firmware for the Ambika polysynth + +Carcosa is a custom firmware for the [Mutable Instruments Ambika](https://pichenettes.github.io/mutable-instruments-diy-archive/ambika/) that replaces the stock oscillator set with focused synthesis engines while keeping the analog signal path and 6-voice architecture intact. + +**[Download the latest release](https://github.com/joegiralt/ambika/releases)** + +**[Read the full manual](CARCOSA.md)** + +### Synthesis Engines + +| Mode | Description | +|------|-------------| +| saw, square, triangle, sine | Classic dual-oscillator subtractive (unchanged) | +| pad | 4-voice detuned supersaw | +| noise | Filtered noise with variable color | +| fm4op | 4-operator FM with 8 TX81Z algorithms, 8 waveforms, exponential levels, 16-bit feedback | +| pluck | Karplus-Strong with IIR damping, 3-head ensemble chorus, stiffness, 4 excitation types | +| wcoast | Buchla-style wavefolder (16+ folds), 2-pole color filter, FM, self-sync, sub oscillator | +| wshape | Waveshaping through nonlinear transfer function | + +### Additional Features + +- Per-envelope curve modes: exponential, linear, looping, looping linear +- Analog slop parameter for vintage poly character +- Smart patch randomizer with per-section depth control +- S1+encoder synthesis mode navigation + +### What was removed + +Step sequencer, CZ synthesis (9 variants), old 2-op FM, 8-bit land, dirty PWM, vowel synthesis, and wavetable oscillators (16 + wavequence). + +--- + +## Install + +Download all 7 .BIN files from the [releases page](https://github.com/joegiralt/ambika/releases) and copy them to the root of your SD card. + +1. Hold **S8** during power-on to flash the controller (AMBIKA.BIN) +2. Go to the OS info page, select each voicecard port (1-6), press **S4** to flash each + +Flash the controller first, then all 6 voicecards. + +For detailed instructions and recovery, see the [original Ambika firmware update guide](https://pichenettes.github.io/mutable-instruments-diy-archive/ambika/firmware/). + +To revert to stock Ambika firmware, place the original files on the SD card and reflash. + +## Build from source + +You'll need: +- make +- gcc-avr +- avr-libc + +``` +sudo apt-get install gcc-avr make avr-libc +git submodule update --init +export AVRPATH=$(which avr-gcc) +sed "s|AVRLIB_TOOLS_PATH ?=.*|AVRLIB_TOOLS_PATH ?= $(dirname $AVRPATH)/|" avrlib/makefile.mk > mkfiletmp +mv mkfiletmp avrlib/makefile.mk +``` + +Build voicecard: +``` +make all && make bin +``` + +Build controller: +``` +make -f controller/makefile && make -f controller/makefile bin +``` + +Flash to SD card: +``` +./flash_sd.sh /mnt/sdcard +``` + +## Credits + +Original Ambika firmware by Emilie Gillet (Mutable Instruments). Released under GPL v3.0. + +Carcosa firmware by Joseph Martin Giralt. diff --git a/avrlib b/avrlib index 7faa91d..276b288 160000 --- a/avrlib +++ b/avrlib @@ -1 +1 @@ -Subproject commit 7faa91d0e1a63a80d02b456272d888cb484eab01 +Subproject commit 276b2887e4110ca913294fcbb313163dfb28a448 diff --git a/common/lfo.h b/common/lfo.h index a559708..0e00951 100755 --- a/common/lfo.h +++ b/common/lfo.h @@ -1,6 +1,6 @@ -// Copyright 2009 Olivier Gillet. +// Copyright 2009 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/common/patch.h b/common/patch.h index d09ad9a..ca39971 100755 --- a/common/patch.h +++ b/common/patch.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -37,6 +37,22 @@ struct FilterSettings { uint8_t mode; }; +enum SynthEngine { + ENGINE_CLASSIC, + ENGINE_FM4OP, + ENGINE_KS_PLUCK, + ENGINE_WESTCOAST, + ENGINE_LAST +}; + +enum EnvelopeCurve { + ENVELOPE_CURVE_EXPONENTIAL, + ENVELOPE_CURVE_LINEAR, + ENVELOPE_CURVE_LOOP, + ENVELOPE_CURVE_LOOP_LINEAR, + ENVELOPE_CURVE_LAST +}; + struct EnvelopeLfoSettings { uint8_t attack; uint8_t decay; @@ -44,7 +60,7 @@ struct EnvelopeLfoSettings { uint8_t release; uint8_t shape; uint8_t rate; - uint8_t padding; + uint8_t envelope_curve; uint8_t retrigger_mode; }; @@ -65,25 +81,12 @@ enum OscillatorAlgorithm { WAVEFORM_SQUARE, WAVEFORM_TRIANGLE, WAVEFORM_SINE, - WAVEFORM_CZ_SAW, - WAVEFORM_CZ_SAW_LP, - WAVEFORM_CZ_SAW_PK, - WAVEFORM_CZ_SAW_BP, - WAVEFORM_CZ_SAW_HP, - WAVEFORM_CZ_PLS_LP, - WAVEFORM_CZ_PLS_PK, - WAVEFORM_CZ_PLS_BP, - WAVEFORM_CZ_PLS_HP, - WAVEFORM_CZ_TRI_LP, WAVEFORM_QUAD_SAW_PAD, - WAVEFORM_FM, - WAVEFORM_8BITLAND, - WAVEFORM_DIRTY_PWM, WAVEFORM_FILTERED_NOISE, - WAVEFORM_VOWEL, - WAVEFORM_WAVETABLE_1, - WAVEFORM_WAVETABLE_16 = WAVEFORM_WAVETABLE_1 - 1 + 16, - WAVEFORM_WAVEQUENCE, + WAVEFORM_FM4OP, + WAVEFORM_KS_PLUCK, + WAVEFORM_WAVESHAPE, + WAVEFORM_WESTCOAST, WAVEFORM_LAST }; @@ -163,19 +166,21 @@ enum ModulationSource { MOD_SRC_ENV_1, MOD_SRC_ENV_2, MOD_SRC_ENV_3, - + MOD_SRC_ENV_4, + MOD_SRC_ENV_5, + MOD_SRC_ENV_6, + MOD_SRC_ENV_7, + MOD_SRC_LFO_1, MOD_SRC_LFO_2, MOD_SRC_LFO_3, MOD_SRC_LFO_4, - + MOD_SRC_OP_1, MOD_SRC_OP_2, MOD_SRC_OP_3, MOD_SRC_OP_4, - - MOD_SRC_SEQ_1, - MOD_SRC_SEQ_2, + MOD_SRC_ARP_STEP, MOD_SRC_VELOCITY, @@ -235,8 +240,10 @@ enum FilterMode { }; static const uint8_t kNumSyncedLfoRates = 15; -static const uint8_t kNumEnvelopes = 3; -static const uint8_t kNumLfos = kNumEnvelopes; +static const uint8_t kNumEnvLfoSlots = 3; +static const uint8_t kNumLfos = kNumEnvLfoSlots; +static const uint8_t kNumTotalEnvelopes = 7; +static const uint8_t kNumExtraEnvelopes = 4; static const uint8_t kNumModulations = 14; static const uint8_t kNumModifiers = 4; static const uint8_t kNumOscillators = 2; @@ -263,7 +270,7 @@ struct Patch { int8_t filter_lfo; // Offset: 24-48 - EnvelopeLfoSettings env_lfo[kNumEnvelopes]; + EnvelopeLfoSettings env_lfo[kNumEnvLfoSlots]; // Offset: 48-50 uint8_t voice_lfo_shape; @@ -277,6 +284,9 @@ struct Patch { // Offset: 104-112 uint8_t padding[8]; + + // Offset: 112-144 + EnvelopeLfoSettings extra_env_lfo[kNumExtraEnvelopes]; }; typedef Patch PROGMEM prog_Patch; @@ -315,7 +325,7 @@ enum PatchParameter { PRM_PATCH_ENV_RELEASE, PRM_PATCH_LFO_SHAPE, PRM_PATCH_LFO_RATE, - PRM_PATCH_LFO_ATTACK, + PRM_PATCH_ENV_CURVE, PRM_PATCH_LFO_SYNC, PRM_PATCH_VOICE_LFO_SHAPE = 48, @@ -327,7 +337,14 @@ enum PatchParameter { PRM_PATCH_MOD_OPERAND1 = 92, PRM_PATCH_MOD_OPERAND2, - PRM_PATCH_MOD_OPERATOR + PRM_PATCH_MOD_OPERATOR, + + PRM_PATCH_SLOP = 105, + + PRM_PATCH_EXTRA_ENV_ATTACK = 112, + PRM_PATCH_EXTRA_ENV_DECAY, + PRM_PATCH_EXTRA_ENV_SUSTAIN, + PRM_PATCH_EXTRA_ENV_RELEASE, }; } // namespace ambika diff --git a/common/protocol.h b/common/protocol.h index 08f7f1a..7871376 100755 --- a/common/protocol.h +++ b/common/protocol.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/bootloader/bootloader.cc b/controller/bootloader/bootloader.cc index 7a65323..2b93554 100755 --- a/controller/bootloader/bootloader.cc +++ b/controller/bootloader/bootloader.cc @@ -1,4 +1,4 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/bootloader/makefile b/controller/bootloader/makefile index 57f3864..32ae723 100755 --- a/controller/bootloader/makefile +++ b/controller/bootloader/makefile @@ -1,4 +1,4 @@ -# Copyright 2011 Olivier Gillet. +# Copyright 2011 Emilie Gillet. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/controller/controller.cc b/controller/controller.cc index e68ce92..33ba01a 100644 --- a/controller/controller.cc +++ b/controller/controller.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -113,6 +113,7 @@ void Init() { system_settings.Init(true); } multi.Init(ui.shifted()); + ui.ShowPreviousPage(); storage.Init(); } diff --git a/controller/controller.h b/controller/controller.h index a4c8cbb..8fb7ac4 100755 --- a/controller/controller.h +++ b/controller/controller.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -34,7 +34,7 @@ const uint8_t kNumArpeggiatorPatterns = 22; const uint8_t kNumParts = 6; const uint8_t kNumVoices = 6; -const uint8_t kSystemVersion = 0x10; +const uint8_t kSystemVersion = 0x11; } // namespace ambika diff --git a/controller/display.cc b/controller/display.cc index dec1ccc..d70edec 100755 --- a/controller/display.cc +++ b/controller/display.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/display.h b/controller/display.h index 744210f..b09c3df 100755 --- a/controller/display.h +++ b/controller/display.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/hardware_config.h b/controller/hardware_config.h index 48d30d3..518f967 100755 --- a/controller/hardware_config.h +++ b/controller/hardware_config.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/hardware_design/pcb/Mobo-v09.brd b/controller/hardware_design/pcb/Mobo-v09.brd index 40ef71d..3177438 100644 Binary files a/controller/hardware_design/pcb/Mobo-v09.brd and b/controller/hardware_design/pcb/Mobo-v09.brd differ diff --git a/controller/hardware_design/pcb/Mobo-v09.sch b/controller/hardware_design/pcb/Mobo-v09.sch index 712af5c..8c062be 100644 Binary files a/controller/hardware_design/pcb/Mobo-v09.sch and b/controller/hardware_design/pcb/Mobo-v09.sch differ diff --git a/controller/leds.cc b/controller/leds.cc index ae848a4..bc71b2e 100755 --- a/controller/leds.cc +++ b/controller/leds.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/leds.h b/controller/leds.h index 9d94312..edb6e80 100755 --- a/controller/leds.h +++ b/controller/leds.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/makefile b/controller/makefile index 88dcf0d..15a780b 100755 --- a/controller/makefile +++ b/controller/makefile @@ -1,4 +1,4 @@ -# Copyright 2011 Olivier Gillet. +# Copyright 2011 Emilie Gillet. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/controller/midi_dispatcher.cc b/controller/midi_dispatcher.cc index 010096f..750d28c 100755 --- a/controller/midi_dispatcher.cc +++ b/controller/midi_dispatcher.cc @@ -1,6 +1,6 @@ -// Copyright 2009 Olivier Gillet. +// Copyright 2009 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/midi_dispatcher.h b/controller/midi_dispatcher.h index 41eee93..e04d3e2 100755 --- a/controller/midi_dispatcher.h +++ b/controller/midi_dispatcher.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/multi.cc b/controller/multi.cc index 4e90bff..4616543 100755 --- a/controller/multi.cc +++ b/controller/multi.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -40,9 +40,9 @@ uint8_t Multi::flags_; /* */ static const prog_MultiData init_settings PROGMEM = { - // Parts mappings. - 1, 0, 127, 0x15, - 2, 0, 127, 0x2a, + // Parts mappings: part 1 gets all 6 voices. + 1, 0, 127, 0x3F, + 2, 0, 127, 0, 3, 0, 127, 0, 4, 0, 127, 0, 5, 0, 127, 0, @@ -61,8 +61,8 @@ static const prog_MultiData init_settings PROGMEM = { 1, 22, 0, 1, 42, 0, - // Padding. - 0, 0, 0, 0, + // Padding: [0]=EEPROM version marker, [1-3]=unused. + 0x26, 0, 0, 0, }; /* static */ @@ -75,7 +75,8 @@ void Multi::Init(bool force_reset) { InitSettings(INITIALIZATION_DEFAULT); Storage::WriteMultiToEeprom(); } else { - if (!Storage::LoadMultiFromEeprom()) { + if (!Storage::LoadMultiFromEeprom() || data_.padding2[0] != 0x26) { + // EEPROM corrupt or from older firmware version — reset. InitSettings(INITIALIZATION_DEFAULT); Storage::WriteMultiToEeprom(); } else { diff --git a/controller/multi.h b/controller/multi.h index 4812837..56df504 100755 --- a/controller/multi.h +++ b/controller/multi.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/note_stack.h b/controller/note_stack.h index d6223e2..5bc853f 100755 --- a/controller/note_stack.h +++ b/controller/note_stack.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -62,7 +62,7 @@ class NoteStack { // In case of saturation, remove the least recently played note from the // stack. if (size_ == capacity) { - uint8_t least_recent_note; + uint8_t least_recent_note = pool_[1].note; for (uint8_t i = 1; i <= capacity; ++i) { if (pool_[i].next_ptr == 0) { least_recent_note = pool_[i].note; @@ -71,7 +71,7 @@ class NoteStack { NoteOff(least_recent_note); } // Now we are ready to insert the new note. Find a free slot to insert it. - uint8_t free_slot; + uint8_t free_slot = 1; for (uint8_t i = 1; i <= capacity; ++i) { if (pool_[i].note == kFreeSlot) { free_slot = i; @@ -151,6 +151,9 @@ class NoteStack { return pool_[current]; } const NoteEntry& played_note(uint8_t index) const { + if (size_ == 0) { + return pool_[0]; + } uint8_t current = root_ptr_; index = size_ - index - 1; for (uint8_t i = 0; i < index; ++i) { diff --git a/controller/parameter.cc b/controller/parameter.cc index ed70892..ad55289 100755 --- a/controller/parameter.cc +++ b/controller/parameter.cc @@ -1,6 +1,6 @@ -// Copyright 2009 Olivier Gillet. +// Copyright 2009 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -57,6 +57,8 @@ static const prog_uint16_t units_definitions[UNIT_LAST] PROGMEM = { STR_RES_SWING, // UNIT_GROOVE_TEMPLATE STR_RES_____, // UNIT_MIDI_IN_MASK STR_RES_THRU, // UNIT_MIDI_OUT_MODE + STR_RES_OMNI, // UNIT_MIDI_CHANNEL + 0, // UNIT_ENVELOPE_CURVE (handled as special case) }; static const prog_char note_names[] PROGMEM = " CC# DD# E FF# GG# AA# B"; @@ -187,12 +189,6 @@ void Parameter::PrintValue(uint8_t value, char* buffer, uint8_t width) const { } break; - case UNIT_MODULATION_SOURCE: - if (width > 5) { - text = STR_RES_ENV_1; - } - break; - case UNIT_MODULATION_DESTINATION: if (width > 5) { text = STR_RES_PARAM_1; @@ -213,9 +209,131 @@ void Parameter::PrintValue(uint8_t value, char* buffer, uint8_t width) const { case UNIT_MIDI_CHANNEL: if (value == 0) { text = STR_RES_OMNI; + } else { + text = 0; } } + // Envelope curve type. + if (unit == UNIT_ENVELOPE_CURVE) { + const prog_char* name; + switch (value) { + case ENVELOPE_CURVE_EXPONENTIAL: name = PSTR("exp"); break; + case ENVELOPE_CURVE_LINEAR: name = PSTR("lin"); break; + case ENVELOPE_CURVE_LOOP: name = PSTR("loop"); break; + case ENVELOPE_CURVE_LOOP_LINEAR: name = PSTR("lpln"); break; + default: name = PSTR("exp"); break; + } + strncpy_P(buffer, name, width); + AlignRight(buffer, width); + return; + } + + // Carcosa waveform names. + if (unit == UNIT_OSCILLATOR_WAVEFORM) { + const prog_char* name; + switch (value) { + case WAVEFORM_NONE: name = PSTR("none"); break; + case WAVEFORM_SAW: name = PSTR("saw"); break; + case WAVEFORM_SQUARE: name = PSTR("square"); break; + case WAVEFORM_TRIANGLE: name = PSTR("triangle"); break; + case WAVEFORM_SINE: name = PSTR("sine"); break; + case WAVEFORM_QUAD_SAW_PAD: name = PSTR("pad"); break; + case WAVEFORM_FILTERED_NOISE: name = PSTR("noise"); break; + case WAVEFORM_FM4OP: name = PSTR("fm4op"); break; + case WAVEFORM_KS_PLUCK: name = PSTR("pluck"); break; + case WAVEFORM_WAVESHAPE: name = PSTR("wshape"); break; + case WAVEFORM_WESTCOAST: name = PSTR("wcoast"); break; + default: name = PSTR("?"); break; + } + strncpy_P(buffer, name, width); + AlignRight(buffer, width); + return; + } + + // Engine-aware mod destination names for the mixer/osc destinations (0-11). + // Destinations 12+ (filter, envelope, vca) are the same across all engines. + if (unit == UNIT_MODULATION_DESTINATION && value < 12) { + uint8_t engine = multi.part(ui.state().active_part).raw_patch_data()[106]; + // Packed 5-char short names and 7-char long names per engine. + // Order: prm1 prm2 osc1 osc2 1+2 vibr mix xmod nois sub fuzz crsh + static const prog_char dst_classic_short[] PROGMEM = + "prm1\0" "prm2\0" "osc1\0" "osc2\0" "1+2 \0" "vibr\0" + "mix \0" "xmod\0" "nois\0" "sub \0" "fuzz\0" "crsh\0"; + static const prog_char dst_fm_short[] PROGMEM = + "algo\0" "wave\0" "osc1\0" "osc2\0" "1+2 \0" "vibr\0" + "rat3\0" "fin3\0" "lvl2\0" "lvl1\0" "lvl3\0" "lvl4\0"; + static const prog_char dst_ks_short[] PROGMEM = + "damp\0" "colr\0" "ptch\0" "osc2\0" "1+2 \0" "vibr\0" + "body\0" "xmod\0" "stif\0" "emix\0" "feed\0" "sync\0"; + static const prog_char dst_wc_short[] PROGMEM = + "fold\0" "sym \0" "ptch\0" "osc2\0" "1+2 \0" "vibr\0" + "bias\0" "colr\0" "envf\0" "gain\0" "sub \0" "sync\0"; + static const prog_char dst_classic_long[] PROGMEM = + "param1\0" "param2\0" "osc 1 \0" "osc 2 \0" "osc1+2\0" "vibrat\0" + "mix \0" "xmod \0" "noise \0" "subosc\0" "fuzz \0" "crush \0"; + static const prog_char dst_fm_long[] PROGMEM = + "algo \0" "waves \0" "osc 1 \0" "osc 2 \0" "osc1+2\0" "vibrat\0" + "rat 3 \0" "fine 3\0" "lvl 2 \0" "lvl 1 \0" "lvl 3 \0" "lvl 4 \0"; + static const prog_char dst_ks_long[] PROGMEM = + "dampin\0" "color \0" "pitch \0" "osc 2 \0" "osc1+2\0" "vibrat\0" + "body \0" "xmod \0" "stiff \0" "ensmix\0" "feedbk\0" "sync \0"; + static const prog_char dst_wc_long[] PROGMEM = + "fold \0" "symm \0" "pitch \0" "osc 2 \0" "osc1+2\0" "vibrat\0" + "bias \0" "color \0" "env>fl\0" "gain \0" "sub \0" "sync \0"; + const prog_char* table; + if (width > 5) { + static const prog_char* const long_tables[] PROGMEM = { + dst_classic_long, dst_fm_long, dst_ks_long, dst_wc_long + }; + table = (engine < ENGINE_LAST) + ? (const prog_char*)pgm_read_word(&long_tables[engine]) + : dst_classic_long; + memcpy_P(buffer, table + value * 7, width < 7 ? width : 7); + } else { + static const prog_char* const short_tables[] PROGMEM = { + dst_classic_short, dst_fm_short, dst_ks_short, dst_wc_short + }; + table = (engine < ENGINE_LAST) + ? (const prog_char*)pgm_read_word(&short_tables[engine]) + : dst_classic_short; + memcpy_P(buffer, table + value * 5, width < 5 ? width : 5); + } + AlignRight(buffer, width); + return; + } + + // Mod source names — packed PROGMEM tables, 5 chars each (short) / 7 chars each (long). + // Order matches ModulationSource enum. + if (unit == UNIT_MODULATION_SOURCE) { + static const prog_char mod_src_short[] PROGMEM = + "env1\0" "env2\0" "env3\0" "env4\0" "env5\0" "env6\0" "env7\0" + "lfo1\0" "lfo2\0" "lfo3\0" "lfo4\0" + "mod1\0" "mod2\0" "mod3\0" "mod4\0" + "arp \0" "velo\0" "aftr\0" "bend\0" "mwhl\0" "whl2\0" "pdal\0" + "note\0" "gate\0" "nois\0" "rand\0" + "=256\0" "=128\0" "=64 \0" "=32 \0" "=16 \0" "=8 \0" "=4 \0"; + static const prog_char mod_src_long[] PROGMEM = + "env 1\0\0" "env 2\0\0" "env 3\0\0" "env 4\0\0" + "env 5\0\0" "env 6\0\0" "env 7\0\0" + "lfo 1\0\0" "lfo 2\0\0" "lfo 3\0\0" "lfo 4\0\0" + "mod. 1\0" "mod. 2\0" "mod. 3\0" "mod. 4\0" + "arp \0" "velo \0" "afttch\0" "bender\0" + "mwheel\0" "wheel2\0" "pedal \0" + "note \0" "gate \0" "noise \0" "random\0" + "= 256 \0" "= 128 \0" "= 64 \0" "= 32 \0" + "= 16 \0" "= 8 \0" "= 4 \0"; + if (value < MOD_SRC_LAST) { + if (width > 5) { + memcpy_P(buffer, mod_src_long + value * 7, width < 7 ? width : 7); + } else { + memcpy_P(buffer, mod_src_short + value * 5, width < 5 ? width : 5); + } + } + AlignRight(buffer, width); + return; + } + if (text) { ResourcesManager::LoadStringResource(text + value, buffer, width); } else if (unit != UNIT_NOTE) { @@ -226,6 +344,11 @@ void Parameter::PrintValue(uint8_t value, char* buffer, uint8_t width) const { } void Parameter::PrintName(char* buffer, uint8_t width) const { + if (offset == PRM_PATCH_SLOP) { + strncpy_P(buffer, PSTR("slop"), width); + AlignLeft(buffer, width); + return; + } ResourceId caption = (width <= 6) ? short_name : long_name; ResourcesManager::LoadStringResource(caption, buffer, width); AlignLeft(buffer, width); @@ -293,9 +416,9 @@ static const prog_char midi_cc_map[128] PROGMEM = { 28, 25, 255, 26, 255, 255, 44, 45, // 96-111 255, 255, 255, 255, 255, 255, 49, 50, - 51, 52, 53, 57, 255, 255, 255, 255, + 51, 52, 53, 57, 75, 74, 73, 76, // 112-127 - 255, 255, 255, 255, 255, 255, 255, 255, + 77, 78, 79, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }; @@ -356,7 +479,7 @@ static const prog_Parameter parameters[kNumParameters] PROGMEM = { // 0 { PARAMETER_LEVEL_PATCH, PRM_PATCH_OSC1_SHAPE, - UNIT_OSCILLATOR_WAVEFORM, WAVEFORM_NONE, WAVEFORM_LAST - 1, + UNIT_OSCILLATOR_WAVEFORM, WAVEFORM_NONE, WAVEFORM_FILTERED_NOISE, 1, 0, 0xff, 16, STR_RES_WAVEFORM, STR_RES_WAVEFORM, STR_RES_OSCILLATOR_1 }, @@ -384,7 +507,7 @@ static const prog_Parameter parameters[kNumParameters] PROGMEM = { // 4 { PARAMETER_LEVEL_PATCH, PRM_PATCH_OSC2_SHAPE, - UNIT_OSCILLATOR_WAVEFORM, WAVEFORM_NONE, WAVEFORM_LAST - 1, + UNIT_OSCILLATOR_WAVEFORM, WAVEFORM_NONE, WAVEFORM_FILTERED_NOISE, 1, 0, 0xff, 18, STR_RES_WAVEFORM, STR_RES_WAVEFORM, STR_RES_OSCILLATOR_2 }, @@ -659,7 +782,7 @@ static const prog_Parameter parameters[kNumParameters] PROGMEM = { // 43 { PARAMETER_LEVEL_PART, PRM_PART_OCTAVE, - UNIT_INT8, -2, 2, + UNIT_INT8, -4, 4, 1, 0, 0xff, 0xff, STR_RES_OCTV, STR_RES_OCTAVE, STR_RES_PART }, @@ -733,26 +856,26 @@ static const prog_Parameter parameters[kNumParameters] PROGMEM = { 1, 0, 0xff, 106, STR_RES_GRID, STR_RES_GRID, STR_RES_ARPEGGIO }, - // 54 + // 54 - (removed: was sequence length 1) { PARAMETER_LEVEL_PART, - PRM_PART_SEQUENCE_LENGTH_1, - UNIT_UINT8, 1, 32, + PRM_PART_POLYPHONY_MODE + 1, + UNIT_UINT8, 0, 0, 1, 0, 0xff, 0xff, - STR_RES_LEN1, STR_RES_SEQ1_LEN, STR_RES_SEQUENCER }, + STR_RES_LEN1, STR_RES_SEQ1_LEN, STR_RES_PART }, - // 55 + // 55 - (removed: was sequence length 2) { PARAMETER_LEVEL_PART, - PRM_PART_SEQUENCE_LENGTH_2, - UNIT_UINT8, 1, 32, + PRM_PART_POLYPHONY_MODE + 2, + UNIT_UINT8, 0, 0, 1, 0, 0xff, 0xff, - STR_RES_LEN2, STR_RES_SEQ2_LEN, STR_RES_SEQUENCER }, + STR_RES_LEN2, STR_RES_SEQ2_LEN, STR_RES_PART }, - // 56 + // 56 - (removed: was sequence length 3) { PARAMETER_LEVEL_PART, - PRM_PART_SEQUENCE_LENGTH_3, - UNIT_UINT8, 1, 32, + PRM_PART_POLYPHONY_MODE + 3, + UNIT_UINT8, 0, 0, 1, 0, 0xff, 0xff, - STR_RES_LENP, STR_RES_PATT_LEN, STR_RES_SEQUENCER }, + STR_RES_LENP, STR_RES_PATT_LEN, STR_RES_PART }, // 57 { PARAMETER_LEVEL_PART, @@ -859,12 +982,61 @@ static const prog_Parameter parameters[kNumParameters] PROGMEM = { 1, 0, 0xff, 0xff, STR_RES_LEDS, STR_RES_CARD_LEDS, STR_RES_SYSTEM }, - // 71 2 + // 72 { PARAMETER_LEVEL_SYSTEM, PRM_SYSTEM_VOICECARD_SWAP_LEDS_COLORS, UNIT_BOOLEAN, 0, 1, 1, 0, 0xff, 0xff, STR_RES_SWAP_COLORS, STR_RES_SWAP_COLORS, STR_RES_SYSTEM }, + + // 73 - envelope mode: exp, lin, loop, lpln + { PARAMETER_LEVEL_PATCH, + PRM_PATCH_ENV_CURVE, + UNIT_ENVELOPE_CURVE, 0, ENVELOPE_CURVE_LAST - 1, + 3, 8, PRM_UI_ACTIVE_ENV_LFO, 0xff, + STR_RES_MODE, STR_RES_MODE, STR_RES_ENVELOPE }, + + // 74 - analog slop: per-note pitch/envelope randomization + { PARAMETER_LEVEL_PATCH, + PRM_PATCH_SLOP, + UNIT_RAW_UINT8, 0, 127, + 1, 0, 0xff, 0xff, + STR_RES_NOISE, STR_RES_NOISE, STR_RES_MIXER }, + + // 75 - FM feedback (padding[0], MIDI-only, not on any UI page) + { PARAMETER_LEVEL_PATCH, + 104, + UNIT_RAW_UINT8, 0, 127, + 1, 0, 0xff, 0xff, + STR_RES_AMOUNT, STR_RES_AMOUNT, STR_RES_MIXER }, + + // 76 - extra envelope 4 attack + { PARAMETER_LEVEL_PATCH, + PRM_PATCH_EXTRA_ENV_ATTACK, + UNIT_RAW_UINT8, 0, 127, + 4, 8, 0xff, 0xff, + STR_RES_ATTK, STR_RES_ATTACK, STR_RES_ENVELOPE }, + + // 77 - extra envelope 4 decay + { PARAMETER_LEVEL_PATCH, + PRM_PATCH_EXTRA_ENV_DECAY, + UNIT_RAW_UINT8, 0, 127, + 4, 8, 0xff, 0xff, + STR_RES_DECAY, STR_RES_DECAY, STR_RES_ENVELOPE }, + + // 78 - extra envelope 4 sustain + { PARAMETER_LEVEL_PATCH, + PRM_PATCH_EXTRA_ENV_SUSTAIN, + UNIT_RAW_UINT8, 0, 127, + 4, 8, 0xff, 0xff, + STR_RES_SUSTAIN, STR_RES_SUSTAIN, STR_RES_ENVELOPE }, + + // 79 - extra envelope 4 release + { PARAMETER_LEVEL_PATCH, + PRM_PATCH_EXTRA_ENV_RELEASE, + UNIT_RAW_UINT8, 0, 127, + 4, 8, 0xff, 0xff, + STR_RES_RELEASE, STR_RES_RELEASE, STR_RES_ENVELOPE }, }; /* static */ diff --git a/controller/parameter.h b/controller/parameter.h index 9e28330..e052807 100755 --- a/controller/parameter.h +++ b/controller/parameter.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -54,6 +54,7 @@ enum Unit { UNIT_MIDI_IN_MASK, UNIT_MIDI_OUT_MODE, UNIT_MIDI_CHANNEL, + UNIT_ENVELOPE_CURVE, UNIT_LAST }; @@ -117,7 +118,7 @@ struct Parameter { static void PrintNote(uint8_t note, char* buffer); }; -const uint8_t kNumParameters = 73; +const uint8_t kNumParameters = 80; typedef Parameter PROGMEM prog_Parameter; // The parameter manager is the class who knows how to apply a parameter change diff --git a/controller/part.cc b/controller/part.cc index 55d906a..7c20037 100755 --- a/controller/part.cc +++ b/controller/part.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -37,12 +37,12 @@ static const prog_uint16_t lfo_phase_increment_per_clock_tick[15] PROGMEM = { }; static const prog_Patch init_patch PROGMEM = { - // Oscillators - WAVEFORM_SAW, 0, 0, 0, - WAVEFORM_SQUARE, 32, -12, 12, - - // Mixer - 32, OP_SUM, 31, WAVEFORM_SUB_OSC_SQUARE_1, 0, 0, 0, 0, + // Oscillators (FM mode: algo, ratios, waveforms packed) + WAVEFORM_SAW, 1, 4, 0, // osc0: shape=SAW(fallback), param=algo1, range=R_100, det=0 + 0, 0, 4, 0, // osc1: wav1|wav2=0(sine), wav3|wav4=0, range=R_100, det=0 + + // Mixer (FM mode: ratios, levels) + 4, 0, 4, 0, 20, 10, 10, 1, // Filter 96, 0, 0, 0, 0, 0, 24, 0, @@ -62,8 +62,8 @@ static const prog_Patch init_patch PROGMEM = { MOD_SRC_LFO_2, MOD_DST_PARAMETER_2, 0, MOD_SRC_LFO_3, MOD_DST_MIX_BALANCE, 0, MOD_SRC_LFO_4, MOD_DST_FILTER_CUTOFF, 0, - MOD_SRC_SEQ_1, MOD_DST_FILTER_CUTOFF, 0, - MOD_SRC_SEQ_2, MOD_DST_MIX_BALANCE, 0, + MOD_SRC_ENV_4, MOD_DST_FILTER_CUTOFF, 0, + MOD_SRC_ENV_5, MOD_DST_MIX_BALANCE, 0, MOD_SRC_ENV_3, MOD_DST_VCA, 63, MOD_SRC_VELOCITY, MOD_DST_VCA, 16, @@ -73,63 +73,37 @@ static const prog_Patch init_patch PROGMEM = { // Modifiers MOD_SRC_LFO_1, MOD_SRC_LFO_2, 0, MOD_SRC_LFO_2, MOD_SRC_LFO_3, 0, - MOD_SRC_LFO_3, MOD_SRC_SEQ_1, 0, - MOD_SRC_SEQ_1, MOD_SRC_SEQ_2, 0, + MOD_SRC_LFO_3, MOD_SRC_ENV_4, 0, + MOD_SRC_ENV_4, MOD_SRC_ENV_5, 0, - // Padding - 0, 0, 0, 0, 0, 0, 0, 0, + // Padding: [0]=feedback, [1]=unused, [2]=engine (1=FM), [3-7]=unused + 0, 0, ENGINE_FM4OP, 0, 0, 0, 0, 0, + + // Extra envelope-LFO settings (env4-7: ADSR + LFO shape/rate/curve/retrigger) + 0, 40, 20, 60, LFO_WAVEFORM_TRIANGLE, 0, 0, 0, + 0, 40, 20, 60, LFO_WAVEFORM_TRIANGLE, 0, 0, 0, + 0, 40, 20, 60, LFO_WAVEFORM_TRIANGLE, 0, 0, 0, + 0, 40, 20, 60, LFO_WAVEFORM_TRIANGLE, 0, 0, 0, }; static const prog_PartData init_part PROGMEM = { // Volume 120, - + // Octave and tuning - 0, 0, 0, 0, - + -1, 0, 0, 0, + // Legato, portamento, seq mode 0, 0, 0, - + // Arp data 0, 1, 0, 10, - - // Sequence length - 16, - 16, - 16, + + // Polyphony mode POLY, - - // Step sequence 1 - 0xff, 0xff, 0x80, 0x80, 0xcc, 0xcc, 0x20, 0x20, - 0x00, 0x20, 0x40, 0x60, 0x80, 0xa0, 0xc0, 0xff, - // Step sequence 1 - 0x00, 0x10, 0x20, 0x40, 0x80, 0xff, 0x80, 0x40, - 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x80, 0x40, - - // Note sequence - 60 | 0x80, 100, - 60, 100, - 48 | 0x80, 100, - 48, 100, - 60 | 0x80, 100, - 60, 100, - 48 | 0x80, 100, - 48, 100, - 60 | 0x80, 100, - 60, 100, - 48 | 0x80, 100, - 48 | 0x80, 100 | 0x80, - 60 | 0x80, 100, - 60, 100, - 48 | 0x80, 100, - 48, 100, - // Padding - 0, - 0, - 0, - 0, + 0, 0, 0, 0, 0, 0, 0, }; void Part::Touch() { @@ -154,6 +128,15 @@ void Part::Touch() { } void Part::TouchPatch() { + // Sanitize waveform values — only in classic mode. + // Special modes repurpose osc[1].shape for packed FM waveform nibbles. + if (patch_.padding[2] == ENGINE_CLASSIC) { + for (uint8_t i = 0; i < kNumOscillators; ++i) { + if (patch_.osc[i].shape >= WAVEFORM_LAST) { + patch_.osc[i].shape = WAVEFORM_NONE; + } + } + } flags_ = FLAG_HAS_CHANGE; // Send an "enter block write" command to all voicecards. for (uint8_t i = 0; i < num_allocated_voices_; ++i) { @@ -195,17 +178,6 @@ void Part::InitSettings(InitializationMode mode) { Touch(); } -void Part::InitSequence(InitializationMode mode) { - if (mode == INITIALIZATION_DEFAULT) { - memcpy_P( - mutable_raw_sequence_data(), - (prog_char*)(&init_part) + 8, - 72); - } else { - RandomizeRange(PRM_PART_VOLUME + 8, 72); - } -} - void Part::RandomizeRange(uint8_t start, uint8_t size) { for (uint8_t i = start; i < start + size; ++i) { uint8_t parameter_id = parameter_manager.AddressToParameterId(i); @@ -591,7 +563,6 @@ void Part::Clock() { ++midi_clock_counter_; if (midi_clock_counter_ >= midi_clock_prescaler_) { midi_clock_counter_ = 0; - ClockSequencer(); ClockArpeggiator(); } @@ -615,7 +586,6 @@ void Part::Clock() { } void Part::Start() { - memset(sequencer_step_, 0, kNumSequences); memset(lfo_step_, 0, kNumLfos); midi_clock_counter_ = midi_clock_prescaler_; previous_generated_note_ = 0xff; @@ -835,54 +805,6 @@ void Part::RetriggerLfos() { } } -void Part::ClockSequencer() { - // Update the value of the sequencer in the modulation matrix. - for (uint8_t i = 0; i < 2; ++i) { - uint8_t value = data_.step_value(i, sequencer_step_[i]); - if (data_.sequence_length[i]) { - WriteToAllVoices(VOICECARD_DATA_MODULATION, MOD_SRC_SEQ_1 + i, value); - } - } - - // Trigger notes if there's a note sequence, and if a key is pressed on the - // keyboard. - if (data_.arp_sequencer_mode == ARP_SEQUENCER_MODE_NOTE && - pressed_keys_.size() && data_.sequence_length[2]) { - NoteStep n = data_.note_step(sequencer_step_[2]); - uint8_t note = Clip(static_cast(n.note) + \ - pressed_keys_.most_recent_note().note - 60, 0, 127); - if (!n.gate) { - // Just kill the previous note. - InternalNoteOff(previous_generated_note_); - previous_generated_note_ = 0xff; - } else { - if (!n.legato) { - // Kill the previous note and move to the new note. - InternalNoteOff(previous_generated_note_); - InternalNoteOn(note, n.velocity & 0x7f); - } else { - // Kill the previous note, but only after having started playing the - // new one. - if (previous_generated_note_ != note) { - InternalNoteOn(note, n.velocity & 0x7f); - InternalNoteOff(previous_generated_note_); - } else { - // Do nothing, this is just a note being held. - } - } - previous_generated_note_ = note; - } - } - - // Jump to the next step in the sequencer. - for (uint8_t i = 0; i < kNumSequences; ++i) { - ++sequencer_step_[i]; - if (sequencer_step_[i] >= data_.sequence_length[i]) { - sequencer_step_[i] = 0; - } - } -} - void Part::ClockArpeggiator() { uint16_t pattern = ResourcesManager::Lookup( lut_res_arpeggiator_patterns, diff --git a/controller/part.h b/controller/part.h index 5fd464f..eedd5e4 100755 --- a/controller/part.h +++ b/controller/part.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -69,15 +69,6 @@ enum PartFlags { FLAG_HAS_USER_CHANGE = 2, }; -static const uint8_t kNumSequences = 3; - -struct NoteStep { - uint8_t note; - uint8_t velocity; - uint8_t legato; - uint8_t gate; -}; - struct PartData { // Offset: 0 uint8_t volume; @@ -99,76 +90,11 @@ struct PartData { uint8_t arp_pattern; uint8_t arp_divider; - // Offset: 12..16 - uint8_t sequence_length[kNumSequences]; + // Offset: 12 uint8_t polyphony_mode; - // Offset: 16-80 - // 0..15: step sequence 1 - // 16..31: step sequence 2 - // 32..63: (note value | 0x80 if gate), (note velocity | 0x80 if legato) - uint8_t sequence_data[64]; - - // Offset: 80-84 - uint8_t padding[4]; - - uint8_t step_value(uint8_t sequence, uint8_t step) const { - return sequence_data[(step + (sequence << 4)) & 0x1f]; - } - - void set_step_value(uint8_t sequence, uint8_t step, uint8_t value) { - sequence_data[(step + (sequence << 4)) & 0x1f] = value; - } - - uint8_t note(uint8_t step) const { - uint8_t offset = (32 + (step << 1)) & 0x3f; - return sequence_data[offset] & 0x7f; - } - - void set_note(uint8_t step, uint8_t note) { - uint8_t offset = (32 + (step << 1)) & 0x3f; - sequence_data[offset] &= 0x80; - sequence_data[offset] |= note; - } - - uint16_t ordered_gate_velocity(uint8_t step) const { - uint8_t offset = (32 + (step << 1)) & 0x3f; - if (!(sequence_data[offset] & 0x80)) { - return 0; - } else { - uint8_t o = sequence_data[offset + 1] + 128; - return static_cast(o) + 1; - } - } - - void set_ordered_gate_velocity(uint8_t step, uint16_t value) { - uint8_t offset = (32 + (step << 1)) & 0x3f; - if (!value) { - sequence_data[offset] &= 0x7f; - sequence_data[offset + 1] = 0; - } else { - sequence_data[offset] |= 0x80; - sequence_data[offset + 1] = (value - 1) + 128; - } - } - - void set_velocity(uint8_t step, uint8_t velocity) { - uint8_t offset = (32 + (step << 1)) & 0x3f; - sequence_data[offset + 1] &= 0x80; - sequence_data[offset + 1] |= velocity; - } - - NoteStep note_step(uint8_t step) const { - NoteStep n; - uint8_t offset = (32 + (step << 1)) & 0x3f; - n.note = sequence_data[offset]; - n.velocity = sequence_data[offset + 1]; - n.gate = n.note & 0x80; - n.legato = n.velocity & 0x80; - n.note &= 0x7f; - n.velocity &= 0x7f; - return n; - } + // Offset: 13-19 + uint8_t padding[7]; }; typedef PartData PROGMEM prog_PartData; @@ -186,9 +112,6 @@ enum PartParameter { PRM_PART_ARP_OCTAVE, PRM_PART_ARP_PATTERN, PRM_PART_ARP_RESOLUTION, - PRM_PART_SEQUENCE_LENGTH_1, - PRM_PART_SEQUENCE_LENGTH_2, - PRM_PART_SEQUENCE_LENGTH_3, PRM_PART_POLYPHONY_MODE }; @@ -199,7 +122,6 @@ class Part { void InitPatch(InitializationMode mode); void InitSettings(InitializationMode mode); - void InitSequence(InitializationMode mode); void NoteOn(uint8_t note, uint8_t velocity); void NoteOff(uint8_t note); @@ -230,19 +152,13 @@ class Part { PartData* mutable_data() { return &data_; } const PartData& data() const { return data_; } - const uint8_t* raw_sequence_data() const { - return static_cast(static_cast(&data_)) + 8; - } - const uint8_t* raw_data() const { + const uint8_t* raw_data() const { return static_cast(static_cast(&data_)); } const uint8_t* raw_patch_data() const { return static_cast(static_cast(&patch_)); } - uint8_t* mutable_raw_sequence_data() { - return static_cast(static_cast(&data_)) + 8; - } uint8_t* mutable_raw_data() { return static_cast(static_cast(&data_)); } @@ -251,10 +167,10 @@ class Part { } uint8_t lfo_value(uint8_t index) const { return lfo_previous_values_[index]; } - uint8_t step(uint8_t index) const { return sequencer_step_[index]; } void Touch(); void TouchPatch(); + void WriteToAllVoices(uint8_t data_type, uint8_t address, uint8_t value); void UpdateLfos(uint8_t refresh_cycle); void AssignVoices(uint8_t allocation); @@ -271,16 +187,13 @@ class Part { void RetriggerLfos(); - // Called on each "tick" of the arpeggiator and sequencer clock. - void ClockSequencer(); + // Called on each "tick" of the arpeggiator clock. void ClockArpeggiator(); // Called whenever a new arpeggiator note has to be triggered. void StartArpeggio(); void StepArpeggio(); - void WriteToAllVoices(uint8_t data_type, uint8_t address, uint8_t value); - // Check whether a note should be played by this part. A note can be rejected // if it is outside of the split region, or if it is outside of the selected // scale/raga. @@ -321,9 +234,6 @@ class Part { uint8_t nrpn_msb_; uint8_t nrpn_; - // Sequencer state. - uint8_t sequencer_step_[kNumSequences]; - // Arpeggiator state. uint8_t previous_generated_note_; uint16_t arp_pattern_mask_; diff --git a/controller/resources.cc b/controller/resources.cc index 0682519..d0c3201 100644 --- a/controller/resources.cc +++ b/controller/resources.cc @@ -1,6 +1,6 @@ -// Copyright 2009 Olivier Gillet. +// Copyright 2009 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/resources.h b/controller/resources.h index 525b614..9cb0f12 100644 --- a/controller/resources.h +++ b/controller/resources.h @@ -1,6 +1,6 @@ -// Copyright 2009 Olivier Gillet. +// Copyright 2009 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/resources/characters.py b/controller/resources/characters.py index 313de6d..044586c 100755 --- a/controller/resources/characters.py +++ b/controller/resources/characters.py @@ -1,8 +1,8 @@ #!/usr/bin/python2.5 # -# Copyright 2009 Olivier Gillet. +# Copyright 2009 Emilie Gillet. # -# Author: Olivier Gillet (ol.gillet@gmail.com) +# Author: Emilie Gillet (emilie.o.gillet@gmail.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/controller/resources/lookup_tables.py b/controller/resources/lookup_tables.py index 3f56cf9..4c72d32 100755 --- a/controller/resources/lookup_tables.py +++ b/controller/resources/lookup_tables.py @@ -1,8 +1,8 @@ #!/usr/bin/python2.5 # -# Copyright 2009 Olivier Gillet. +# Copyright 2009 Emilie Gillet. # -# Author: Olivier Gillet (ol.gillet@gmail.com) +# Author: Emilie Gillet (emilie.o.gillet@gmail.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/controller/resources/resources.py b/controller/resources/resources.py index 52b8e21..a299ffe 100755 --- a/controller/resources/resources.py +++ b/controller/resources/resources.py @@ -1,8 +1,8 @@ #!/usr/bin/python2.5 # -# Copyright 2009 Olivier Gillet. +# Copyright 2009 Emilie Gillet. # -# Author: Olivier Gillet (ol.gillet@gmail.com) +# Author: Emilie Gillet (emilie.o.gillet@gmail.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -19,9 +19,9 @@ # # Master resources file. -header = """// Copyright 2009 Olivier Gillet. +header = """// Copyright 2009 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/resources/strings.py b/controller/resources/strings.py index 7f50230..48982e8 100755 --- a/controller/resources/strings.py +++ b/controller/resources/strings.py @@ -1,8 +1,8 @@ #!/usr/bin/python2.5 # -# Copyright 2011 Olivier Gillet. +# Copyright 2011 Emilie Gillet. # -# Author: Olivier Gillet (ol.gillet@gmail.com) +# Author: Emilie Gillet (emilie.o.gillet@gmail.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/controller/resources/waveforms.py b/controller/resources/waveforms.py index 91a08a2..1a2a480 100755 --- a/controller/resources/waveforms.py +++ b/controller/resources/waveforms.py @@ -1,8 +1,8 @@ #!/usr/bin/python2.5 # -# Copyright 2011 Olivier Gillet. +# Copyright 2011 Emilie Gillet. # -# Author: Olivier Gillet (ol.gillet@gmail.com) +# Author: Emilie Gillet (emilie.o.gillet@gmail.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/controller/storage.cc b/controller/storage.cc index b0e6df0..0758d0e 100755 --- a/controller/storage.cc +++ b/controller/storage.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -154,10 +154,7 @@ uint8_t Storage::object_size(const StorageLocation& location) { switch (location.object) { case STORAGE_OBJECT_PATCH: return sizeof(Patch); - - case STORAGE_OBJECT_SEQUENCE: - return 72; - + case STORAGE_OBJECT_PART: return sizeof(PartData); @@ -188,10 +185,7 @@ const uint8_t* Storage::object_data(const StorageLocation& location) { switch (location.object) { case STORAGE_OBJECT_PATCH: return multi.part(location.part).raw_patch_data(); - - case STORAGE_OBJECT_SEQUENCE: - return multi.part(location.part).raw_sequence_data(); - + case STORAGE_OBJECT_PART: return multi.part(location.part).raw_data(); @@ -206,10 +200,7 @@ uint8_t* Storage::mutable_object_data(const StorageLocation& location) { switch (location.object) { case STORAGE_OBJECT_PATCH: return multi.mutable_part(location.part)->mutable_raw_patch_data(); - - case STORAGE_OBJECT_SEQUENCE: - return multi.mutable_part(location.part)->mutable_raw_sequence_data(); - + case STORAGE_OBJECT_PART: return multi.mutable_part(location.part)->mutable_raw_data(); @@ -279,7 +270,6 @@ void Storage::ForEachObject( destination.alias = 0; switch(destination.object) { case STORAGE_OBJECT_PATCH: - case STORAGE_OBJECT_SEQUENCE: case STORAGE_OBJECT_PART: (*object_fn)(destination); break; @@ -320,11 +310,13 @@ void Storage::TouchObject(const StorageLocation& location) { // object is freshly loaded and has received no user changes. switch (location.object) { case STORAGE_OBJECT_PATCH: + multi.mutable_part(location.part)->AllSoundOff(); + multi.mutable_part(location.part)->TouchPatch(); + ConstantDelay(5); multi.mutable_part(location.part)->TouchPatch(); break; case STORAGE_OBJECT_PART: - case STORAGE_OBJECT_SEQUENCE: multi.mutable_part(location.part)->Touch(); break; @@ -336,8 +328,12 @@ void Storage::TouchObject(const StorageLocation& location) { /* static */ void Storage::SysExSendObject(const StorageLocation& location) { + // Map StorageObject to SysEx command, preserving wire protocol + // (skip old slot 2 = sequence). + uint8_t cmd = location.object + 1; + if (cmd >= 2) ++cmd; // shift up past removed sequence slot SysExSendRaw( - location.object + 1, + cmd, location.alias, object_data(location), object_size(location), @@ -397,8 +393,11 @@ void Storage::RIFFWriteObject(const StorageLocation& location) { w.value = size + 4; file_.Write(w.bytes, 4, &written); // Write the position words. + // Map StorageObject to wire format (skip old slot 2 = sequence). w.value = 0; - w.bytes[0] = location.object + 1; + uint8_t wire_obj = location.object + 1; + if (wire_obj >= 2) ++wire_obj; // shift up past removed sequence slot + w.bytes[0] = wire_obj; w.bytes[1] = location.alias; file_.Write(w.bytes, 4, &written); @@ -450,7 +449,14 @@ FilesystemStatus Storage::Load( if (id.value == kObjectTag && load_contents) { file_.Read(id.bytes, 4, &read); StorageLocation destination; - destination.object = static_cast(id.bytes[0] - 1); + uint8_t obj_cmd = id.bytes[0]; + if (obj_cmd == 2) { + // Old sequence object — skip. + file_.Seek(file_.tell() + size.value - 4); + continue; + } + if (obj_cmd >= 3) --obj_cmd; // shift down past removed sequence slot + destination.object = static_cast(obj_cmd - 1); destination.part = id.bytes[1] == 0 ? location.part : (id.bytes[1] - 1); uint8_t expected_size = object_size(destination); @@ -670,13 +676,15 @@ void Storage::SysExParseCommand() { sysex_rx_state_ = RECEIVING_DATA; switch (sysex_rx_command_[0]) { case 0x01: - case 0x02: case 0x03: case 0x04: case 0x05: { StorageLocation location; - location.object = static_cast(sysex_rx_command_[0] - 1); + // Map SysEx command to StorageObject (skip old 0x02 = sequence). + uint8_t cmd = sysex_rx_command_[0]; + if (cmd >= 0x03) --cmd; // shift down past removed sequence slot + location.object = static_cast(cmd - 1); sysex_rx_expected_size_ = object_size(location); } break; @@ -689,7 +697,6 @@ void Storage::SysExParseCommand() { break; case 0x11: - case 0x12: case 0x13: case 0x14: case 0x15: @@ -719,13 +726,16 @@ void Storage::SysExAcceptCommand() { switch (sysex_rx_command_[0]) { case 0x01: - case 0x02: case 0x03: case 0x04: case 0x05: - location.object = static_cast(sysex_rx_command_[0] - 0x01); - ReadObject(location); - TouchObject(location); + { + uint8_t cmd = sysex_rx_command_[0]; + if (cmd >= 0x03) --cmd; // shift down past removed sequence slot + location.object = static_cast(cmd - 0x01); + ReadObject(location); + TouchObject(location); + } break; case 0x0f: @@ -741,12 +751,15 @@ void Storage::SysExAcceptCommand() { break; case 0x11: - case 0x12: case 0x13: case 0x14: case 0x15: - location.object = static_cast(sysex_rx_command_[0] - 0x11); - SysExSend(location); + { + uint8_t cmd = sysex_rx_command_[0]; + if (cmd >= 0x13) --cmd; // shift down past removed sequence slot + location.object = static_cast(cmd - 0x11); + SysExSend(location); + } break; case 0x1f: diff --git a/controller/storage.h b/controller/storage.h index b32796b..8e436d1 100755 --- a/controller/storage.h +++ b/controller/storage.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -34,10 +34,9 @@ using avrlib::FilesystemStatus; enum StorageObject { STORAGE_OBJECT_PATCH, - STORAGE_OBJECT_SEQUENCE, + STORAGE_OBJECT_PART, STORAGE_OBJECT_PROGRAM, STORAGE_OBJECT_MULTI, - STORAGE_OBJECT_PART, }; enum SysExReceptionState { diff --git a/controller/system_settings.cc b/controller/system_settings.cc index 2435395..e02bf6b 100755 --- a/controller/system_settings.cc +++ b/controller/system_settings.cc @@ -1,6 +1,6 @@ -// Copyright 2009 Olivier Gillet. +// Copyright 2009 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/system_settings.h b/controller/system_settings.h index 1cde3e2..dec3932 100755 --- a/controller/system_settings.h +++ b/controller/system_settings.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/ui.cc b/controller/ui.cc index 9f02dd0..84b761c 100755 --- a/controller/ui.cc +++ b/controller/ui.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -33,8 +33,12 @@ #include "controller/ui_pages/library.h" #include "controller/ui_pages/os_info_page.h" #include "controller/ui_pages/parameter_editor.h" +#include "controller/ui_pages/env_page.h" +#include "controller/ui_pages/fm_page.h" +#include "controller/ui_pages/ks_page.h" +#include "controller/ui_pages/wc_page.h" #include "controller/ui_pages/performance_page.h" -#include "controller/ui_pages/sequence_editor.h" +#include "controller/ui_pages/randomizer.h" #include "controller/ui_pages/version_manager.h" #include "controller/ui_pages/voice_assigner.h" #include "controller/voicecard_tx.h" @@ -61,14 +65,14 @@ const prog_PageInfo page_registry[] PROGMEM = { }, { PAGE_ENV_LFO, - &ParameterEditor::event_handlers_, - { 24, 30, 31, 29, 25, 26, 27, 28 }, + &EnvPage::event_handlers_, + { 0, 0, 0, 0, 0, 0, 0, 0 }, PAGE_VOICE_LFO, 2, 0xf0, }, - + { PAGE_VOICE_LFO, &ParameterEditor::event_handlers_, - { 0xff, 32, 33, 0xff, 0xff, 0xff, 0xff, 0xff }, + { 0xff, 32, 33, 29, 0xff, 0xff, 0xff, 0xff }, PAGE_ENV_LFO, 2, 0x0f, }, @@ -80,20 +84,14 @@ const prog_PageInfo page_registry[] PROGMEM = { { PAGE_PART, &ParameterEditor::event_handlers_, - { 42, 57, 47, 48, 43, 44, 45, 46 }, + { 42, 57, 74, 48, 43, 44, 45, 46 }, PAGE_PART_ARPEGGIATOR, 4, 0xf0, }, { PAGE_PART_ARPEGGIATOR, &ParameterEditor::event_handlers_, - { 49, 50, 51, 52, 53, 54, 55, 56 }, - PAGE_PART_SEQUENCER, 4, 0x0f, - }, - - { PAGE_PART_SEQUENCER, - &SequenceEditor::event_handlers_, - { 0, 0, 0, 0, 0, 0, 0, 0 }, - PAGE_PART, 4, 0xff, + { 49, 50, 51, 52, 53, 0xff, 0xff, 0xff }, + PAGE_PART, 4, 0x0f, }, { PAGE_MULTI, @@ -117,7 +115,7 @@ const prog_PageInfo page_registry[] PROGMEM = { { PAGE_KNOB_ASSIGN, &KnobAssigner::event_handlers_, { 0, 0, 0, 0, 0, 0, 0, 0, }, - PAGE_PERFORMANCE, 6, 0x0f, + PAGE_RANDOMIZER, 6, 0x0f, }, { PAGE_LIBRARY, @@ -132,6 +130,30 @@ const prog_PageInfo page_registry[] PROGMEM = { PAGE_LIBRARY, 8, 0xf0, }, + { PAGE_FM4OP, + &FmPage::event_handlers_, + { 0, 0, 0, 0, 0, 0, 0, 0, }, + PAGE_FM4OP, 0, 0xf0, + }, + + { PAGE_KS_PLUCK, + &KsPage::event_handlers_, + { 0, 0, 0, 0, 0, 0, 0, 0, }, + PAGE_KS_PLUCK, 0, 0xf0, + }, + + { PAGE_WESTCOAST, + &WcPage::event_handlers_, + { 0, 0, 0, 0, 0, 0, 0, 0, }, + PAGE_WESTCOAST, 0, 0xf0, + }, + + { PAGE_RANDOMIZER, + &Randomizer::event_handlers_, + { 0, 0, 0, 0, 0, 0, 0, 0, }, + PAGE_PERFORMANCE, 6, 0x0f, + }, + { PAGE_SYSTEM_SETTINGS, &ParameterEditor::event_handlers_, { 66, 67, 71, 72, 68, 69, 0xff, 70, }, @@ -169,6 +191,7 @@ Pots Ui::pots_; UiState Ui::state_; EventQueue<8> Ui::queue_; uint8_t Ui::pot_value_[8]; +uint8_t Ui::s1_held_; UiPageNumber Ui::most_recent_page_in_group_[9]; PageInfo Ui::page_info_; @@ -183,7 +206,7 @@ static char line[41]; /* static */ void Ui::Init() { memset(&state_, 0, sizeof(UiState)); - memcpy_P(most_recent_page_in_group_, default_most_recent_page_in_group, 8); + memcpy_P(most_recent_page_in_group_, default_most_recent_page_in_group, 9); encoder_.Init(); switches_.Init(); @@ -314,21 +337,88 @@ void Ui::DoEvents() { case CONTROL_ENCODER: if (e.control_id == 0) { - (*event_handlers_.OnIncrement)(e.value); + if (s1_held_) { + // Mode select: cycle engine type via padding[2]. + // Each engine gets its own slot for the shared patch bytes + // (offsets 0-15, 104) so switching engines preserves state. + static const uint8_t pages[] PROGMEM = { + PAGE_OSCILLATORS, PAGE_FM4OP, + PAGE_KS_PLUCK, PAGE_WESTCOAST + }; + + // Shared byte offsets: 0-15 plus 104 (feedback). + static const uint8_t kSharedCount = 17; + static const uint8_t shared_offsets[kSharedCount] PROGMEM = { + 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15, + 104 + }; + + // Per-engine state slots, initialized with defaults. + // [engine][byte index] — 4 engines × 17 bytes = 68 bytes. + static uint8_t engine_state[ENGINE_LAST][kSharedCount] = { + // ENGINE_CLASSIC: saw osc, centered mix, everything else 0. + { WAVEFORM_SAW, 0, 0, 0, WAVEFORM_NONE, 0, 0, 0, + 32, 0, 0, 0, 0, 0, 0, 0, 0 }, + // ENGINE_FM4OP: algo 1, 1:1 ratios, gentle levels. + { WAVEFORM_SAW, 1, 4, 0, 0, 0, 4, 0, + 4, 0, 4, 0, 20, 10, 10, 1, 0 }, + // ENGINE_KS_PLUCK: moderate damping, centered body/position. + { 0, 40, 0, 0, 0, 64, 64, 64, + 64, 0, 0, 0, 0, 0, 0, 0, 0 }, + // ENGINE_WESTCOAST: sine, moderate fold. + { 0, 80, 0, 0, 0, 64, 0, 0, + 64, 0, 0, 0, 0, 0, 0, 0, 0 }, + }; + + uint8_t* patch = multi.mutable_part( + state_.active_part)->mutable_raw_patch_data(); + uint8_t prev = patch[106]; // current engine + int8_t idx = prev; + idx += (static_cast(e.value) > 0) ? 1 : -1; + if (idx < 0) idx = ENGINE_LAST - 1; + if (idx >= ENGINE_LAST) idx = 0; + + // Save current engine's state. + for (uint8_t i = 0; i < kSharedCount; ++i) { + engine_state[prev][i] = patch[pgm_read_byte(&shared_offsets[i])]; + } + // Load target engine's state. + patch[106] = idx; + for (uint8_t i = 0; i < kSharedCount; ++i) { + patch[pgm_read_byte(&shared_offsets[i])] = engine_state[idx][i]; + } + + multi.mutable_part(state_.active_part)->TouchPatch(); + ShowPage(static_cast( + pgm_read_byte(&pages[idx]))); + } else { + (*event_handlers_.OnIncrement)(e.value); + } } else { int8_t new_part = state_.active_part + e.value; new_part = Clip(new_part, 0, kNumParts - 1); state_.active_part = new_part; } break; - + case CONTROL_SWITCH: + // Double-press S1 on osc/special page toggles mode select. + if (e.control_id == 0 && ( + active_page_ == PAGE_OSCILLATORS || + active_page_ == PAGE_MIXER || + active_page_ == PAGE_FM4OP || + active_page_ == PAGE_KS_PLUCK || + active_page_ == PAGE_WESTCOAST)) { + s1_held_ = !s1_held_; + break; + } + // Any other button exits mode select. + s1_held_ = 0; if (!(*event_handlers_.OnKey)(e.control_id)) { - // Cycle through the next page in the group. if (page_info_.group == e.control_id) { ShowPage(page_info_.next_page); } else { - // Jump to the most recently visited page in the group. ShowPage(most_recent_page_in_group_[e.control_id]); } } @@ -375,6 +465,15 @@ void Ui::DoEvents() { velocity | leds.pixel(LED_PART_1 + led_index)); } (*event_handlers_.UpdateLeds)(); + // Blink LED 1 when S1 held on osc/special page (mode select ready). + if (s1_held_ && ( + active_page_ == PAGE_OSCILLATORS || + active_page_ == PAGE_FM4OP || + active_page_ == PAGE_KS_PLUCK || + active_page_ == PAGE_WESTCOAST)) { + uint8_t blink = (cycle_ & 0x40) ? 0xf0 : 0x0f; + leds.set_pixel(LED_1, blink); + } if (system_settings.data().swap_leds_colors) { for (uint8_t i = 0; i < 15; ++i) { leds.set_pixel(i, U8Swap4(leds.pixel(i))); @@ -383,6 +482,18 @@ void Ui::DoEvents() { leds.Sync(); } +/* static */ +void Ui::ShowPreviousPage() { + // Route to the correct engine page based on padding[2]. + uint8_t engine = multi.part(state_.active_part).raw_patch_data()[106]; + switch (engine) { + case ENGINE_FM4OP: ShowPage(PAGE_FM4OP); break; + case ENGINE_KS_PLUCK: ShowPage(PAGE_KS_PLUCK); break; + case ENGINE_WESTCOAST: ShowPage(PAGE_WESTCOAST); break; + default: ShowPage(most_recent_non_system_page_); break; + } +} + /* static */ void Ui::ShowPage(UiPageNumber page, uint8_t initialize) { // Flush the event queue. diff --git a/controller/ui.h b/controller/ui.h index 7cf4272..404292b 100755 --- a/controller/ui.h +++ b/controller/ui.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -86,7 +86,6 @@ enum UiPageNumber { PAGE_PART, PAGE_PART_ARPEGGIATOR, - PAGE_PART_SEQUENCER, PAGE_MULTI, PAGE_MULTI_CLOCK, @@ -96,7 +95,12 @@ enum UiPageNumber { PAGE_LIBRARY, PAGE_VERSION_MANAGER, - PAGE_SYSTEM_SETTINGS, + PAGE_FM4OP, + PAGE_KS_PLUCK, + PAGE_WESTCOAST, + PAGE_RANDOMIZER, + + PAGE_SYSTEM_SETTINGS, PAGE_OS_INFO, }; @@ -175,9 +179,8 @@ class Ui { static void ShowPage(UiPageNumber page) { ShowPage(page, 1); } - static void ShowPreviousPage() { - ShowPage(most_recent_non_system_page_); - }; + static void ShowPreviousPage(); + static void ShowDialogBox(uint8_t dialog_id, Dialog dialog, uint8_t choice); static void CloseDialogBox(uint8_t return_value); @@ -215,6 +218,7 @@ class Ui { static uint8_t pot_value_[8]; static uint8_t active_group_; + static uint8_t s1_held_; static UiPageNumber most_recent_page_in_group_[9]; static EventHandlers event_handlers_; static PageInfo page_info_; diff --git a/controller/ui_pages/card_info_page.cc b/controller/ui_pages/card_info_page.cc index 9141bba..f240666 100755 --- a/controller/ui_pages/card_info_page.cc +++ b/controller/ui_pages/card_info_page.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/ui_pages/card_info_page.h b/controller/ui_pages/card_info_page.h index c323c77..69d6f27 100755 --- a/controller/ui_pages/card_info_page.h +++ b/controller/ui_pages/card_info_page.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/ui_pages/dialog_box.cc b/controller/ui_pages/dialog_box.cc index 1a87628..026f64b 100755 --- a/controller/ui_pages/dialog_box.cc +++ b/controller/ui_pages/dialog_box.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/ui_pages/dialog_box.h b/controller/ui_pages/dialog_box.h index 016560f..8bc209e 100755 --- a/controller/ui_pages/dialog_box.h +++ b/controller/ui_pages/dialog_box.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/ui_pages/env_page.cc b/controller/ui_pages/env_page.cc new file mode 100644 index 0000000..2e50b76 --- /dev/null +++ b/controller/ui_pages/env_page.cc @@ -0,0 +1,349 @@ +// Copyright 2026 Ambika contributors. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ----------------------------------------------------------------------------- +// +// Custom UI page for 7-envelope selector (env1-3 with LFO, env4-7 ADSR only). + +#include "controller/ui_pages/env_page.h" + +#include +#include "avrlib/string.h" + +#include "controller/display.h" +#include "controller/leds.h" +#include "controller/multi.h" +#include "controller/voicecard_tx.h" +#include "controller/ui.h" +#include "common/patch.h" + +namespace ambika { + +using namespace avrlib; + +/* static */ +uint8_t EnvPage::selector_; + +// LFO shape short names (4 chars each). Only first 4 basic shapes shown by +// name; wavetable shapes displayed as number. +static const prog_char lfo_shape_names[] PROGMEM = + "tri " "sqr " "s&h " "ramp"; + +// Envelope curve short names (4 chars each). +static const prog_char env_curve_names[] PROGMEM = + "exp " "lin " "loop" "lpln"; + +// Top-row labels for selector 0-2 (4 chars each, knobs 0-3). +static const prog_char env_top_labels_lfo[] PROGMEM = + "lfeg" "rate" "shap" "curv"; + +// Bottom-row labels (knobs 4-7), same for all selectors. +static const prog_char env_bot_labels[] PROGMEM = + "attk" "dec " "sus " "rel "; + +// Top-row label for selector 3-6 knob 0 only. +static const prog_char env_top_label_sel[] PROGMEM = "lfeg"; + +/* static */ +const prog_EventHandlers EnvPage::event_handlers_ PROGMEM = { + OnInit, + NULL, + OnIncrement, + OnClick, + OnPot, + OnKey, + NULL, + OnIdle, + UpdateScreen, + UpdateLeds, + NULL, +}; + +static inline uint8_t* GetPatchData() { + return multi.mutable_part(ui.state().active_part)->mutable_raw_patch_data(); +} + +/* static */ +uint8_t EnvPage::AdsrOffset(uint8_t knob) { + // knob: 0=attack, 1=decay, 2=sustain, 3=release + if (selector_ < 3) { + return 24 + selector_ * 8 + knob; + } else { + return 112 + (selector_ - 3) * 8 + knob; + } +} + +/* static */ +void EnvPage::OnInit(PageInfo* info) { + UiPage::OnInit(info); + active_control_ = 0; + selector_ = 0; +} + +/* static */ +uint8_t EnvPage::OnIncrement(int8_t increment) { + if (edit_mode_ != EDIT_IDLE) { + uint8_t* patch = GetPatchData(); + uint8_t ctrl = active_control_; + + if (ctrl == 0) { + // Selector: 0-6. + int16_t val = selector_ + increment; + if (val < 0) val = 0; + if (val > 6) val = 6; + selector_ = val; + } else if (ctrl >= 4) { + // ADSR knob (ctrl 4-7 -> knob 0-3). + uint8_t offset = AdsrOffset(ctrl - 4); + int16_t val = patch[offset] + increment; + if (val < 0) val = 0; + if (val > 127) val = 127; + patch[offset] = val; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + } else if (ctrl == 3) { + // Curve knob — works for all 7 envelopes. + uint8_t base = selector_ < 3 + ? (24 + selector_ * 8) + : (112 + (selector_ - 3) * 8); + uint8_t offset = base + 6; + int16_t val = patch[offset] + increment; + if (val < 0) val = 0; + if (val >= ENVELOPE_CURVE_LAST) val = ENVELOPE_CURVE_LAST - 1; + patch[offset] = val; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + } else if (selector_ < 3) { + // LFO controls (ctrl 1-2) only for env 1-3. + uint8_t base = 24 + selector_ * 8; + if (ctrl == 1) { + // Rate (offset +5), 0-127. + uint8_t offset = base + 5; + int16_t val = patch[offset] + increment; + if (val < 0) val = 0; + if (val > 127) val = 127; + patch[offset] = val; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + } else if (ctrl == 2) { + // Shape (offset +4), 0 to LFO_WAVEFORM_LAST-1. + uint8_t offset = base + 4; + int16_t val = patch[offset] + increment; + if (val < 0) val = 0; + if (val >= LFO_WAVEFORM_LAST) val = LFO_WAVEFORM_LAST - 1; + patch[offset] = val; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + } + } + // ctrl 1-2 and selector >= 3: ignore (no LFO for extra envs). + edit_mode_ = EDIT_STARTED_BY_ENCODER; + } else { + // Navigate controls. + int8_t new_control = active_control_ + increment; + if (new_control >= 0 && new_control < 8) { + // Skip LFO knobs (1-2) when selector >= 3 (curve knob 3 is valid). + if (selector_ >= 3 && new_control >= 1 && new_control <= 2) { + new_control = (increment > 0) ? 3 : 0; + } + if (new_control >= 0 && new_control < 8) { + active_control_ = new_control; + } + } + } + return 1; +} + +/* static */ +uint8_t EnvPage::OnClick() { + if (edit_mode_ == EDIT_IDLE) { + edit_mode_ = EDIT_STARTED_BY_ENCODER; + } else { + edit_mode_ = EDIT_IDLE; + } + return 1; +} + +/* static */ +uint8_t EnvPage::OnPot(uint8_t index, uint8_t value) { + uint8_t* patch = GetPatchData(); + + if (index == 0) { + // Selector pot: scale 0-127 to 0-6. + uint8_t val = (static_cast(value) * 7) >> 7; + if (val > 6) val = 6; + selector_ = val; + active_control_ = 0; + edit_mode_ = EDIT_STARTED_BY_POT; + return 1; + } + + if (index >= 4) { + // ADSR knob. + uint8_t offset = AdsrOffset(index - 4); + patch[offset] = value; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + active_control_ = index; + edit_mode_ = EDIT_STARTED_BY_POT; + return 1; + } + + // Knob 3 (curve) works for all envelopes. + if (index == 3) { + uint8_t base = selector_ < 3 + ? (24 + selector_ * 8) + : (112 + (selector_ - 3) * 8); + uint8_t offset = base + 6; + uint8_t val = (static_cast(value) * ENVELOPE_CURVE_LAST) >> 7; + if (val >= ENVELOPE_CURVE_LAST) val = ENVELOPE_CURVE_LAST - 1; + patch[offset] = val; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + active_control_ = index; + edit_mode_ = EDIT_STARTED_BY_POT; + return 1; + } + + // Knobs 1-2: LFO rate, shape — only for selector 0-2. + if (selector_ >= 3) return 1; + + uint8_t base = 24 + selector_ * 8; + if (index == 1) { + // Rate. + uint8_t offset = base + 5; + patch[offset] = value; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + } else if (index == 2) { + // Shape. + uint8_t offset = base + 4; + uint8_t val = (static_cast(value) * LFO_WAVEFORM_LAST) >> 7; + if (val >= LFO_WAVEFORM_LAST) val = LFO_WAVEFORM_LAST - 1; + patch[offset] = val; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + } + + active_control_ = index; + edit_mode_ = EDIT_STARTED_BY_POT; + return 1; +} + +/* static */ +uint8_t EnvPage::OnKey(uint8_t key) { + // S1 toggles to the VOICE_LFO page (same as original behavior). + return 0; +} + +/* static */ +void EnvPage::UpdateScreen() { + uint8_t* patch = GetPatchData(); + + for (uint8_t i = 0; i < 8; ++i) { + uint8_t line = i < 4 ? 0 : 1; + uint8_t row = (i & 3) * 10; + char* buffer = display.line_buffer(line) + row; + + if (row != 0) buffer[0] = kDelimiter; + if ((row + 10) != kLcdWidth) buffer[10] = kDelimiter; + + if (i == 0) { + // Selector cell: "lfeg N" + memcpy_P(&buffer[1], env_top_label_sel, 4); + if (i == active_control_ && buffer[1] >= 'a' && buffer[1] <= 'z') { + buffer[1] -= 0x20; + } + buffer[5] = ' '; + buffer[6] = ' '; + buffer[7] = ' '; + buffer[8] = ' '; + buffer[9] = '1' + selector_; + } else if (i >= 4) { + // ADSR cell. + memcpy_P(&buffer[1], env_bot_labels + (i - 4) * 4, 4); + if (i == active_control_ && buffer[1] >= 'a' && buffer[1] <= 'z') { + buffer[1] -= 0x20; + } + buffer[5] = ' '; + uint8_t offset = AdsrOffset(i - 4); + UnsafeItoa(patch[offset], 4, &buffer[6]); + AlignRight(&buffer[6], 4); + } else if (selector_ < 3) { + // LFO controls (knobs 1-3) for env 1-3. + memcpy_P(&buffer[1], env_top_labels_lfo + i * 4, 4); + if (i == active_control_ && buffer[1] >= 'a' && buffer[1] <= 'z') { + buffer[1] -= 0x20; + } + buffer[5] = ' '; + + uint8_t base = 24 + selector_ * 8; + if (i == 1) { + // Rate: numeric. + UnsafeItoa(patch[base + 5], 4, &buffer[6]); + AlignRight(&buffer[6], 4); + } else if (i == 2) { + // Shape: name or number. + uint8_t shape = patch[base + 4]; + if (shape < 4) { + memcpy_P(&buffer[6], lfo_shape_names + shape * 4, 4); + } else { + // Wavetable shape: show as number. + UnsafeItoa(shape, 4, &buffer[6]); + AlignRight(&buffer[6], 4); + } + } else { + // Curve: name. + uint8_t curve = patch[base + 6]; + if (curve < ENVELOPE_CURVE_LAST) { + memcpy_P(&buffer[6], env_curve_names + curve * 4, 4); + } else { + buffer[6] = ' '; + buffer[7] = ' '; + buffer[8] = ' '; + buffer[9] = '?'; + } + } + } else { + // Selector >= 3: knobs 1-2 blank, knob 3 shows curve. + if (i == 3) { + // Curve knob for extra envelopes. + memcpy_P(&buffer[1], env_top_labels_lfo + 3 * 4, 4); // "curv" + if (i == active_control_ && buffer[1] >= 'a' && buffer[1] <= 'z') { + buffer[1] -= 0x20; + } + buffer[5] = ' '; + uint8_t base = 112 + (selector_ - 3) * 8; + uint8_t curve = patch[base + 6]; + if (curve < ENVELOPE_CURVE_LAST) { + memcpy_P(&buffer[6], env_curve_names + curve * 4, 4); + } else { + buffer[6] = ' '; + buffer[7] = ' '; + buffer[8] = ' '; + buffer[9] = '?'; + } + } else { + for (uint8_t j = 1; j < 10; ++j) buffer[j] = ' '; + } + } + } +} + +/* static */ +void EnvPage::UpdateLeds() { + leds.set_pixel(LED_1, 0xf0); + leds.set_pixel(LED_2, 0x0f); +} + +} // namespace ambika diff --git a/controller/ui_pages/sequence_editor.h b/controller/ui_pages/env_page.h old mode 100755 new mode 100644 similarity index 61% rename from controller/ui_pages/sequence_editor.h rename to controller/ui_pages/env_page.h index ff7d677..3cff515 --- a/controller/ui_pages/sequence_editor.h +++ b/controller/ui_pages/env_page.h @@ -1,6 +1,4 @@ -// Copyright 2011 Olivier Gillet. -// -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Copyright 2026 Ambika contributors. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -15,44 +13,39 @@ // // ----------------------------------------------------------------------------- // -// Sequence editor UI page. +// Custom UI page for 7-envelope selector (env1-3 with LFO, env4-7 ADSR only). -#ifndef CONTROLLER_UI_PAGES_SEQUENCE_EDITOR_H_ -#define CONTROLLER_UI_PAGES_SEQUENCE_EDITOR_H_ +#ifndef CONTROLLER_UI_PAGES_ENV_PAGE_H_ +#define CONTROLLER_UI_PAGES_ENV_PAGE_H_ #include "controller/ui_pages/ui_page.h" namespace ambika { -struct PartData; - -class SequenceEditor : public UiPage { +class EnvPage : public UiPage { public: - SequenceEditor() { } - - static void OnInit(PageInfo* info); - static void SetActiveControl(ActiveControl active_control); + EnvPage() { } + static void OnInit(PageInfo* info); static uint8_t OnIncrement(int8_t increment); - static uint8_t OnPot(uint8_t index, uint8_t value); static uint8_t OnClick(); - static uint8_t OnNote(uint8_t note, uint8_t velocity); - + static uint8_t OnPot(uint8_t index, uint8_t value); + static uint8_t OnKey(uint8_t key); + static void UpdateScreen(); - + static void UpdateLeds(); + static const prog_EventHandlers event_handlers_; - + private: - static uint8_t actual_step(uint8_t index, uint8_t sequence); - - static PartData* mutable_part_data(); - static const PartData& part_data(); - - static uint8_t step_; - - DISALLOW_COPY_AND_ASSIGN(SequenceEditor); + static uint8_t selector_; // 0-6: which envelope + + // Compute the patch byte offset for ADSR knob (0-3) given current selector. + static uint8_t AdsrOffset(uint8_t knob); + + DISALLOW_COPY_AND_ASSIGN(EnvPage); }; } // namespace ambika -#endif // CONTROLLER_UI_PAGES_SEQUENCE_EDITOR_H_ +#endif // CONTROLLER_UI_PAGES_ENV_PAGE_H_ diff --git a/controller/ui_pages/fm_page.cc b/controller/ui_pages/fm_page.cc new file mode 100644 index 0000000..213feee --- /dev/null +++ b/controller/ui_pages/fm_page.cc @@ -0,0 +1,370 @@ +// Copyright 2026 Ambika contributors. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ----------------------------------------------------------------------------- +// +// Custom UI page for 4-op FM synthesis parameters. + +#include "controller/ui_pages/fm_page.h" + +#include +#include "avrlib/string.h" + +#include "controller/display.h" +#include "controller/leds.h" +#include "controller/multi.h" +#include "controller/voicecard_tx.h" +#include "controller/ui.h" +#include "common/patch.h" + +namespace ambika { + +// FM constants (must match voicecard/fm4op.h). +static const uint8_t kFmWaveLast = 8; +static const uint8_t kFmAlgLast = 8; + +using namespace avrlib; + +/* static */ +uint8_t FmPage::page_index_; + +// TX81Z waveform short names. +static const prog_char fm_wave_names[] PROGMEM = + "sin halfabs qtr habstri pls saw "; + +// Algorithm short descriptions. +static const prog_char fm_alg_names[] PROGMEM = + "4>3>2>1 3+4>2>1 4>3+2>1 43 + 21" + "4>2+4311 4>1234>1+2+31+2+3+4"; + +// FM page 1: algorithm, feedback, ops 1-2 +// FM page 2: ops 3-4, levels 1-4 +// +// Patch byte mapping (from fm4op.h): +// osc[0].parameter (offset 1) = algorithm +// osc[0].range (offset 2) = op1 coarse ratio +// osc[0].detune (offset 3) = op1 fine +// osc[1].shape (offset 4) = op1 wave (lo) | op2 wave (hi) +// osc[1].parameter (offset 5) = op3 wave (lo) | op4 wave (hi) +// osc[1].range (offset 6) = op2 coarse ratio +// osc[1].detune (offset 7) = op2 fine +// mix_balance (offset 8) = op3 coarse ratio +// mix_op (offset 9) = op3 fine +// mix_parameter (offset 10) = op4 coarse ratio +// mix_sub_osc_shape (offset 11)= op4 fine +// mix_sub_osc (offset 12) = op1 level +// mix_noise (offset 13) = op2 level +// mix_fuzz (offset 14) = op3 level +// mix_crush (offset 15) = op4 level +// padding[0] (offset 104) = feedback + +// Page 1 knob layout: +// algo fbk wav1 wav2 +// rat1 fin1 rat2 fin2 +// Patch offsets for page 1 knobs: +static const uint8_t fm_page1_offsets[] PROGMEM = { + 1, // algo (osc[0].parameter) + 104, // fbk (padding[0]) + 4, // wav1 (osc[1].shape, low nibble) + 4, // wav2 (osc[1].shape, high nibble) + 2, // rat1 (osc[0].range) + 3, // fin1 (osc[0].detune) + 6, // rat2 (osc[1].range) + 7, // fin2 (osc[1].detune) +}; + +// Page 2 knob layout: +// wav3 wav4 lvl1 lvl2 +// rat3 fin3 lvl3 lvl4 +static const uint8_t fm_page2_offsets[] PROGMEM = { + 5, // wav3 (osc[1].parameter, low nibble) + 5, // wav4 (osc[1].parameter, high nibble) + 12, // lvl1 (mix_sub_osc) + 13, // lvl2 (mix_noise) + 8, // rat3 (mix_balance) + 9, // fin3 (mix_op) + 14, // lvl3 (mix_fuzz) + 15, // lvl4 (mix_crush) +}; + +enum FmParamType { + FM_PT_ALGO, + FM_PT_FEEDBACK, + FM_PT_WAVE_LO, + FM_PT_WAVE_HI, + FM_PT_RATIO, + FM_PT_FINE, + FM_PT_LEVEL, +}; + +// Parameter type for each knob on page 1. +static const uint8_t fm_page1_types[] PROGMEM = { + FM_PT_ALGO, FM_PT_FEEDBACK, FM_PT_WAVE_LO, FM_PT_WAVE_HI, + FM_PT_RATIO, FM_PT_FINE, FM_PT_RATIO, FM_PT_FINE, +}; + +// Parameter type for each knob on page 2. +static const uint8_t fm_page2_types[] PROGMEM = { + FM_PT_WAVE_LO, FM_PT_WAVE_HI, FM_PT_LEVEL, FM_PT_LEVEL, + FM_PT_RATIO, FM_PT_FINE, FM_PT_LEVEL, FM_PT_LEVEL, +}; + +// Labels for page 1 knobs (4 chars each). +static const prog_char fm_page1_labels[] PROGMEM = + "algo" "fbk " "wav1" "wav2" + "rat1" "fin1" "rat2" "fin2"; + +// Labels for page 2 knobs (4 chars each). +static const prog_char fm_page2_labels[] PROGMEM = + "wav3" "wav4" "lvl1" "lvl2" + "rat3" "fin3" "lvl3" "lvl4"; + +/* static */ +const prog_EventHandlers FmPage::event_handlers_ PROGMEM = { + OnInit, + NULL, + OnIncrement, + OnClick, + OnPot, + OnKey, + NULL, + OnIdle, + UpdateScreen, + UpdateLeds, + NULL, +}; + +/* static */ +uint8_t FmPage::IsFm4OpActive() { + Part* part = multi.mutable_part(ui.state().active_part); + uint8_t* patch = part->mutable_raw_patch_data(); + return patch[0] == WAVEFORM_FM4OP; +} + +/* static */ +void FmPage::OnInit(PageInfo* info) { + UiPage::OnInit(info); + active_control_ = 0; + page_index_ = 0; +} + +static inline uint8_t* GetPatchData() { + return multi.mutable_part(ui.state().active_part)->mutable_raw_patch_data(); +} + +/* static */ +uint8_t FmPage::OnIncrement(int8_t increment) { + if (edit_mode_ != EDIT_IDLE) { + uint8_t* patch = GetPatchData(); + const uint8_t* offsets = page_index_ ? fm_page2_offsets : fm_page1_offsets; + const uint8_t* types = page_index_ ? fm_page2_types : fm_page1_types; + uint8_t offset = pgm_read_byte(&offsets[active_control_]); + uint8_t type = pgm_read_byte(&types[active_control_]); + + if (type == FM_PT_WAVE_LO) { + uint8_t val = patch[offset] & 0x0F; + val = static_cast(val + increment); + if (val >= kFmWaveLast) val = (increment > 0) ? kFmWaveLast - 1 : 0; + patch[offset] = (patch[offset] & 0xF0) | val; + } else if (type == FM_PT_WAVE_HI) { + uint8_t val = (patch[offset] >> 4) & 0x0F; + val = static_cast(val + increment); + if (val >= kFmWaveLast) val = (increment > 0) ? kFmWaveLast - 1 : 0; + patch[offset] = (patch[offset] & 0x0F) | (val << 4); + } else if (type == FM_PT_ALGO) { + int16_t val = patch[offset] + increment; + if (val < 0) val = 0; + if (val >= kFmAlgLast) val = kFmAlgLast - 1; + patch[offset] = val; + } else if (type == FM_PT_FINE) { + int16_t val = static_cast(patch[offset]) + increment; + if (val < -64) val = -64; + if (val > 63) val = 63; + patch[offset] = static_cast(val); + } else { + // RATIO, FEEDBACK, LEVEL: 0-127 + int16_t val = patch[offset] + increment; + if (val < 0) val = 0; + if (val > 127) val = 127; + patch[offset] = val; + } + + // Push change to voicecard. + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + edit_mode_ = EDIT_STARTED_BY_ENCODER; + } else { + int8_t new_control = active_control_ + increment; + if (new_control < 0) { + if (page_index_ > 0) { + page_index_--; + active_control_ = 7; + } + } else if (new_control >= 8) { + if (page_index_ < 1) { + page_index_++; + active_control_ = 0; + } + } else { + active_control_ = new_control; + } + } + return 1; +} + +/* static */ +uint8_t FmPage::OnClick() { + if (edit_mode_ == EDIT_IDLE) { + edit_mode_ = EDIT_STARTED_BY_ENCODER; + } else { + edit_mode_ = EDIT_IDLE; + } + return 1; +} + +/* static */ +uint8_t FmPage::OnPot(uint8_t index, uint8_t value) { + uint8_t* patch = GetPatchData(); + const uint8_t* offsets = page_index_ ? fm_page2_offsets : fm_page1_offsets; + const uint8_t* types = page_index_ ? fm_page2_types : fm_page1_types; + uint8_t offset = pgm_read_byte(&offsets[index]); + uint8_t type = pgm_read_byte(&types[index]); + + // Scale 7-bit pot value to parameter range. + if (type == FM_PT_WAVE_LO) { + uint8_t val = (value * kFmWaveLast) >> 7; + if (val >= kFmWaveLast) val = kFmWaveLast - 1; + patch[offset] = (patch[offset] & 0xF0) | val; + } else if (type == FM_PT_WAVE_HI) { + uint8_t val = (value * kFmWaveLast) >> 7; + if (val >= kFmWaveLast) val = kFmWaveLast - 1; + patch[offset] = (patch[offset] & 0x0F) | (val << 4); + } else if (type == FM_PT_ALGO) { + uint8_t val = (value * kFmAlgLast) >> 7; + if (val >= kFmAlgLast) val = kFmAlgLast - 1; + patch[offset] = val; + } else if (type == FM_PT_FINE) { + patch[offset] = static_cast(static_cast(value) - 64); + } else { + patch[offset] = value; + } + + active_control_ = index; + edit_mode_ = EDIT_STARTED_BY_POT; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + return 1; +} + +/* static */ +uint8_t FmPage::OnKey(uint8_t key) { + if (key == SWITCH_1) { + page_index_ = page_index_ ? 0 : 1; + return 1; + } + return 0; +} + +/* static */ +void FmPage::UpdateScreen() { + const prog_char* labels = page_index_ ? fm_page2_labels : fm_page1_labels; + const uint8_t* offsets = page_index_ ? fm_page2_offsets : fm_page1_offsets; + const uint8_t* types = page_index_ ? fm_page2_types : fm_page1_types; + uint8_t* patch = GetPatchData(); + + for (uint8_t i = 0; i < 8; ++i) { + uint8_t line = i < 4 ? 0 : 1; + uint8_t row = (i & 3) * 10; + char* buffer = display.line_buffer(line) + row; + + if (row != 0) buffer[0] = kDelimiter; + if ((row + 10) != kLcdWidth) buffer[10] = kDelimiter; + + // Label (4 chars). + memcpy_P(&buffer[1], labels + i * 4, 4); + + // Uppercase first char if active. + if (i == active_control_ && buffer[1] >= 'a' && buffer[1] <= 'z') { + buffer[1] -= 0x20; + } + + buffer[5] = ' '; + + // Value (4 chars at buffer[6..9]). + uint8_t offset = pgm_read_byte(&offsets[i]); + uint8_t type = pgm_read_byte(&types[i]); + uint8_t raw = patch[offset]; + + switch (type) { + case FM_PT_ALGO: + buffer[6] = ' '; + buffer[7] = ' '; + buffer[8] = ' '; + buffer[9] = '1' + raw; + break; + + case FM_PT_WAVE_LO: { + uint8_t w = raw & 0x0F; + if (w >= kFmWaveLast) w = 0; + memcpy_P(&buffer[6], fm_wave_names + w * 4, 4); + break; + } + + case FM_PT_WAVE_HI: { + uint8_t w = (raw >> 4) & 0x0F; + if (w >= kFmWaveLast) w = 0; + memcpy_P(&buffer[6], fm_wave_names + w * 4, 4); + break; + } + + case FM_PT_RATIO: { + // TX81Z ratio display. Index 0-63 maps to ratio table. + // Show as X.XX with one decimal for compact display. + static const prog_char ratio_display[] PROGMEM = + ".50 " ".71 " ".78 " ".87 " "1.0 " "1.41" "1.57" "1.73" + "2.0 " "2.82" "3.0 " "3.14" "3.46" "4.0 " "4.24" "4.71" + "5.0 " "5.19" "5.65" "6.0 " "6.28" "6.92" "7.0 " "7.07" + "7.85" "8.0 " "8.48" "8.65" "9.0 " "9.42" "9.89" "10.0" + "10.4" "11.0" "11.0" "11.3" "12.0" "12.1" "12.6" "12.7" + "13.0" "13.8" "14.0" "14.1" "14.1" "15.0" "15.6" "15.6" + "15.7" "17.0" "17.3" "17.3" "18.4" "18.8" "19.0" "19.8" + "20.4" "20.8" "21.2" "22.0" "22.5" "23.6" "24.2" "26.0"; + uint8_t idx = raw & 0x3F; + memcpy_P(&buffer[6], ratio_display + idx * 4, 4); + break; + } + + case FM_PT_FINE: { + int8_t v = static_cast(raw); + UnsafeItoa(v, 4, &buffer[6]); + AlignRight(&buffer[6], 4); + break; + } + + case FM_PT_FEEDBACK: + case FM_PT_LEVEL: + default: + UnsafeItoa(raw, 4, &buffer[6]); + AlignRight(&buffer[6], 4); + break; + } + } +} + +/* static */ +void FmPage::UpdateLeds() { + leds.set_pixel(LED_1, page_index_ == 0 ? 0xf0 : 0x0f); + leds.set_pixel(LED_2, page_index_ == 1 ? 0xf0 : 0x0f); +} + +} // namespace ambika diff --git a/controller/ui_pages/fm_page.h b/controller/ui_pages/fm_page.h new file mode 100644 index 0000000..0b0bea7 --- /dev/null +++ b/controller/ui_pages/fm_page.h @@ -0,0 +1,60 @@ +// Copyright 2026 Ambika contributors. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ----------------------------------------------------------------------------- +// +// Custom UI page for 4-op FM synthesis parameters. + +#ifndef CONTROLLER_UI_PAGES_FM_PAGE_H_ +#define CONTROLLER_UI_PAGES_FM_PAGE_H_ + +#include "controller/ui_pages/ui_page.h" + +namespace ambika { + +// FM parameter definitions for each knob position. +// Each entry: patch byte offset, min, max. +struct FmParam { + uint8_t offset; + uint8_t min_value; + uint8_t max_value; +}; + +class FmPage : public UiPage { + public: + FmPage() { } + + static void OnInit(PageInfo* info); + static uint8_t OnIncrement(int8_t increment); + static uint8_t OnClick(); + static uint8_t OnPot(uint8_t index, uint8_t value); + static uint8_t OnKey(uint8_t key); + + static void UpdateScreen(); + static void UpdateLeds(); + + static const prog_EventHandlers event_handlers_; + + // Check if the active part's osc1 is set to FM4OP. + static uint8_t IsFm4OpActive(); + + private: + static void PrintFmValue(char* buffer, uint8_t value, uint8_t param_type); + static uint8_t page_index_; // 0 = ops 1-2, 1 = ops 3-4 + + DISALLOW_COPY_AND_ASSIGN(FmPage); +}; + +} // namespace ambika + +#endif // CONTROLLER_UI_PAGES_FM_PAGE_H_ diff --git a/controller/ui_pages/knob_assigner.cc b/controller/ui_pages/knob_assigner.cc index 3a59ce6..0208aa4 100755 --- a/controller/ui_pages/knob_assigner.cc +++ b/controller/ui_pages/knob_assigner.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/ui_pages/knob_assigner.h b/controller/ui_pages/knob_assigner.h index 64b9cab..49dc1ec 100755 --- a/controller/ui_pages/knob_assigner.h +++ b/controller/ui_pages/knob_assigner.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/ui_pages/ks_page.cc b/controller/ui_pages/ks_page.cc new file mode 100644 index 0000000..8ce75f1 --- /dev/null +++ b/controller/ui_pages/ks_page.cc @@ -0,0 +1,248 @@ +// Copyright 2026 Ambika contributors. +// +// Custom UI page for Karplus-Strong plucked string synthesis. + +#include "controller/ui_pages/ks_page.h" + +#include +#include "avrlib/string.h" + +#include "controller/display.h" +#include "controller/leds.h" +#include "controller/multi.h" +#include "controller/voicecard_tx.h" +#include "controller/ui.h" +#include "common/patch.h" + +namespace ambika { + +using namespace avrlib; + +static const uint8_t kKsExcLast = 4; + +// Page 1: damp colr dcay body | rang tune pos exc +// Page 2: rate dpth sprd wmix | stif feed +// +// Patch offsets: +// osc[0].parameter (1) = damping +// osc[1].parameter (5) = excitation color +// osc[1].range (6) = decay rate +// mix_balance (8) = body resonance +// osc[0].range (2) = pitch range +// osc[0].detune (3) = fine tune +// osc[1].detune (7) = pluck position +// osc[1].shape (4) = excitation type +// mix_op (9) = ensemble rate +// mix_parameter (10) = ensemble depth +// mix_sub_osc_shape(11) = ensemble spread +// mix_sub_osc (12) = ensemble mix +// mix_noise (13) = stiffness +// mix_fuzz (14) = feedback + +static const uint8_t ks_page1_offsets[] PROGMEM = { + 1, 5, 6, 8, 2, 3, 7, 4, +}; +static const uint8_t ks_page2_offsets[] PROGMEM = { + 9, 10, 11, 12, 13, 14, 0xff, 0xff, +}; + +enum KsParamType { + KS_PT_UINT7, + KS_PT_INT8, + KS_PT_EXCITATION, + KS_PT_NONE, +}; + +static const uint8_t ks_page1_types[] PROGMEM = { + KS_PT_UINT7, KS_PT_UINT7, KS_PT_UINT7, KS_PT_UINT7, + KS_PT_INT8, KS_PT_INT8, KS_PT_UINT7, KS_PT_EXCITATION, +}; +static const uint8_t ks_page2_types[] PROGMEM = { + KS_PT_UINT7, KS_PT_UINT7, KS_PT_UINT7, KS_PT_UINT7, + KS_PT_UINT7, KS_PT_UINT7, KS_PT_NONE, KS_PT_NONE, +}; + +static const prog_char ks_page1_labels[] PROGMEM = + "damp" "colr" "dcay" "body" + "rang" "tune" "pos " "exc "; +static const prog_char ks_page2_labels[] PROGMEM = + "rate" "dpth" "sprd" "wmix" + "stif" "feed" " " " "; + +static const prog_char ks_exc_names[] PROGMEM = + "noisclicbritdark"; + +/* static */ +uint8_t KsPage::page_index_; + +/* static */ +const prog_EventHandlers KsPage::event_handlers_ PROGMEM = { + OnInit, NULL, OnIncrement, OnClick, OnPot, OnKey, + NULL, OnIdle, UpdateScreen, UpdateLeds, NULL, +}; + +/* static */ +uint8_t KsPage::IsKsActive() { + return multi.mutable_part(ui.state().active_part)->mutable_raw_patch_data()[0] + == WAVEFORM_KS_PLUCK; +} + +static inline uint8_t* GetPatchData() { + return multi.mutable_part(ui.state().active_part)->mutable_raw_patch_data(); +} + +/* static */ +void KsPage::OnInit(PageInfo* info) { + UiPage::OnInit(info); + active_control_ = 0; + page_index_ = 0; +} + +/* static */ +uint8_t KsPage::OnIncrement(int8_t increment) { + if (edit_mode_ != EDIT_IDLE) { + const uint8_t* offsets = page_index_ ? ks_page2_offsets : ks_page1_offsets; + const uint8_t* types = page_index_ ? ks_page2_types : ks_page1_types; + uint8_t offset = pgm_read_byte(&offsets[active_control_]); + uint8_t type = pgm_read_byte(&types[active_control_]); + if (offset == 0xff || type == KS_PT_NONE) return 1; + + uint8_t* patch = GetPatchData(); + if (type == KS_PT_EXCITATION) { + int16_t val = patch[offset] + increment; + if (val < 0) val = 0; + if (val >= kKsExcLast) val = kKsExcLast - 1; + patch[offset] = val; + } else if (type == KS_PT_INT8) { + int16_t val = static_cast(patch[offset]) + increment; + if (val < -24) val = -24; + if (val > 24) val = 24; + patch[offset] = static_cast(val); + } else { + int16_t val = patch[offset] + increment; + if (val < 0) val = 0; + if (val > 127) val = 127; + patch[offset] = val; + } + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + edit_mode_ = EDIT_STARTED_BY_ENCODER; + } else { + int8_t new_control = active_control_ + increment; + if (new_control < 0) { + if (page_index_ > 0) { page_index_--; active_control_ = 7; } + } else if (new_control >= 8) { + if (page_index_ < 1) { page_index_++; active_control_ = 0; } + } else { + // Skip empty slots. + const uint8_t* types = page_index_ ? ks_page2_types : ks_page1_types; + if (pgm_read_byte(&types[new_control]) != KS_PT_NONE) { + active_control_ = new_control; + } + } + } + return 1; +} + +/* static */ +uint8_t KsPage::OnClick() { + if (edit_mode_ == EDIT_IDLE) { + edit_mode_ = EDIT_STARTED_BY_ENCODER; + } else { + edit_mode_ = EDIT_IDLE; + } + return 1; +} + +/* static */ +uint8_t KsPage::OnPot(uint8_t index, uint8_t value) { + const uint8_t* offsets = page_index_ ? ks_page2_offsets : ks_page1_offsets; + const uint8_t* types = page_index_ ? ks_page2_types : ks_page1_types; + uint8_t offset = pgm_read_byte(&offsets[index]); + uint8_t type = pgm_read_byte(&types[index]); + if (offset == 0xff || type == KS_PT_NONE) return 1; + + uint8_t* patch = GetPatchData(); + if (type == KS_PT_EXCITATION) { + uint8_t val = (value * kKsExcLast) >> 7; + if (val >= kKsExcLast) val = kKsExcLast - 1; + patch[offset] = val; + } else if (type == KS_PT_INT8) { + patch[offset] = static_cast(static_cast(value) - 64); + } else { + patch[offset] = value; + } + + active_control_ = index; + edit_mode_ = EDIT_STARTED_BY_POT; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + return 1; +} + +/* static */ +uint8_t KsPage::OnKey(uint8_t key) { + if (key == SWITCH_1) { + page_index_ = page_index_ ? 0 : 1; + return 1; + } + return 0; +} + +/* static */ +void KsPage::UpdateScreen() { + const prog_char* labels = page_index_ ? ks_page2_labels : ks_page1_labels; + const uint8_t* offsets = page_index_ ? ks_page2_offsets : ks_page1_offsets; + const uint8_t* types = page_index_ ? ks_page2_types : ks_page1_types; + uint8_t* patch = GetPatchData(); + + for (uint8_t i = 0; i < 8; ++i) { + uint8_t line = i < 4 ? 0 : 1; + uint8_t row = (i & 3) * 10; + char* buffer = display.line_buffer(line) + row; + + if (row != 0) buffer[0] = kDelimiter; + if ((row + 10) != kLcdWidth) buffer[10] = kDelimiter; + + uint8_t type = pgm_read_byte(&types[i]); + if (type == KS_PT_NONE) { + for (uint8_t j = 1; j < 10; ++j) buffer[j] = ' '; + continue; + } + + memcpy_P(&buffer[1], labels + i * 4, 4); + if (i == active_control_ && buffer[1] >= 'a' && buffer[1] <= 'z') { + buffer[1] -= 0x20; + } + buffer[5] = ' '; + + uint8_t offset = pgm_read_byte(&offsets[i]); + uint8_t raw = patch[offset]; + + switch (type) { + case KS_PT_EXCITATION: { + uint8_t idx = raw; + if (idx >= kKsExcLast) idx = 0; + memcpy_P(&buffer[6], ks_exc_names + idx * 4, 4); + break; + } + case KS_PT_INT8: { + UnsafeItoa(static_cast(raw), 4, &buffer[6]); + AlignRight(&buffer[6], 4); + break; + } + default: + UnsafeItoa(raw, 4, &buffer[6]); + AlignRight(&buffer[6], 4); + break; + } + } +} + +/* static */ +void KsPage::UpdateLeds() { + leds.set_pixel(LED_1, page_index_ == 0 ? 0xf0 : 0x0f); + leds.set_pixel(LED_2, page_index_ == 1 ? 0xf0 : 0x0f); +} + +} // namespace ambika diff --git a/controller/ui_pages/ks_page.h b/controller/ui_pages/ks_page.h new file mode 100644 index 0000000..d8729f7 --- /dev/null +++ b/controller/ui_pages/ks_page.h @@ -0,0 +1,41 @@ +// Copyright 2026 Ambika contributors. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Custom UI page for Karplus-Strong plucked string synthesis. + +#ifndef CONTROLLER_UI_PAGES_KS_PAGE_H_ +#define CONTROLLER_UI_PAGES_KS_PAGE_H_ + +#include "controller/ui_pages/ui_page.h" + +namespace ambika { + +class KsPage : public UiPage { + public: + KsPage() { } + + static void OnInit(PageInfo* info); + static uint8_t OnIncrement(int8_t increment); + static uint8_t OnClick(); + static uint8_t OnPot(uint8_t index, uint8_t value); + static uint8_t OnKey(uint8_t key); + + static void UpdateScreen(); + static void UpdateLeds(); + + static const prog_EventHandlers event_handlers_; + + static uint8_t IsKsActive(); + + private: + static uint8_t page_index_; + DISALLOW_COPY_AND_ASSIGN(KsPage); +}; + +} // namespace ambika + +#endif // CONTROLLER_UI_PAGES_KS_PAGE_H_ diff --git a/controller/ui_pages/library.cc b/controller/ui_pages/library.cc index 04f0412..03c0ed2 100755 --- a/controller/ui_pages/library.cc +++ b/controller/ui_pages/library.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -165,6 +165,10 @@ uint8_t Library::OnKeyBrowse(uint8_t key) { location_.object = STORAGE_OBJECT_PATCH; } else { location_.object = static_cast(location_.object + 1); + // Skip PART (not independently browseable). + if (location_.object == STORAGE_OBJECT_PART) { + location_.object = STORAGE_OBJECT_PROGRAM; + } } location_.bank = loaded_objects_indices_[location_.index()] >> 8; location_.slot = loaded_objects_indices_[location_.index()] & 0xff; @@ -307,6 +311,15 @@ void Library::UpdateScreen() { memcpy(&buffer[20], name_, sizeof(name_) - 1); AlignLeft(&buffer[20], sizeof(name_) - 1); + // Show engine type after patch name: VA/FM/KS/WC + if (location_.object == STORAGE_OBJECT_PATCH) { + uint8_t eng = multi.part(ui.state().active_part).raw_patch_data()[106]; + static const prog_char eng_names[] PROGMEM = "VA" "FM" "KS" "WC"; + if (eng < ENGINE_LAST) { + memcpy_P(&buffer[37], eng_names + eng * 2, 2); + } + } + // Second line: buttons if (action_ == LIBRARY_ACTION_BROWSE) { buffer = display.line_buffer(1) + 1; @@ -382,10 +395,6 @@ void Library::OnDialogClosed(uint8_t dialog_id, uint8_t return_value) { multi.mutable_part(location_.part)->InitPatch(mode); break; - case STORAGE_OBJECT_SEQUENCE: - multi.mutable_part(location_.part)->InitSequence(mode); - break; - case STORAGE_OBJECT_PROGRAM: multi.mutable_part(location_.part)->InitSettings(mode); break; diff --git a/controller/ui_pages/library.h b/controller/ui_pages/library.h index 9203bcc..ebf206e 100755 --- a/controller/ui_pages/library.h +++ b/controller/ui_pages/library.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/ui_pages/os_info_page.cc b/controller/ui_pages/os_info_page.cc index 76a7265..2cd646f 100755 --- a/controller/ui_pages/os_info_page.cc +++ b/controller/ui_pages/os_info_page.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -130,9 +130,10 @@ void OsInfoPage::PrintVersionNumber(char* buffer, uint8_t number) { /* static */ void OsInfoPage::UpdateScreen() { + display.line_buffer(0)[0] = ' '; char* buffer = display.line_buffer(0) + 1; - memcpy_P(&buffer[0], PSTR("ambika"), 6); - PrintVersionNumber(&buffer[10], kSystemVersion); + memcpy_P(&buffer[0], PSTR("carcosa"), 7); + memcpy_P(&buffer[8], PSTR("v2.06"), 5); memcpy_P(&buffer[15], PSTR("port 1 device ?"), 15); Word version_number = voicecard_tx.GetVersion(active_control_); diff --git a/controller/ui_pages/os_info_page.h b/controller/ui_pages/os_info_page.h index 1c2c14b..f5f2376 100755 --- a/controller/ui_pages/os_info_page.h +++ b/controller/ui_pages/os_info_page.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/ui_pages/parameter_editor.cc b/controller/ui_pages/parameter_editor.cc index e6dc125..cf595af 100755 --- a/controller/ui_pages/parameter_editor.cc +++ b/controller/ui_pages/parameter_editor.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/ui_pages/parameter_editor.h b/controller/ui_pages/parameter_editor.h index 8f2b10b..6837832 100755 --- a/controller/ui_pages/parameter_editor.h +++ b/controller/ui_pages/parameter_editor.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -48,7 +48,7 @@ class ParameterEditor : public UiPage { static uint8_t instance_index(uint8_t control_id); static const prog_EventHandlers event_handlers_; - + private: static SnapMask snapped_; diff --git a/controller/ui_pages/performance_page.cc b/controller/ui_pages/performance_page.cc index cfd99be..4d8dcb3 100755 --- a/controller/ui_pages/performance_page.cc +++ b/controller/ui_pages/performance_page.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/ui_pages/performance_page.h b/controller/ui_pages/performance_page.h index af6b2c0..180784e 100755 --- a/controller/ui_pages/performance_page.h +++ b/controller/ui_pages/performance_page.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/ui_pages/randomizer.cc b/controller/ui_pages/randomizer.cc new file mode 100644 index 0000000..3ae2e28 --- /dev/null +++ b/controller/ui_pages/randomizer.cc @@ -0,0 +1,257 @@ +// Copyright 2026 Ambika contributors. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ----------------------------------------------------------------------------- +// +// Smart patch randomizer implementation. + +#include "controller/ui_pages/randomizer.h" + +#include "avrlib/random.h" +#include "avrlib/string.h" + +#include "controller/display.h" +#include "controller/leds.h" +#include "controller/multi.h" +#include "controller/parameter.h" +#include "controller/ui.h" + +namespace ambika { + +using namespace avrlib; + +/* static */ +uint8_t Randomizer::section_depth_[RANDOM_SECTION_LAST]; + +/* static */ +uint8_t Randomizer::triggered_; + +static const prog_char random_labels[] PROGMEM = + "osc filt env mod mix lfo dpth trg "; + +/* static */ +const prog_EventHandlers Randomizer::event_handlers_ PROGMEM = { + OnInit, + NULL, + OnIncrement, + OnClick, + OnPot, + OnKey, + NULL, + OnIdle, + UpdateScreen, + UpdateLeds, + NULL, +}; + +/* static */ +void Randomizer::OnInit(PageInfo* info) { + triggered_ = 0; +} + +/* static */ +uint8_t Randomizer::OnIncrement(int8_t increment) { + return 1; +} + +/* static */ +uint8_t Randomizer::OnClick() { + Randomize(); + triggered_ = 30; // Flash counter for visual feedback. + return 1; +} + +/* static */ +uint8_t Randomizer::OnPot(uint8_t index, uint8_t value) { + if (index < RANDOM_SECTION_LAST) { + section_depth_[index] = value; + } + return 1; +} + +/* static */ +uint8_t Randomizer::OnKey(uint8_t key) { + if (key < 6) { + Ui::mutable_state()->active_part = key; + return 1; + } + return 0; +} + +/* static */ +void Randomizer::RandomizeRange( + uint8_t* data, uint8_t start, uint8_t end, + uint8_t min_val, uint8_t max_val, uint8_t depth) { + if (depth == 0) return; + for (uint8_t i = start; i < end; ++i) { + // Generate a random offset scaled by depth. + int16_t current = data[i]; + int16_t range = max_val - min_val; + int16_t offset = static_cast(Random::GetByte()); + offset = (offset * depth * range) >> 14; + current += offset; + if (current < min_val) current = min_val; + if (current > max_val) current = max_val; + data[i] = static_cast(current); + } +} + +/* static */ +void Randomizer::Randomize() { + Part* part = multi.mutable_part(Ui::state().active_part); + uint8_t* patch = part->mutable_raw_patch_data(); + + // Global depth multiplier (knob 7). + uint8_t global = section_depth_[RANDOM_SECTION_DEPTH]; + if (global == 0) global = 127; // Default to full if not set. + + // --- Oscillators (offset 0-7) --- + uint8_t osc_depth = (section_depth_[RANDOM_SECTION_OSC] * global) >> 7; + if (osc_depth) { + // Waveform shapes: offset 0, 4 + for (uint8_t i = 0; i < 2; ++i) { + uint8_t idx = i * 4; // osc[0].shape=0, osc[1].shape=4 + if (Random::GetByte() < osc_depth * 2) { + patch[idx] = Random::GetByte() % WAVEFORM_LAST; + } + } + // Parameters: offset 1, 5 + RandomizeRange(patch, 1, 2, 0, 127, osc_depth); + RandomizeRange(patch, 5, 6, 0, 127, osc_depth); + // Range: offset 2, 6 (signed, keep in -12..12) + RandomizeRange(patch, 2, 3, 0, 48, osc_depth); // Will be interpreted as int8 + RandomizeRange(patch, 6, 7, 0, 48, osc_depth); + // Detune: offset 3, 7 + RandomizeRange(patch, 3, 4, 0, 128, osc_depth); + RandomizeRange(patch, 7, 8, 0, 128, osc_depth); + } + + // --- Mixer (offset 8-15) --- + uint8_t mix_depth = (section_depth_[RANDOM_SECTION_MIXER] * global) >> 7; + if (mix_depth) { + RandomizeRange(patch, 8, 9, 0, 63, mix_depth); // mix balance + // Skip mix_op (9) — keep current operator + RandomizeRange(patch, 10, 11, 0, 63, mix_depth); // mix parameter + // Skip sub osc shape (11) + RandomizeRange(patch, 12, 13, 0, 63, mix_depth); // sub osc level + RandomizeRange(patch, 13, 14, 0, 63, mix_depth); // noise level + RandomizeRange(patch, 14, 15, 0, 63, mix_depth); // fuzz + RandomizeRange(patch, 15, 16, 0, 31, mix_depth); // crush + } + + // --- Filter (offset 16-23) --- + uint8_t filt_depth = (section_depth_[RANDOM_SECTION_FILTER] * global) >> 7; + if (filt_depth) { + RandomizeRange(patch, 16, 17, 0, 127, filt_depth); // cutoff + RandomizeRange(patch, 17, 18, 0, 63, filt_depth); // resonance + // Skip filter mode (18) — keep current + // Skip filter 2 (19-21) + RandomizeRange(patch, 22, 23, 0, 63, filt_depth); // filter env + RandomizeRange(patch, 23, 24, 0, 63, filt_depth); // filter lfo + } + + // --- Envelopes (offset 24-47, 3x8 bytes) --- + uint8_t env_depth = (section_depth_[RANDOM_SECTION_ENVELOPES] * global) >> 7; + if (env_depth) { + for (uint8_t env = 0; env < 3; ++env) { + uint8_t base = 24 + env * 8; + RandomizeRange(patch, base, base + 4, 0, 127, env_depth); // ADSR + } + for (uint8_t env = 0; env < 4; ++env) { + uint8_t base = 112 + env * 4; + RandomizeRange(patch, base, base + 4, 0, 127, env_depth); // Extra env ADSR + } + } + + // --- LFOs (within envelope blocks, offset 28-29, 36-37, 44-45) --- + uint8_t lfo_depth = (section_depth_[RANDOM_SECTION_LFOS] * global) >> 7; + if (lfo_depth) { + for (uint8_t lfo = 0; lfo < 3; ++lfo) { + uint8_t base = 24 + lfo * 8; + // LFO shape (offset +4) + if (Random::GetByte() < lfo_depth * 2) { + patch[base + 4] = Random::GetByte() % LFO_WAVEFORM_LAST; + } + // LFO rate (offset +5) + RandomizeRange(patch, base + 5, base + 6, 0, 127, lfo_depth); + } + } + + // --- Modulation matrix (offset 50-91, 14x3 bytes) --- + uint8_t mod_depth = (section_depth_[RANDOM_SECTION_MODULATION] * global) >> 7; + if (mod_depth) { + for (uint8_t mod = 0; mod < 14; ++mod) { + uint8_t base = 50 + mod * 3; + // Only randomize if depth is high enough — mod matrix is sensitive. + if (Random::GetByte() < mod_depth) { + // Source + patch[base] = Random::GetByte() % MOD_SRC_LAST; + } + if (Random::GetByte() < mod_depth) { + // Destination + patch[base + 1] = Random::GetByte() % MOD_DST_LAST; + } + // Amount: always randomize with depth scaling + RandomizeRange(patch, base + 2, base + 3, 0, 126, mod_depth); + } + } + + // Push the modified patch to the voicecards. + part->TouchPatch(); +} + +/* static */ +void Randomizer::UpdateScreen() { + char* buffer = display.line_buffer(0); + // Top row: section labels + for (uint8_t i = 0; i < 40; ++i) { + buffer[i] = pgm_read_byte(random_labels + i); + } + + buffer = display.line_buffer(1); + // Bottom row: depth values for each section. + for (uint8_t i = 0; i < 7; ++i) { + uint8_t pos = i * 5; + uint8_t val = section_depth_[i]; + // Display as 0-99 to fit in 3 chars. + uint8_t display_val = (val * 100) >> 7; + if (display_val > 99) display_val = 99; + buffer[pos] = ' '; + buffer[pos + 1] = display_val >= 10 ? ('0' + display_val / 10) : ' '; + buffer[pos + 2] = '0' + display_val % 10; + buffer[pos + 3] = ' '; + buffer[pos + 4] = ' '; + } + // Last position: show trigger feedback. + if (triggered_) { + buffer[35] = '*'; + buffer[36] = 'G'; + buffer[37] = 'O'; + buffer[38] = '*'; + --triggered_; + } else { + buffer[35] = 'p'; + buffer[36] = 'u'; + buffer[37] = 's'; + buffer[38] = 'h'; + } + buffer[39] = ' '; +} + +/* static */ +void Randomizer::UpdateLeds() { + leds.set_pixel(LED_7, 0x0f); + leds.set_pixel(LED_8, 0x0f); +} + +} // namespace ambika diff --git a/controller/ui_pages/randomizer.h b/controller/ui_pages/randomizer.h new file mode 100644 index 0000000..c7007e7 --- /dev/null +++ b/controller/ui_pages/randomizer.h @@ -0,0 +1,66 @@ +// Copyright 2026 Ambika contributors. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ----------------------------------------------------------------------------- +// +// Smart patch randomizer. Each knob controls randomization depth +// for a different section of the patch. Click encoder to trigger. + +#ifndef CONTROLLER_UI_PAGES_RANDOMIZER_H_ +#define CONTROLLER_UI_PAGES_RANDOMIZER_H_ + +#include "controller/ui_pages/ui_page.h" + +namespace ambika { + +// Randomization sections controlled by each knob. +enum RandomSection { + RANDOM_SECTION_OSC, // Knob 1: Oscillator shapes, params, range, detune + RANDOM_SECTION_FILTER, // Knob 2: Filter cutoff, resonance, mode, env/lfo + RANDOM_SECTION_ENVELOPES, // Knob 3: ADSR times for all 3 envelopes + RANDOM_SECTION_MODULATION, // Knob 4: Mod matrix sources, destinations, amounts + RANDOM_SECTION_MIXER, // Knob 5: Mix balance, sub, noise, fuzz, crush + RANDOM_SECTION_LFOS, // Knob 6: LFO shapes, rates + RANDOM_SECTION_DEPTH, // Knob 7: Global depth (scales all others) + RANDOM_SECTION_LAST +}; + +class Randomizer : public UiPage { + public: + Randomizer() { } + + static void OnInit(PageInfo* info); + static uint8_t OnIncrement(int8_t increment); + static uint8_t OnClick(); + static uint8_t OnPot(uint8_t index, uint8_t value); + static uint8_t OnKey(uint8_t key); + + static void UpdateScreen(); + static void UpdateLeds(); + + static const prog_EventHandlers event_handlers_; + + private: + static void Randomize(); + static void RandomizeRange(uint8_t* data, uint8_t start, uint8_t end, + uint8_t min_val, uint8_t max_val, uint8_t depth); + + static uint8_t section_depth_[RANDOM_SECTION_LAST]; + static uint8_t triggered_; + + DISALLOW_COPY_AND_ASSIGN(Randomizer); +}; + +} // namespace ambika + +#endif // CONTROLLER_UI_PAGES_RANDOMIZER_H_ diff --git a/controller/ui_pages/sequence_editor.cc b/controller/ui_pages/sequence_editor.cc deleted file mode 100755 index b099e63..0000000 --- a/controller/ui_pages/sequence_editor.cc +++ /dev/null @@ -1,289 +0,0 @@ -// Copyright 2011 Olivier Gillet. -// -// Author: Olivier Gillet (ol.gillet@gmail.com) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// -// ----------------------------------------------------------------------------- -// -// Sequence editor UI page. - -#include "controller/ui_pages/sequence_editor.h" - -#include "avrlib/string.h" - -#include "controller/display.h" -#include "controller/leds.h" -#include "controller/multi.h" -#include "controller/parameter.h" -#include "controller/part.h" - -namespace ambika { - -/* static */ -uint8_t SequenceEditor::step_ = 0; - - -/* static */ -const prog_EventHandlers SequenceEditor::event_handlers_ PROGMEM = { - OnInit, - SetActiveControl, - OnIncrement, - OnClick, - OnPot, - OnKey, - OnNote, - OnIdle, - UpdateScreen, - UpdateLeds, - OnDialogClosed, -}; - -/* static */ -inline PartData* SequenceEditor::mutable_part_data() { - return multi.mutable_part(ui.state().active_part)->mutable_data(); -} - -/* static */ -inline const PartData& SequenceEditor::part_data() { - return multi.part(ui.state().active_part).data(); -} - -/* static */ -void SequenceEditor::OnInit(PageInfo* info) { - UiPage::OnInit(info); - active_control_ = 4; - OnIncrement(0); -} - -/* static */ -void SequenceEditor::SetActiveControl(ActiveControl active_control) { - if (active_control == ACTIVE_CONTROL_FIRST) { - step_ = 0; - } else { - step_ = 127; - OnIncrement(-1); - } -} - -/* static */ -uint8_t SequenceEditor::OnNote(uint8_t note, uint8_t velocity) { - if (edit_mode_ != EDIT_IDLE) { - int8_t step = step_ - 1; - if (active_control_ >= 4) { - ++step; - } - if (step < 0) { - return 0; - } - step = actual_step(step, 2); - mutable_part_data()->set_note(step, note); - mutable_part_data()->set_velocity(step, velocity); - step_ = actual_step(step + 1, 2); - return 1; - } else { - return 0; - } -} - - -/* static */ -uint8_t SequenceEditor::OnIncrement(int8_t increment) { - if (edit_mode_ == EDIT_IDLE) { - int8_t step = step_; - step += increment; - if (step < 0) { - ui.ShowPageRelative(-1); - return 1; - } - uint8_t max_num_steps = 0x00; - for (uint8_t i = 0; i < 3; ++i) { - uint8_t l = part_data().sequence_length[i]; - if (l > max_num_steps) { - max_num_steps = l; - } - } - if (step >= max_num_steps) { - if (increment > 0) { - ui.ShowPageRelative(1); - return 1; - } else { - step = max_num_steps - 1; - } - } - step_ = step; - } else { - edit_mode_ = EDIT_STARTED_BY_ENCODER; - uint8_t index = active_control_; - int8_t step = step_ - 1; - if (index >= 4) { - ++step; - index -= 4; - } - if (step < 0) { - return 1; - } - switch (index) { - case 0: - { - step = actual_step(step, 2); - uint8_t note = part_data().note(step) + increment; - if (note >= 0 && note <= 127) { - mutable_part_data()->set_note(step, note); - } - } - break; - case 1: - { - step = actual_step(step, 2); - int16_t velocity = part_data().ordered_gate_velocity(step); - velocity += increment; - if (velocity >= 0 && velocity <= 0x100) { - mutable_part_data()->set_ordered_gate_velocity(step, velocity); - } - } - break; - case 2: - case 3: - { - step = actual_step(step, index - 2); - int16_t value = part_data().step_value(index - 2, step); - value += increment; - if (value >= 0 && value <= 255) { - mutable_part_data()->set_step_value(index - 2, step, value); - } - } - break; - } - } - return 1; -} - -/* static */ -uint8_t SequenceEditor::OnClick() { - if (edit_mode_ != EDIT_IDLE) { - edit_mode_ = EDIT_IDLE; - active_control_ = 4; - } else { - edit_mode_ = EDIT_STARTED_BY_ENCODER; - } - return 1; -} - -/* static */ -uint8_t SequenceEditor::OnPot(uint8_t index, uint8_t value) { - int8_t step = step_ - 1; - active_control_ = index; - if (index >= 4) { - ++step; - index -= 4; - } - if (step < 0) { - return 1; - } - switch (index) { - case 0: - step = actual_step(step, 2); - mutable_part_data()->set_note(step, 24 + (value >> 1)); - break; - case 1: - step = actual_step(step, 2); - mutable_part_data()->set_ordered_gate_velocity(step, value << 1); - break; - case 2: - case 3: - step = actual_step(step, index - 2); - mutable_part_data()->set_step_value(index - 2, step, value << 1); - break; - } - return 1; -} - -/* static */ -uint8_t SequenceEditor::actual_step(uint8_t index, uint8_t sequence) { - uint8_t l = part_data().sequence_length[sequence]; - if (l == 0) { - return 0; - } else { - while (index >= l) { - index -= l; - } - return index; - } -} - -/* static */ -void SequenceEditor::UpdateScreen() { - for (uint8_t i = 0; i < 2; ++i) { - char* buffer = display.line_buffer(i); - int8_t step = step_ - 1 + i; - memset(buffer, ' ', kLcdWidth); - if (step >= 0) { - // Quick and dirty %02d - buffer[1] = '0'; - uint8_t c = step + 1; - while (c >= 10) { - c -= 10; - ++buffer[1]; - } - buffer[2] = '0' + c; - uint8_t step_index = actual_step(step, 2); - buffer[3] = kDelimiter; - buffer[10] = kDelimiter; - if (step_index == 0) { - buffer[4] = buffer[11] = '\xa5'; - } - NoteStep n = part_data().note_step(step_index); - Parameter::PrintNote(n.note, &buffer[5]); - buffer[8] = ' '; - if (n.gate) { - buffer[15] = n.legato ? '\x04' : '\x01'; - UnsafeItoa(n.velocity, 3, &buffer[16]); - AlignRight(&buffer[16], 3); - } else { - buffer[15] = '\x05'; - } - uint8_t column = 20; - for (uint8_t j = 0; j < 2; ++j) { - step_index = actual_step(step, j); - uint8_t step_value = part_data().step_value(j, step_index); - if (step_index == 0) { - buffer[column + 1] = '\xa5'; - } - UnsafeItoa(step_value, 3, &buffer[column + 6]); - AlignRight(&buffer[column + 6], 3); - buffer[column] = kDelimiter; - column += 10; - } - } else { - memcpy_P( - &buffer[3], - PSTR("\x07 note \x07velocity \x07stepseq 1\x07stepseq 2"), - 37); - } - } - display.set_cursor_character(edit_mode_ == EDIT_IDLE ? 0xff : '_'); - if (edit_mode_ == EDIT_IDLE) { - display.set_cursor_position(kLcdNoCursor); - } else { - uint8_t index = active_control_; - uint8_t position = 0; - if (index >= 4) { - index -= 4; - position += kLcdWidth; - } - position += index * 10 + 4 + (index >> 1); - display.set_cursor_position(position); - } -} - -} // namespace ambika diff --git a/controller/ui_pages/ui_page.cc b/controller/ui_pages/ui_page.cc index 9971afc..0c33710 100755 --- a/controller/ui_pages/ui_page.cc +++ b/controller/ui_pages/ui_page.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/ui_pages/ui_page.h b/controller/ui_pages/ui_page.h index f5df63b..89d3674 100755 --- a/controller/ui_pages/ui_page.h +++ b/controller/ui_pages/ui_page.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/ui_pages/version_manager.cc b/controller/ui_pages/version_manager.cc index bd56207..edb46d0 100755 --- a/controller/ui_pages/version_manager.cc +++ b/controller/ui_pages/version_manager.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/ui_pages/version_manager.h b/controller/ui_pages/version_manager.h index 7c55f94..ce7fd93 100755 --- a/controller/ui_pages/version_manager.h +++ b/controller/ui_pages/version_manager.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/ui_pages/voice_assigner.cc b/controller/ui_pages/voice_assigner.cc index 415734c..0273a8f 100755 --- a/controller/ui_pages/voice_assigner.cc +++ b/controller/ui_pages/voice_assigner.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/ui_pages/voice_assigner.h b/controller/ui_pages/voice_assigner.h index c61e511..75f99ce 100755 --- a/controller/ui_pages/voice_assigner.h +++ b/controller/ui_pages/voice_assigner.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/ui_pages/wc_page.cc b/controller/ui_pages/wc_page.cc new file mode 100644 index 0000000..0ffde20 --- /dev/null +++ b/controller/ui_pages/wc_page.cc @@ -0,0 +1,247 @@ +// Copyright 2026 Ambika contributors. +// +// West coast oscillator UI page. + +#include "controller/ui_pages/wc_page.h" + +#include +#include "avrlib/string.h" + +#include "controller/display.h" +#include "controller/leds.h" +#include "controller/multi.h" +#include "controller/voicecard_tx.h" +#include "controller/ui.h" +#include "common/patch.h" + +namespace ambika { + +using namespace avrlib; + +static const uint8_t kWcWaveLast = 2; + +// Page 1: wave fold sym bias | fmdp fmrt colr drve +// Page 2: flds gain env sub | sync +// +// Patch offsets: +// Page 1: +// osc[1].shape (4) = base waveform +// osc[0].parameter (1) = fold depth +// osc[1].parameter (5) = symmetry +// mix_balance (8) = bias +// osc[1].range (6) = FM depth +// osc[1].detune (7) = FM ratio +// mix_parameter (10) = color +// mix_op (9) = drive +// Page 2: +// mix_sub_osc_shape(11) = fold stages +// mix_sub_osc (12) = input gain +// mix_noise (13) = env-to-fold +// mix_fuzz (14) = sub level +// mix_crush (15) = sync amount + +static const uint8_t wc_page1_offsets[] PROGMEM = { + 4, 1, 5, 8, 6, 7, 10, 9, +}; +static const uint8_t wc_page2_offsets[] PROGMEM = { + 11, 12, 13, 14, 15, 0xff, 0xff, 0xff, +}; + +enum WcParamType { + WC_PT_WAVE, + WC_PT_UINT7, + WC_PT_INT8, + WC_PT_NONE, +}; + +static const uint8_t wc_page1_types[] PROGMEM = { + WC_PT_WAVE, WC_PT_UINT7, WC_PT_UINT7, WC_PT_UINT7, + WC_PT_UINT7, WC_PT_INT8, WC_PT_UINT7, WC_PT_UINT7, +}; +static const uint8_t wc_page2_types[] PROGMEM = { + WC_PT_UINT7, WC_PT_UINT7, WC_PT_UINT7, WC_PT_UINT7, + WC_PT_UINT7, WC_PT_NONE, WC_PT_NONE, WC_PT_NONE, +}; + +static const prog_char wc_page1_labels[] PROGMEM = + "wave" "fold" "sym " "bias" + "fmdp" "fmrt" "colr" "drve"; +static const prog_char wc_page2_labels[] PROGMEM = + "flds" "gain" "env " "sub " + "sync" " " " " " "; + +static const prog_char wc_wave_names[] PROGMEM = "sin tri "; + +/* static */ +uint8_t WcPage::page_index_; + +/* static */ +const prog_EventHandlers WcPage::event_handlers_ PROGMEM = { + OnInit, NULL, OnIncrement, OnClick, OnPot, OnKey, + NULL, OnIdle, UpdateScreen, UpdateLeds, NULL, +}; + +/* static */ +uint8_t WcPage::IsWcActive() { + return multi.mutable_part(ui.state().active_part)->mutable_raw_patch_data()[0] + == WAVEFORM_WESTCOAST; +} + +static inline uint8_t* GetPatchData() { + return multi.mutable_part(ui.state().active_part)->mutable_raw_patch_data(); +} + +/* static */ +void WcPage::OnInit(PageInfo* info) { + UiPage::OnInit(info); + active_control_ = 0; + page_index_ = 0; +} + +/* static */ +uint8_t WcPage::OnIncrement(int8_t increment) { + if (edit_mode_ != EDIT_IDLE) { + const uint8_t* offsets = page_index_ ? wc_page2_offsets : wc_page1_offsets; + const uint8_t* types = page_index_ ? wc_page2_types : wc_page1_types; + uint8_t offset = pgm_read_byte(&offsets[active_control_]); + uint8_t type = pgm_read_byte(&types[active_control_]); + if (offset == 0xff || type == WC_PT_NONE) return 1; + + uint8_t* patch = GetPatchData(); + if (type == WC_PT_WAVE) { + int16_t val = patch[offset] + increment; + if (val < 0) val = 0; + if (val >= kWcWaveLast) val = kWcWaveLast - 1; + patch[offset] = val; + } else if (type == WC_PT_INT8) { + int16_t val = static_cast(patch[offset]) + increment; + if (val < -24) val = -24; + if (val > 24) val = 24; + patch[offset] = static_cast(val); + } else { + int16_t val = patch[offset] + increment; + if (val < 0) val = 0; + if (val > 127) val = 127; + patch[offset] = val; + } + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + edit_mode_ = EDIT_STARTED_BY_ENCODER; + } else { + int8_t new_control = active_control_ + increment; + if (new_control < 0) { + if (page_index_ > 0) { page_index_--; active_control_ = 7; } + } else if (new_control >= 8) { + if (page_index_ < 1) { page_index_++; active_control_ = 0; } + } else { + const uint8_t* types = page_index_ ? wc_page2_types : wc_page1_types; + if (pgm_read_byte(&types[new_control]) != WC_PT_NONE) { + active_control_ = new_control; + } + } + } + return 1; +} + +/* static */ +uint8_t WcPage::OnClick() { + if (edit_mode_ == EDIT_IDLE) { + edit_mode_ = EDIT_STARTED_BY_ENCODER; + } else { + edit_mode_ = EDIT_IDLE; + } + return 1; +} + +/* static */ +uint8_t WcPage::OnPot(uint8_t index, uint8_t value) { + const uint8_t* offsets = page_index_ ? wc_page2_offsets : wc_page1_offsets; + const uint8_t* types = page_index_ ? wc_page2_types : wc_page1_types; + uint8_t offset = pgm_read_byte(&offsets[index]); + uint8_t type = pgm_read_byte(&types[index]); + if (offset == 0xff || type == WC_PT_NONE) return 1; + + uint8_t* patch = GetPatchData(); + if (type == WC_PT_WAVE) { + uint8_t val = (value * kWcWaveLast) >> 7; + if (val >= kWcWaveLast) val = kWcWaveLast - 1; + patch[offset] = val; + } else if (type == WC_PT_INT8) { + patch[offset] = static_cast(static_cast(value) - 64); + } else { + patch[offset] = value; + } + + active_control_ = index; + edit_mode_ = EDIT_STARTED_BY_POT; + multi.mutable_part(ui.state().active_part)->WriteToAllVoices( + VOICECARD_DATA_PATCH, offset, patch[offset]); + return 1; +} + +/* static */ +uint8_t WcPage::OnKey(uint8_t key) { + if (key == SWITCH_1) { + page_index_ = page_index_ ? 0 : 1; + return 1; + } + return 0; +} + +/* static */ +void WcPage::UpdateScreen() { + const prog_char* labels = page_index_ ? wc_page2_labels : wc_page1_labels; + const uint8_t* offsets = page_index_ ? wc_page2_offsets : wc_page1_offsets; + const uint8_t* types = page_index_ ? wc_page2_types : wc_page1_types; + uint8_t* patch = GetPatchData(); + + for (uint8_t i = 0; i < 8; ++i) { + uint8_t line = i < 4 ? 0 : 1; + uint8_t row = (i & 3) * 10; + char* buffer = display.line_buffer(line) + row; + + if (row != 0) buffer[0] = kDelimiter; + if ((row + 10) != kLcdWidth) buffer[10] = kDelimiter; + + uint8_t type = pgm_read_byte(&types[i]); + if (type == WC_PT_NONE) { + for (uint8_t j = 1; j < 10; ++j) buffer[j] = ' '; + continue; + } + + memcpy_P(&buffer[1], labels + i * 4, 4); + if (i == active_control_ && buffer[1] >= 'a' && buffer[1] <= 'z') { + buffer[1] -= 0x20; + } + buffer[5] = ' '; + + uint8_t offset = pgm_read_byte(&offsets[i]); + uint8_t raw = patch[offset]; + + switch (type) { + case WC_PT_WAVE: { + uint8_t w = raw; + if (w >= kWcWaveLast) w = 0; + memcpy_P(&buffer[6], wc_wave_names + w * 4, 4); + break; + } + case WC_PT_INT8: { + UnsafeItoa(static_cast(raw), 4, &buffer[6]); + AlignRight(&buffer[6], 4); + break; + } + default: + UnsafeItoa(raw, 4, &buffer[6]); + AlignRight(&buffer[6], 4); + break; + } + } +} + +/* static */ +void WcPage::UpdateLeds() { + leds.set_pixel(LED_1, page_index_ == 0 ? 0xf0 : 0x0f); + leds.set_pixel(LED_2, page_index_ == 1 ? 0xf0 : 0x0f); +} + +} // namespace ambika diff --git a/controller/ui_pages/wc_page.h b/controller/ui_pages/wc_page.h new file mode 100644 index 0000000..e7ca0a3 --- /dev/null +++ b/controller/ui_pages/wc_page.h @@ -0,0 +1,35 @@ +// Copyright 2026 Ambika contributors. +// +// West coast oscillator UI page. + +#ifndef CONTROLLER_UI_PAGES_WC_PAGE_H_ +#define CONTROLLER_UI_PAGES_WC_PAGE_H_ + +#include "controller/ui_pages/ui_page.h" + +namespace ambika { + +class WcPage : public UiPage { + public: + WcPage() { } + + static void OnInit(PageInfo* info); + static uint8_t OnIncrement(int8_t increment); + static uint8_t OnClick(); + static uint8_t OnPot(uint8_t index, uint8_t value); + static uint8_t OnKey(uint8_t key); + + static void UpdateScreen(); + static void UpdateLeds(); + + static const prog_EventHandlers event_handlers_; + static uint8_t IsWcActive(); + + private: + static uint8_t page_index_; + DISALLOW_COPY_AND_ASSIGN(WcPage); +}; + +} // namespace ambika + +#endif // CONTROLLER_UI_PAGES_WC_PAGE_H_ diff --git a/controller/voice_allocator.cc b/controller/voice_allocator.cc index 4eb6f33..e9d4805 100755 --- a/controller/voice_allocator.cc +++ b/controller/voice_allocator.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/voice_allocator.h b/controller/voice_allocator.h index d1d04fe..1973861 100755 --- a/controller/voice_allocator.h +++ b/controller/voice_allocator.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/voicecard_tx.cc b/controller/voicecard_tx.cc index 8e7268d..f8d0b7c 100755 --- a/controller/voicecard_tx.cc +++ b/controller/voicecard_tx.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/controller/voicecard_tx.h b/controller/voicecard_tx.h index da6f2f5..d04339e 100755 --- a/controller/voicecard_tx.h +++ b/controller/voicecard_tx.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/docs/superpowers/plans/2026-04-20-seven-envelopes.md b/docs/superpowers/plans/2026-04-20-seven-envelopes.md new file mode 100644 index 0000000..5e51df0 --- /dev/null +++ b/docs/superpowers/plans/2026-04-20-seven-envelopes.md @@ -0,0 +1,425 @@ +# Seven Envelopes Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Expand Carcosa from 3 to 7 ADSR envelopes available as modulation sources in all synthesis modes. + +**Architecture:** Grow the Patch struct from 112 to 128 bytes by appending 4 extra ADSR blocks (4 bytes each = 16 bytes). Decouple kNumLfos from kNumEnvelopes — add kNumTotalEnvelopes=7 while keeping kNumLfos=3 and kNumEnvLfoSlots=3. Remove MOD_SRC_SEQ_1 and MOD_SRC_SEQ_2 from the modulation source enum, insert MOD_SRC_ENV_4 through MOD_SRC_ENV_7 after ENV_3. Create custom EnvPage class for dynamic parameter routing based on selected envelope. Breaking change to patch format — all stored patches become invalid. + +**Tech Stack:** AVR C++ (avr-gcc), ATmega328p voicecard, ATmega644p controller + +--- + +## File Structure + +| File | Action | Responsibility | +|------|--------|---------------| +| `common/patch.h` | Modify | Add ExtraEnvelopeSettings struct, expand Patch to 128 bytes, update ModulationSource enum, add kNumTotalEnvelopes | +| `voicecard/envelope.h` | No change | Envelope class already supports all needed features | +| `voicecard/voice.h` | Modify | Change envelope array size to kNumTotalEnvelopes | +| `voicecard/voice.cc` | Modify | Render and update all 7 envelopes, extend init_patch with 16 zero bytes, replace MOD_SRC_SEQ references | +| `voicecard/voicecard_rx.h` | Modify | Extend COMMAND_RETRIGGER_ENVELOPE cases from 0-2 to 0-6 | +| `controller/parameter.h` | Modify | Bump kNumParameters to 80 | +| `controller/parameter.cc` | Modify | Add env4-7 ADSR parameters, replace mod source string display with switch, update CC map | +| `controller/part.cc` | Modify | Replace MOD_SRC_SEQ references in init_patch, extend init_patch with 16 zero bytes | +| `controller/ui.h` | Modify | Add PAGE_ENV_EXTRA enum entry | +| `controller/ui.cc` | Modify | Expand envelope selector range 0-6, register EnvPage | +| `controller/ui_pages/env_page.h` | Create | Custom envelope page header | +| `controller/ui_pages/env_page.cc` | Create | Custom envelope page — routes knobs to env1-3 (stride 8) or env4-7 (stride 4) based on selector | +| `controller/ui_pages/randomizer.cc` | Modify | Extend envelope randomization to cover extra_env offsets 112-127 | +| `controller/ui_pages/os_info_page.cc` | Modify | Bump version to v2.0 | +| `CARCOSA.md` | Modify | Document 7 envelopes, update mod source list, MIDI CC table | + +**Not updated (deprecated):** `utils/convert_shruthi_data.py` — hardcoded enum values no longer valid, utility is unmaintained. + +--- + +## Data Layout + +### New Patch (128 bytes) +``` +Offset 0-111: Unchanged (all existing fields at same offsets) +Offset 112-115: extra_env[0] — env4 ADSR (attack, decay, sustain, release) +Offset 116-119: extra_env[1] — env5 ADSR +Offset 120-123: extra_env[2] — env6 ADSR +Offset 124-127: extra_env[3] — env7 ADSR +``` + +### Constants +``` +kNumEnvLfoSlots = 3 (env_lfo[3] — envelopes with paired LFOs) +kNumLfos = 3 (unchanged, decoupled from envelope count) +kNumTotalEnvelopes = 7 +kNumExtraEnvelopes = 4 +``` + +### ModulationSource Enum (new order) +``` + 0: MOD_SRC_ENV_1 + 1: MOD_SRC_ENV_2 + 2: MOD_SRC_ENV_3 + 3: MOD_SRC_ENV_4 ← new + 4: MOD_SRC_ENV_5 ← new + 5: MOD_SRC_ENV_6 ← new + 6: MOD_SRC_ENV_7 ← new + 7: MOD_SRC_LFO_1 ← was 3 + 8: MOD_SRC_LFO_2 ← was 4 + 9: MOD_SRC_LFO_3 ← was 5 +10: MOD_SRC_LFO_4 ← was 6 +11: MOD_SRC_OP_1 ← was 7 +12: MOD_SRC_OP_2 ← was 8 +13: MOD_SRC_OP_3 ← was 9 +14: MOD_SRC_OP_4 ← was 10 +15: MOD_SRC_ARP_STEP ← was 13 (SEQ_1/SEQ_2 removed) +16: MOD_SRC_VELOCITY ← was 14 +17: MOD_SRC_AFTERTOUCH +18: MOD_SRC_PITCH_BEND +19: MOD_SRC_WHEEL +20: MOD_SRC_WHEEL_2 +21: MOD_SRC_EXPRESSION +22: MOD_SRC_NOTE +23: MOD_SRC_GATE +24: MOD_SRC_NOISE +25: MOD_SRC_RANDOM +26: MOD_SRC_CONSTANT_256 +27: MOD_SRC_CONSTANT_128 +28: MOD_SRC_CONSTANT_64 +29: MOD_SRC_CONSTANT_32 +30: MOD_SRC_CONSTANT_16 +31: MOD_SRC_CONSTANT_8 +32: MOD_SRC_CONSTANT_4 +33: MOD_SRC_LAST +``` + +--- + +### Task 1: Expand Patch struct and enums + +**Files:** +- Modify: `common/patch.h` + +- [ ] **Step 1: Add ExtraEnvelopeSettings struct** + +```cpp +struct ExtraEnvelopeSettings { + uint8_t attack; + uint8_t decay; + uint8_t sustain; + uint8_t release; +}; +``` + +- [ ] **Step 2: Expand Patch struct** + +Add after `padding[8]`: +```cpp + // Offset: 112-128 + ExtraEnvelopeSettings extra_env[4]; +}; +``` + +- [ ] **Step 3: Update constants** + +Do NOT change `kNumEnvelopes`. Instead: +```cpp +static const uint8_t kNumEnvLfoSlots = 3; // rename from kNumEnvelopes +static const uint8_t kNumLfos = kNumEnvLfoSlots; // keep at 3 +static const uint8_t kNumTotalEnvelopes = 7; +static const uint8_t kNumExtraEnvelopes = 4; +``` + +Then find/replace all `kNumEnvelopes` references: +- Uses that mean "env_lfo slots" or "LFO count" → `kNumEnvLfoSlots` +- Uses that mean "total envelope instances on voicecard" → `kNumTotalEnvelopes` + +- [ ] **Step 4: Update ModulationSource enum** + +Remove `MOD_SRC_SEQ_1` and `MOD_SRC_SEQ_2`. Insert `MOD_SRC_ENV_4` through `MOD_SRC_ENV_7` after `MOD_SRC_ENV_3`. + +- [ ] **Step 5: Add PatchParameter entries** + +```cpp + PRM_PATCH_EXTRA_ENV_ATTACK = 112, + PRM_PATCH_EXTRA_ENV_DECAY, + PRM_PATCH_EXTRA_ENV_SUSTAIN, + PRM_PATCH_EXTRA_ENV_RELEASE, +``` + +- [ ] **Step 6: Build voicecard to check compile** + +Run: `rm -rf build/ambika_voicecard && make all 2>&1 | grep error | head 20` +Expect: errors from renamed constants — fix all references. + +- [ ] **Step 7: Commit** + +--- + +### Task 2: Update voicecard envelope rendering + +**Files:** +- Modify: `voicecard/voice.h` +- Modify: `voicecard/voice.cc` +- Modify: `voicecard/voicecard_rx.h` + +- [ ] **Step 1: Expand envelope array** + +In `voice.h`, change `envelope_[kNumEnvelopes]` to `envelope_[kNumTotalEnvelopes]`. Update the static definition in `voice.cc`. + +- [ ] **Step 2: Update LoadSources** + +Replace 3 hardcoded render lines with loop: +```cpp +for (uint8_t i = 0; i < kNumTotalEnvelopes; ++i) { + modulation_sources_[MOD_SRC_ENV_1 + i] = envelope_[i].Render(); +} +``` + +- [ ] **Step 3: Update UpdateDestinations** + +Keep existing env_lfo loop for envelopes 0-2 (uses `kNumEnvLfoSlots`). Add second loop for extra envelopes 3-6: +```cpp +for (int i = 0; i < kNumExtraEnvelopes; ++i) { + envelope_[kNumEnvLfoSlots + i].Update( + patch_.extra_env[i].attack, + patch_.extra_env[i].decay, + patch_.extra_env[i].sustain, + patch_.extra_env[i].release); +} +``` + +- [ ] **Step 4: Update Init loop** + +Use `kNumTotalEnvelopes` for envelope init loop. Use `kNumEnvLfoSlots` for env_lfo-specific init. + +- [ ] **Step 5: Update TriggerEnvelope** + +The `TriggerEnvelope(uint8_t stage)` loop that triggers ALL envelopes: use `kNumTotalEnvelopes`. + +- [ ] **Step 6: Extend COMMAND_RETRIGGER_ENVELOPE in voicecard_rx.h** + +Add cases for envelopes 3-6: +```cpp +case COMMAND_RETRIGGER_ENVELOPE: +case COMMAND_RETRIGGER_ENVELOPE + 1: +case COMMAND_RETRIGGER_ENVELOPE + 2: +case COMMAND_RETRIGGER_ENVELOPE + 3: +case COMMAND_RETRIGGER_ENVELOPE + 4: +case COMMAND_RETRIGGER_ENVELOPE + 5: +case COMMAND_RETRIGGER_ENVELOPE + 6: + voice.TriggerEnvelope(command_ & 0x0f, ATTACK); + break; +``` + +- [ ] **Step 7: Update init_patch in voice.cc** + +Extend the PROGMEM `init_patch` with 16 zero bytes for extra_env[4]: +```cpp + // Extra envelopes (env4-7 ADSR) + 0, 40, 20, 60, + 0, 40, 20, 60, + 0, 40, 20, 60, + 0, 40, 20, 60, +``` + +- [ ] **Step 8: Replace MOD_SRC_SEQ references in voice.cc init_patch** + +Replace `MOD_SRC_SEQ_1` and `MOD_SRC_SEQ_2` with `MOD_SRC_ENV_4` and `MOD_SRC_ENV_5` (or zero them out). + +- [ ] **Step 9: Update ResetAllControllers** + +All `MOD_SRC_CONSTANT_*` enum values shifted. These are referenced by name so they auto-update, but verify. + +- [ ] **Step 10: Build voicecard, check size** + +Run: `rm -rf build/ambika_voicecard && make all && make bin` +Check: `avr-size build/ambika_voicecard/ambika_voicecard.elf` +Verify: RAM (data+bss) leaves >= 450 bytes free. Flash fits. + +- [ ] **Step 11: Commit** + +--- + +### Task 3: Update controller init_patch and mod source references + +**Files:** +- Modify: `controller/part.cc` + +- [ ] **Step 1: Replace MOD_SRC_SEQ references in init_patch** + +Replace `MOD_SRC_SEQ_1` and `MOD_SRC_SEQ_2` in both the modulation matrix init and modifier init sections. + +- [ ] **Step 2: Extend init_patch with extra_env bytes** + +Add 16 bytes to the PROGMEM `init_patch` for the extra envelopes. + +- [ ] **Step 3: Fix all kNumEnvelopes → kNumEnvLfoSlots references** + +Grep part.cc for `kNumEnvelopes` and replace with appropriate constant. + +- [ ] **Step 4: Build controller** + +- [ ] **Step 5: Commit** + +--- + +### Task 4: Update mod source display strings + +**Files:** +- Modify: `controller/parameter.cc` + +- [ ] **Step 1: Replace mod source string lookup with switch statement** + +In `PrintValue`, for `UNIT_MODULATION_SOURCE`, replace the `text + value` string table lookup with a complete switch covering all 33 sources. Use PSTR for each name. Handle both short (width <= 5) and long (width > 5) variants. + +```cpp +if (unit == UNIT_MODULATION_SOURCE) { + const prog_char* name; + switch (value) { + case MOD_SRC_ENV_1: name = (width > 5) ? PSTR("env 1") : PSTR("env1"); break; + case MOD_SRC_ENV_2: name = (width > 5) ? PSTR("env 2") : PSTR("env2"); break; + // ... all 33 sources + } + strncpy_P(buffer, name, width); + AlignRight(buffer, width); + return; +} +``` + +- [ ] **Step 2: Build and verify** + +- [ ] **Step 3: Commit** + +--- + +### Task 5: Add controller parameters for extra envelopes + +**Files:** +- Modify: `controller/parameter.h` +- Modify: `controller/parameter.cc` + +- [ ] **Step 1: Add 4 parameter definitions (76-79)** + +Extra envelope ADSR parameters. num_instances=4, stride=4, indexed_by a new UI state variable (see Task 6). + +Actually, since we're creating a custom EnvPage, these parameters don't need the stride/indexed_by pattern — the EnvPage handles the mapping directly by reading/writing patch bytes. The parameters just need to exist for MIDI CC access. + +```cpp +// 76: extra env attack (instance 0, MIDI-accessible) +{ PARAMETER_LEVEL_PATCH, PRM_PATCH_EXTRA_ENV_ATTACK, + UNIT_RAW_UINT8, 0, 127, 4, 4, 0xff, 0xff, + STR_RES_ATTK, STR_RES_ATTACK, STR_RES_ENVELOPE }, +// 77-79: decay, sustain, release (same pattern) +``` + +- [ ] **Step 2: Bump kNumParameters to 80** + +- [ ] **Step 3: Update MIDI CC map** + +CC 111-114 → params 76-79. + +- [ ] **Step 4: Build and verify** + +- [ ] **Step 5: Commit** + +--- + +### Task 6: Create custom EnvPage for 7 envelopes + +**Files:** +- Create: `controller/ui_pages/env_page.h` +- Create: `controller/ui_pages/env_page.cc` +- Modify: `controller/ui.h` +- Modify: `controller/ui.cc` + +- [ ] **Step 1: Create EnvPage class** + +Follows the same pattern as FmPage/KsPage/WcPage. Has a selector (knob 1) that cycles 0-6 (env1-env7). + +When selector 0-2: shows LFO controls (rate, shape) on top row + ADSR on bottom row + envelope curve. Reads/writes from `patch_.env_lfo[selector]`. + +When selector 3-6: hides LFO controls (top row knobs 2-4 = 0xff). ADSR on bottom row reads/writes from `patch_.extra_env[selector - 3]`. No envelope curve knob. + +Display: selector shows "env1" through "env7". + +- [ ] **Step 2: Register EnvPage in ui.h and ui.cc** + +Replace the existing `PAGE_ENV_LFO` entry to use `EnvPage::event_handlers_` instead of `ParameterEditor::event_handlers_`. + +Or: keep PAGE_ENV_LFO for the normal parameter editor and add the EnvPage as the handler. Since the existing page already uses custom parameter indices, swapping the handler is cleanest. + +- [ ] **Step 3: Build and verify** + +- [ ] **Step 4: Test selector cycling through all 7 envelopes** + +- [ ] **Step 5: Commit** + +--- + +### Task 7: Update randomizer for extra envelopes + +**Files:** +- Modify: `controller/ui_pages/randomizer.cc` + +- [ ] **Step 1: Extend envelope randomization** + +The existing loop `for (uint8_t env = 0; env < 3; ++env)` handles env_lfo offsets 24-47. Add a second loop for extra_env offsets 112-127: + +```cpp +if (env_depth) { + // Original 3 envelopes + for (uint8_t env = 0; env < 3; ++env) { + uint8_t base = 24 + env * 8; + RandomizeRange(patch, base, base + 4, 0, 127, env_depth); + } + // Extra 4 envelopes + for (uint8_t env = 0; env < 4; ++env) { + uint8_t base = 112 + env * 4; + RandomizeRange(patch, base, base + 4, 0, 127, env_depth); + } +} +``` + +- [ ] **Step 2: Commit** + +--- + +### Task 8: Version bump, docs, release + +**Files:** +- Modify: `controller/ui_pages/os_info_page.cc` +- Modify: `CARCOSA.md` +- Modify: `README.md` + +- [ ] **Step 1: Bump version to v2.0** + +- [ ] **Step 2: Update manual** + +Add env4-7 documentation to envelope section. Update mod source list (remove seq1/seq2, add env4-7). Update MIDI CC table. Add note about breaking patch format change. + +- [ ] **Step 3: Full build both firmwares** + +```bash +rm -rf build/ambika_voicecard build/ambika_controller +make all && make bin +make -f controller/makefile && make -f controller/makefile bin +avr-size build/ambika_voicecard/ambika_voicecard.elf build/ambika_controller/ambika_controller.elf +``` + +Verify: voicecard RAM leaves >= 450 bytes free. Controller stack >= 240 bytes. + +- [ ] **Step 4: Commit, push, create release** + +--- + +## Risk Assessment + +| Risk | Mitigation | +|------|-----------| +| Enum shift breaks all stored patches | Accepted — breaking change for v2.0. Document in release notes. EEPROM will need wiping on update. | +| kNumLfos coupling with kNumEnvelopes | Decoupled: kNumEnvLfoSlots=3, kNumLfos=3, kNumTotalEnvelopes=7 | +| RETRIGGER_ENVELOPE protocol limited to 0-2 | Extended to 0-6 in voicecard_rx.h | +| init_patch size mismatch after Patch grows | Both init_patches (voice.cc and part.cc) extended with 16 bytes | +| Mod source string table broken by enum shift | Replaced with switch statement — no string table dependency | +| EEPROM layout invalid after Patch grows | Document: user must reinitialize from OS info page or save/reload from SD | +| convert_shruthi_data.py broken | Deprecated — document as unmaintained | +| Controller RAM from custom EnvPage | Minimal — follows proven FmPage/KsPage/WcPage pattern with PROGMEM arrays | diff --git a/flash_sd.sh b/flash_sd.sh new file mode 100755 index 0000000..c9c9bbc --- /dev/null +++ b/flash_sd.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# Flash Ambika firmware to SD card +# Usage: ./flash_sd.sh [mount_point] + +set -e + +SDCARD="${1:-/mnt/sdcard}" +BUILD_DIR="build" +CONTROLLER_BIN="$BUILD_DIR/ambika_controller/ambika_controller.bin" +VOICECARD_BIN="$BUILD_DIR/ambika_voicecard/ambika_voicecard.bin" + +# Check firmware files exist +if [ ! -f "$CONTROLLER_BIN" ]; then + echo "Error: Controller firmware not found at $CONTROLLER_BIN" + echo "Run 'make -f controller/makefile && make -f controller/makefile bin' first" + exit 1 +fi + +if [ ! -f "$VOICECARD_BIN" ]; then + echo "Error: Voicecard firmware not found at $VOICECARD_BIN" + echo "Run 'make all && make bin' first" + exit 1 +fi + +# Find SD card if not mounted +if [ ! -d "$SDCARD" ]; then + echo "Mount point $SDCARD does not exist." + # Try to find and mount the SD card + DEVICE=$(lsblk -rno NAME,FSTYPE | grep vfat | grep mmc | head -1 | awk '{print $1}') + if [ -z "$DEVICE" ]; then + echo "Error: No SD card found" + exit 1 + fi + echo "Found SD card at /dev/$DEVICE" + sudo mkdir -p "$SDCARD" + sudo mount "/dev/$DEVICE" "$SDCARD" + echo "Mounted at $SDCARD" +fi + +# Check it's actually mounted +if ! mountpoint -q "$SDCARD" 2>/dev/null; then + DEVICE=$(lsblk -rno NAME,FSTYPE | grep vfat | grep mmc | head -1 | awk '{print $1}') + if [ -z "$DEVICE" ]; then + echo "Error: No SD card found" + exit 1 + fi + sudo mount "/dev/$DEVICE" "$SDCARD" + echo "Mounted /dev/$DEVICE at $SDCARD" +fi + +echo "Copying firmware to SD card..." +sudo cp "$CONTROLLER_BIN" "$SDCARD/AMBIKA.BIN" +echo " AMBIKA.BIN (controller: $(stat -c%s "$CONTROLLER_BIN") bytes)" + +for i in 1 2 3 4 5 6; do + sudo cp "$VOICECARD_BIN" "$SDCARD/VOICE${i}.BIN" +done +echo " VOICE1-6.BIN (voicecard: $(stat -c%s "$VOICECARD_BIN") bytes)" + +# Generate and copy factory patches +echo "" +echo "Generating factory patches..." +PATCH_STAGING=$(mktemp -d) +python3 tools/make_patches.py "$PATCH_STAGING" +sudo mkdir -p "$SDCARD/PATCH/BANK/C" +sudo cp "$PATCH_STAGING/PATCH/BANK/C/"*.PAT "$SDCARD/PATCH/BANK/C/" 2>/dev/null || true +rm -rf "$PATCH_STAGING" + +sudo sync +echo "" +echo "Done. Safe to eject SD card." +echo "" +echo "To flash on Ambika:" +echo " 1. Insert SD card" +echo " 2. Hold S8 during power-on to flash controller" +echo " 3. Flash each voicecard with S4 from OS info page" diff --git a/make_patches.py b/make_patches.py new file mode 100644 index 0000000..88e01f3 --- /dev/null +++ b/make_patches.py @@ -0,0 +1,305 @@ +#!/usr/bin/env python3 +"""Generate Carcosa factory patches for the Ambika polysynth.""" + +import struct +import os +import sys + +# Carcosa v2.0 Patch layout (144 bytes) +PATCH_SIZE = 144 + +# Waveform enums +WAVEFORM_NONE = 0 +WAVEFORM_SAW = 1 +WAVEFORM_SQUARE = 2 +WAVEFORM_TRIANGLE = 3 +WAVEFORM_SINE = 4 +WAVEFORM_QUAD_SAW_PAD = 5 +WAVEFORM_FILTERED_NOISE = 6 +WAVEFORM_FM4OP = 7 +WAVEFORM_KS_PLUCK = 8 +WAVEFORM_WAVESHAPE = 9 +WAVEFORM_WESTCOAST = 10 + +# FM waveforms (TX81Z W1-W8) +FM_WAVE_SINE = 0 +FM_WAVE_HALF_SINE = 1 +FM_WAVE_ABS_SINE = 2 +FM_WAVE_QUARTER_SINE = 3 +FM_WAVE_HALF_ABS = 4 +FM_WAVE_TRIPLE_ABS = 5 +FM_WAVE_PULSE_SINE = 6 +FM_WAVE_SAW_SINE = 7 + +# TX81Z coarse frequency ratio indices (index into ratio table) +R_050 = 0 # 0.50x +R_071 = 1 # 0.71x (√2/2) +R_078 = 2 # 0.78x +R_087 = 3 # 0.87x +R_100 = 4 # 1.00x +R_141 = 5 # 1.41x (√2) +R_157 = 6 # 1.57x (π/2) +R_173 = 7 # 1.73x (√3) +R_200 = 8 # 2.00x +R_282 = 9 # 2.82x +R_300 = 10 # 3.00x +R_314 = 11 # 3.14x (π) +R_400 = 13 # 4.00x +R_500 = 16 # 5.00x +R_600 = 19 # 6.00x +R_700 = 22 # 7.00x +R_800 = 25 # 8.00x + +# Mod sources (Carcosa v2.0) +MOD_SRC_ENV_1 = 0 +MOD_SRC_ENV_2 = 1 +MOD_SRC_ENV_3 = 2 +MOD_SRC_VELOCITY = 16 +MOD_SRC_PITCH_BEND = 18 +MOD_SRC_WHEEL = 19 + +# Mod destinations +MOD_DST_PARAMETER_1 = 0 +MOD_DST_OSC_1_2_COARSE = 4 +MOD_DST_FILTER_CUTOFF = 12 +MOD_DST_VCA = 18 + +# Envelope curve +ENV_EXP = 0 +ENV_LIN = 1 +LFO_TRI = 0 + + +def make_patch(): + """Return a zeroed 144-byte patch.""" + return bytearray(PATCH_SIZE) + + +ENGINE_CLASSIC = 0 +ENGINE_FM4OP = 1 +ENGINE_KS_PLUCK = 2 +ENGINE_WESTCOAST = 3 + +def set_engine(patch, engine): + """Set the synthesis engine type at padding[2] (offset 106).""" + patch[106] = engine + + +def set_fm(patch, algo, feedback=0, + wav1=0, wav2=0, wav3=0, wav4=0, + rat1=R_100, rat2=R_100, rat3=R_100, rat4=R_100, + fin1=0, fin2=0, fin3=0, fin4=0, + lvl1=127, lvl2=127, lvl3=127, lvl4=127): + """Set FM4OP parameters on a patch.""" + patch[0] = WAVEFORM_SAW # Safe fallback waveform; engine byte controls rendering + patch[1] = algo + patch[2] = rat1 + patch[3] = fin1 + patch[4] = wav1 | (wav2 << 4) + patch[5] = wav3 | (wav4 << 4) + patch[6] = rat2 + patch[7] = fin2 + patch[8] = rat3 # mix_balance + patch[9] = fin3 # mix_op + patch[10] = rat4 # mix_parameter + patch[11] = fin4 # mix_sub_osc_shape + patch[12] = lvl1 # mix_sub_osc + patch[13] = lvl2 # mix_noise + patch[14] = lvl3 # mix_fuzz + patch[15] = lvl4 # mix_crush + patch[104] = feedback # padding[0] + + +def set_filter(patch, cutoff=127, resonance=0, mode=0, env_amt=0, lfo_amt=0): + """Set filter parameters.""" + patch[16] = cutoff + patch[17] = resonance + patch[18] = mode + patch[22] = env_amt + patch[23] = lfo_amt + + +def set_env(patch, env_idx, attack=0, decay=40, sustain=20, release=60, + lfo_shape=LFO_TRI, lfo_rate=0, curve=ENV_EXP, retrigger=0): + """Set envelope/LFO parameters. env_idx 0-2 = env_lfo, 3-6 = extra.""" + if env_idx < 3: + base = 24 + env_idx * 8 + else: + base = 112 + (env_idx - 3) * 8 + patch[base] = attack + patch[base + 1] = decay + patch[base + 2] = sustain + patch[base + 3] = release + patch[base + 4] = lfo_shape + patch[base + 5] = lfo_rate + patch[base + 6] = curve + patch[base + 7] = retrigger + + +def set_mod(patch, slot, source, destination, amount): + """Set a modulation matrix slot (0-13).""" + base = 50 + slot * 3 + patch[base] = source + patch[base + 1] = destination + patch[base + 2] = amount + + +def make_riff_patch(patch_data, name): + """Wrap patch data in Ambika RIFF format.""" + name_bytes = name.encode('ascii')[:16].ljust(16, b'\x00') + name_chunk = struct.pack('<4sI', b'name', 16) + name_bytes + position = struct.pack(' 1 else '.' + + # FM patches — Bank C + save_patch(outdir, 'C', 1, 'FM LatelyBass ', fm_lately_bass()) + save_patch(outdir, 'C', 2, 'FM E.Piano ', fm_epiano()) + save_patch(outdir, 'C', 3, 'FM Brass ', fm_brass()) + save_patch(outdir, 'C', 4, 'FM Marimba ', fm_marimba()) + save_patch(outdir, 'C', 5, 'FM Strings ', fm_strings()) + save_patch(outdir, 'C', 6, 'FM Organ ', fm_organ()) + + +if __name__ == '__main__': + main() diff --git a/makefile b/makefile index 829bc11..b5b94be 100755 --- a/makefile +++ b/makefile @@ -1,4 +1,4 @@ -# Copyright 2011 Olivier Gillet. +# Copyright 2011 Emilie Gillet. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/midi/midi.h b/midi/midi.h index 3f8baee..47271b6 100755 --- a/midi/midi.h +++ b/midi/midi.h @@ -1,6 +1,6 @@ -// Copyright 2009 Olivier Gillet. +// Copyright 2009 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/peripherals/nunchuk/app.cc b/peripherals/nunchuk/app.cc index 104f5b6..892a3e0 100755 --- a/peripherals/nunchuk/app.cc +++ b/peripherals/nunchuk/app.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/peripherals/nunchuk/app.h b/peripherals/nunchuk/app.h index ef6fb9f..2c3305a 100755 --- a/peripherals/nunchuk/app.h +++ b/peripherals/nunchuk/app.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/peripherals/nunchuk/apps/cyclo_sequencer.cc b/peripherals/nunchuk/apps/cyclo_sequencer.cc index 6522c5c..1ca74a4 100755 --- a/peripherals/nunchuk/apps/cyclo_sequencer.cc +++ b/peripherals/nunchuk/apps/cyclo_sequencer.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/peripherals/nunchuk/apps/cyclo_sequencer.h b/peripherals/nunchuk/apps/cyclo_sequencer.h index 03fa92d..f201d7f 100755 --- a/peripherals/nunchuk/apps/cyclo_sequencer.h +++ b/peripherals/nunchuk/apps/cyclo_sequencer.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/peripherals/nunchuk/apps/droner.cc b/peripherals/nunchuk/apps/droner.cc index 8fa214f..638a42a 100755 --- a/peripherals/nunchuk/apps/droner.cc +++ b/peripherals/nunchuk/apps/droner.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/peripherals/nunchuk/apps/droner.h b/peripherals/nunchuk/apps/droner.h index 3d69238..52ac9ff 100755 --- a/peripherals/nunchuk/apps/droner.h +++ b/peripherals/nunchuk/apps/droner.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/peripherals/nunchuk/apps/drummer.cc b/peripherals/nunchuk/apps/drummer.cc index 29a82ba..0b8ec18 100755 --- a/peripherals/nunchuk/apps/drummer.cc +++ b/peripherals/nunchuk/apps/drummer.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/peripherals/nunchuk/apps/drummer.h b/peripherals/nunchuk/apps/drummer.h index e003f69..9ea2bb0 100755 --- a/peripherals/nunchuk/apps/drummer.h +++ b/peripherals/nunchuk/apps/drummer.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/peripherals/nunchuk/hardware_design/pcb/nunchuk-v01.brd b/peripherals/nunchuk/hardware_design/pcb/nunchuk-v01.brd index 4cac6eb..6aa0a2b 100644 Binary files a/peripherals/nunchuk/hardware_design/pcb/nunchuk-v01.brd and b/peripherals/nunchuk/hardware_design/pcb/nunchuk-v01.brd differ diff --git a/peripherals/nunchuk/hardware_design/pcb/nunchuk-v01.sch b/peripherals/nunchuk/hardware_design/pcb/nunchuk-v01.sch index b6f3208..6d6e97f 100644 Binary files a/peripherals/nunchuk/hardware_design/pcb/nunchuk-v01.sch and b/peripherals/nunchuk/hardware_design/pcb/nunchuk-v01.sch differ diff --git a/peripherals/nunchuk/makefile b/peripherals/nunchuk/makefile index f52299a..5dab69d 100755 --- a/peripherals/nunchuk/makefile +++ b/peripherals/nunchuk/makefile @@ -1,4 +1,4 @@ -# Copyright 2011 Olivier Gillet. +# Copyright 2011 Emilie Gillet. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/peripherals/nunchuk/midi_output.cc b/peripherals/nunchuk/midi_output.cc index 92dd518..9756b95 100755 --- a/peripherals/nunchuk/midi_output.cc +++ b/peripherals/nunchuk/midi_output.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/peripherals/nunchuk/midi_output.h b/peripherals/nunchuk/midi_output.h index 777572e..313b944 100755 --- a/peripherals/nunchuk/midi_output.h +++ b/peripherals/nunchuk/midi_output.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/peripherals/nunchuk/nunchuk.cc b/peripherals/nunchuk/nunchuk.cc index 808501c..980e76b 100755 --- a/peripherals/nunchuk/nunchuk.cc +++ b/peripherals/nunchuk/nunchuk.cc @@ -1,4 +1,4 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/peripherals/nunchuk/ui.cc b/peripherals/nunchuk/ui.cc index 5bb3d29..020591e 100755 --- a/peripherals/nunchuk/ui.cc +++ b/peripherals/nunchuk/ui.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/peripherals/nunchuk/ui.h b/peripherals/nunchuk/ui.h index 6c83ddf..625c520 100755 --- a/peripherals/nunchuk/ui.h +++ b/peripherals/nunchuk/ui.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/tools b/tools index d73c7b8..b2224dc 160000 --- a/tools +++ b/tools @@ -1 +1 @@ -Subproject commit d73c7b84eb7238583396582cfedb94acedc6fd59 +Subproject commit b2224dcd689da41135bcb8a708bdd1acc940ff25 diff --git a/utils/compile_bank.py b/utils/compile_bank.py index c05833f..4486473 100755 --- a/utils/compile_bank.py +++ b/utils/compile_bank.py @@ -1,8 +1,8 @@ #!/usr/bin/python2.5 # -# Copyright 2012 Olivier Gillet. +# Copyright 2012 Emilie Gillet. # -# Author: Olivier Gillet (ol.gillet@gmail.com) +# Author: Emilie Gillet (emilie.o.gillet@gmail.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/utils/convert_shruthi_data.py b/utils/convert_shruthi_data.py index 2462f0f..5517acc 100755 --- a/utils/convert_shruthi_data.py +++ b/utils/convert_shruthi_data.py @@ -1,8 +1,8 @@ #!/usr/bin/python2.5 # -# Copyright 2011 Olivier Gillet. +# Copyright 2011 Emilie Gillet. # -# Author: Olivier Gillet (ol.gillet@gmail.com) +# Author: Emilie Gillet (emilie.o.gillet@gmail.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/utils/convert_shruthi_syx.py b/utils/convert_shruthi_syx.py index 045543e..f4b56bb 100755 --- a/utils/convert_shruthi_syx.py +++ b/utils/convert_shruthi_syx.py @@ -1,8 +1,8 @@ #!/usr/bin/python2.5 # -# Copyright 2011 Olivier Gillet. +# Copyright 2011 Emilie Gillet. # -# Author: Olivier Gillet (ol.gillet@gmail.com) +# Author: Emilie Gillet (emilie.o.gillet@gmail.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/utils/list_bank.py b/utils/list_bank.py index 67b3dbf..26b05ff 100755 --- a/utils/list_bank.py +++ b/utils/list_bank.py @@ -1,8 +1,8 @@ #!/usr/bin/python2.5 # -# Copyright 2012 Olivier Gillet. +# Copyright 2012 Emilie Gillet. # -# Author: Olivier Gillet (ol.gillet@gmail.com) +# Author: Emilie Gillet (emilie.o.gillet@gmail.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/voicecard/audio_out.cc b/voicecard/audio_out.cc index 7416b66..deb49d7 100755 --- a/voicecard/audio_out.cc +++ b/voicecard/audio_out.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/audio_out.h b/voicecard/audio_out.h index 7e62e44..124064e 100755 --- a/voicecard/audio_out.h +++ b/voicecard/audio_out.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/bootloader/bootloader.cc b/voicecard/bootloader/bootloader.cc index bfa810b..726ad7f 100755 --- a/voicecard/bootloader/bootloader.cc +++ b/voicecard/bootloader/bootloader.cc @@ -1,4 +1,4 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/bootloader/makefile b/voicecard/bootloader/makefile index bf8569c..23b3c8a 100755 --- a/voicecard/bootloader/makefile +++ b/voicecard/bootloader/makefile @@ -1,4 +1,4 @@ -# Copyright 2011 Olivier Gillet. +# Copyright 2011 Emilie Gillet. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/voicecard/envelope.h b/voicecard/envelope.h index 4878ca3..cf03754 100755 --- a/voicecard/envelope.h +++ b/voicecard/envelope.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -67,25 +67,48 @@ class Envelope { uint8_t attack, uint8_t decay, uint8_t sustain, - uint8_t release) { + uint8_t release, + uint8_t curve = ENVELOPE_CURVE_EXPONENTIAL) { + linear_ = (curve == ENVELOPE_CURVE_LINEAR || + curve == ENVELOPE_CURVE_LOOP_LINEAR); + looping_ = (curve == ENVELOPE_CURVE_LOOP || + curve == ENVELOPE_CURVE_LOOP_LINEAR); stage_phase_increment_[ATTACK] = ResourcesManager::Lookup< uint16_t, uint8_t>(lut_res_env_portamento_increments, attack); stage_phase_increment_[DECAY] = ResourcesManager::Lookup< uint16_t, uint8_t>(lut_res_env_portamento_increments, decay); stage_phase_increment_[RELEASE] = ResourcesManager::Lookup< uint16_t, uint8_t>(lut_res_env_portamento_increments, release); - stage_target_[DECAY] = sustain << 1; - stage_target_[SUSTAIN] = stage_target_[DECAY]; + if (looping_) { + // In loop mode, sustain sets the floor of the cycle. + stage_target_[DECAY] = sustain << 1; + stage_target_[SUSTAIN] = stage_target_[DECAY]; + } else { + stage_target_[DECAY] = sustain << 1; + stage_target_[SUSTAIN] = stage_target_[DECAY]; + } } uint8_t Render() { phase_ += phase_increment_; if (phase_ < phase_increment_) { value_ = U8MixU16(a_, b_, 255); - Trigger(++stage_); + uint8_t next_stage = stage_ + 1; + // In loop mode, retrigger attack after decay completes. + // Skip sustain — go straight from decay to attack. + if (looping_ && stage_ == DECAY) { + Trigger(ATTACK); + } else if (looping_ && stage_ == SUSTAIN) { + // Shouldn't reach here, but safety: retrigger. + Trigger(ATTACK); + } else { + Trigger(next_stage); + } } if (phase_increment_) { - uint8_t step = InterpolateSample(wav_res_env_expo, phase_); + uint8_t step = linear_ + ? (phase_ >> 8) + : InterpolateSample(wav_res_env_expo, phase_); value_ = U8MixU16(a_, b_, step); } return value_ >> 8; @@ -110,6 +133,10 @@ class Envelope { // Current value of the envelope. uint16_t value_; + // Envelope curve type. + uint8_t linear_; + uint8_t looping_; + DISALLOW_COPY_AND_ASSIGN(Envelope); }; diff --git a/voicecard/fm4op.h b/voicecard/fm4op.h new file mode 100644 index 0000000..0bae686 --- /dev/null +++ b/voicecard/fm4op.h @@ -0,0 +1,374 @@ +// Copyright 2026 Ambika contributors. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ----------------------------------------------------------------------------- +// +// 4-operator FM synthesis engine, TX81Z style. + +#ifndef VOICECARD_FM4OP_H_ +#define VOICECARD_FM4OP_H_ + +#include "avrlib/base.h" +#include "avrlib/op.h" +#include "voicecard/resources.h" + +using namespace avrlib; + +namespace ambika { + +// 16-bit sine table (512 entries + 1 wrap), defined in voice.cc. +extern const prog_uint16_t wav_res_sine16[] PROGMEM; + +// 16-bit sine interpolation for FM — 512 entries, returns 0-65535. +static inline uint16_t InterpolateSine16(uint16_t phase) { + // 9-bit index (512 entries), 7-bit fractional + uint16_t index = phase >> 7; + uint8_t frac = (phase << 1) & 0xFE; + uint16_t a = ResourcesManager::Lookup( + wav_res_sine16, index); + uint16_t b = ResourcesManager::Lookup( + wav_res_sine16, index + 1); + // Linear interpolation in 16-bit + return a + (static_cast(b - a) * frac >> 8); +} + +// TX81Z waveform types derived from the sine table. +enum FmWaveform { + FM_WAVE_SINE, // W1: Full sine + FM_WAVE_HALF_SINE, // W2: Positive half, then silence + FM_WAVE_ABS_SINE, // W3: Full-wave rectified sine + FM_WAVE_QUARTER_SINE, // W4: First quarter, rest silence + FM_WAVE_HALF_ABS, // W5: Rectified double sine, first half only + FM_WAVE_TRIPLE_ABS, // W6: Three rectified bumps, then silence + FM_WAVE_PULSE_SINE, // W7: Alternating positive/negative bumps + FM_WAVE_SAW_SINE, // W8: Staircase-like derived waveform + FM_WAVE_LAST +}; + +// TX81Z algorithm topologies. +enum FmAlgorithm { + FM_ALG_1, // 4->3->2->1->out (full serial) + FM_ALG_2, // (3+4)->2->1->out + FM_ALG_3, // (4->3)+2 -> 1->out + FM_ALG_4, // (4->3)+(2->1)->out (two parallel pairs) + FM_ALG_5, // (4->2)+(4->3->1)->out + FM_ALG_6, // 4->(1+2+3)->out (one mod, three carriers) + FM_ALG_7, // (4->1)+2+3->out + FM_ALG_8, // 1+2+3+4->out (all carriers, additive) + FM_ALG_LAST +}; + +// Patch field reinterpretation for FM4OP mode. +// These map onto existing patch byte offsets when osc[0].shape == WAVEFORM_FM4OP. +// +// osc[0].shape = WAVEFORM_FM4OP (mode selector) +// osc[0].parameter = algorithm (0-7) +// osc[0].range = op1 coarse ratio +// osc[0].detune = op1 fine detune +// osc[1].shape = op1 waveform (low nibble) | op2 waveform (high nibble) +// osc[1].parameter = op3 waveform (low nibble) | op4 waveform (high nibble) +// osc[1].range = op2 coarse ratio +// osc[1].detune = op2 fine detune +// mix_balance = op3 coarse ratio +// mix_op = op3 fine detune +// mix_parameter = op4 coarse ratio +// mix_sub_osc_shape = op4 fine detune +// mix_sub_osc = op1 output level +// mix_noise = op2 output level +// mix_fuzz = op3 output level +// mix_crush = op4 output level +// padding[0] = feedback level + +struct FmOperator { + uint16_t phase; + uint16_t phase_increment; +}; + +class Fm4Op { + public: + Fm4Op() { } + + void Init() { + for (uint8_t i = 0; i < 4; ++i) { + op_[i].phase = 0; + op_[i].phase_increment = 0; + } + feedback_state_[0] = 0; + feedback_state_[1] = 0; + } + + // TX81Z-style exponential output level curve (upper 64 entries). + // Levels 0 = silence, 1-63 = 1 (near-silent), 64-127 from table. + static const prog_uint8_t level_to_amplitude_hi_[64] PROGMEM; + + static inline uint8_t LevelToAmplitude(uint8_t level) { + if (level == 0) return 0; + if (level < 64) return 1; + return pgm_read_byte(&level_to_amplitude_hi_[level - 64]); + } + + // Scale modulator output by its amplitude. + // Caller pre-applies exponential curve + envelope before passing amp. + // No shift — max mod index ~3.1 radians (8-bit ceiling). + static inline int16_t ScaleMod(uint8_t op_out, uint8_t amp) { + return static_cast(op_out - 128) * amp; + } + + // Scale carrier output by its amplitude (maintains center at 128). + // Caller pre-applies exponential curve + envelope before passing amp. + static inline uint8_t ScaleCarrier(uint8_t op_out, uint8_t amp) { + return 128 + ((static_cast(op_out - 128) * amp) >> 8); + } + + // Compute the output of a single operator waveform using 16-bit sine. + static inline uint8_t RenderWaveform(uint8_t waveform, uint16_t phase) { + uint16_t s; + switch (waveform) { + case FM_WAVE_SINE: + default: + s = InterpolateSine16(phase); + break; + + case FM_WAVE_HALF_SINE: + s = (phase < 0x8000) ? InterpolateSine16(phase << 1) : 32768; + break; + + case FM_WAVE_ABS_SINE: + s = InterpolateSine16((phase & 0x7FFF) << 1); + break; + + case FM_WAVE_QUARTER_SINE: + s = (phase < 0x4000) ? InterpolateSine16(phase << 2) : 32768; + break; + + case FM_WAVE_HALF_ABS: + s = (phase < 0x8000) ? InterpolateSine16((phase & 0x3FFF) << 2) : 32768; + break; + + case FM_WAVE_TRIPLE_ABS: + if (phase < 0x8000) { + uint16_t tripled = static_cast(phase * 3); + s = InterpolateSine16((tripled & 0x7FFF) << 1); + } else { + s = 32768; + } + break; + + case FM_WAVE_PULSE_SINE: { + s = InterpolateSine16((phase & 0x3FFF) << 2); + if (phase >= 0x8000) { + s = 65536 - s; + } + break; + } + + case FM_WAVE_SAW_SINE: + if (phase < 0x4000) { + s = InterpolateSine16(phase << 2); + } else if (phase < 0x8000) { + s = 65535; + } else if (phase < 0xC000) { + s = InterpolateSine16(phase << 2); + } else { + s = 0; + } + break; + } + return s >> 8; // Truncate to 8-bit at output only + } + + // Render a block of FM4OP audio. + // Reads operator configuration directly from the patch data. + void Render( + uint8_t algorithm, + const uint8_t* op_waveform, // 4 waveform types + const uint8_t* op_level, // 4 output levels + uint8_t feedback, + uint16_t base_increment, + uint8_t* buffer, + uint8_t size) { + + while (size--) { + // Advance all operator phases. + for (uint8_t i = 0; i < 4; ++i) { + op_[i].phase += op_[i].phase_increment; + } + + // 16-bit feedback state for evolving, chaotic oscillation. + // Higher precision prevents the feedback loop from locking into + // a fixed repeating pattern (the TX81Z uses 14-bit internally). + int32_t fb = feedback_state_[0] + feedback_state_[1]; + int32_t fb_mod = (fb * feedback) >> 6; + + // Render op4 waveform, store 16-bit precision for feedback path. + uint16_t op4_phase = op_[3].phase + static_cast(fb_mod); + uint16_t op4_raw = InterpolateSine16(op4_phase); + uint8_t op4_out = RenderWaveform(op_waveform[3], op4_phase); + feedback_state_[1] = feedback_state_[0]; + feedback_state_[0] = static_cast(op4_raw - 32768); + + // Render remaining operators and route per algorithm. + // Carrier outputs are scaled by their level (per-op envelope control). + uint8_t out; + switch (algorithm) { + case FM_ALG_1: { + // 4->3->2->1->out (single carrier, VCA handles volume) + int16_t mod3 = ScaleMod(op4_out, op_level[3]); + uint8_t op3_out = RenderWaveform(op_waveform[2], + op_[2].phase + mod3); + int16_t mod2 = ScaleMod(op3_out, op_level[2]); + uint8_t op2_out = RenderWaveform(op_waveform[1], + op_[1].phase + mod2); + int16_t mod1 = ScaleMod(op2_out, op_level[1]); + out = RenderWaveform(op_waveform[0], + op_[0].phase + mod1); + break; + } + + case FM_ALG_2: { + // (3+4)->2->1->out (single carrier) + uint8_t op3_out = RenderWaveform(op_waveform[2], + op_[2].phase); + int16_t mod2 = ScaleMod(op4_out, op_level[3]) + + ScaleMod(op3_out, op_level[2]); + uint8_t op2_out = RenderWaveform(op_waveform[1], + op_[1].phase + mod2); + int16_t mod1 = ScaleMod(op2_out, op_level[1]); + out = RenderWaveform(op_waveform[0], + op_[0].phase + mod1); + break; + } + + case FM_ALG_3: { + // (4->3) + 2 -> 1->out (single carrier) + int16_t mod3 = ScaleMod(op4_out, op_level[3]); + uint8_t op3_out = RenderWaveform(op_waveform[2], + op_[2].phase + mod3); + uint8_t op2_out = RenderWaveform(op_waveform[1], + op_[1].phase); + int16_t mod1 = ScaleMod(op3_out, op_level[2]) + + ScaleMod(op2_out, op_level[1]); + out = RenderWaveform(op_waveform[0], + op_[0].phase + mod1); + break; + } + + case FM_ALG_4: { + // (4->3) + (2->1) -> out (2 carriers) + int16_t mod3 = ScaleMod(op4_out, op_level[3]); + uint8_t op3_out = RenderWaveform(op_waveform[2], + op_[2].phase + mod3); + uint8_t op2_out = RenderWaveform(op_waveform[1], + op_[1].phase); + int16_t mod1 = ScaleMod(op2_out, op_level[1]); + uint8_t op1_out = RenderWaveform(op_waveform[0], + op_[0].phase + mod1); + out = (op1_out >> 1) + (op3_out >> 1); + break; + } + + case FM_ALG_5: { + // (4->3->1) + (4->2) -> out (2 carriers) + int16_t mod3 = ScaleMod(op4_out, op_level[3]); + uint8_t op3_out = RenderWaveform(op_waveform[2], + op_[2].phase + mod3); + int16_t mod1 = ScaleMod(op3_out, op_level[2]); + uint8_t op1_out = RenderWaveform(op_waveform[0], + op_[0].phase + mod1); + int16_t mod2 = ScaleMod(op4_out, op_level[3]); + uint8_t op2_out = RenderWaveform(op_waveform[1], + op_[1].phase + mod2); + out = (op1_out >> 1) + (op2_out >> 1); + break; + } + + case FM_ALG_6: { + // 4->(1+2+3)->out (3 carriers, VCA handles volume) + int16_t mod = ScaleMod(op4_out, op_level[3]); + uint8_t op1_out = RenderWaveform(op_waveform[0], + op_[0].phase + mod); + uint8_t op2_out = RenderWaveform(op_waveform[1], + op_[1].phase + mod); + uint8_t op3_out = RenderWaveform(op_waveform[2], + op_[2].phase + mod); + out = (op1_out >> 2) + (op2_out >> 2) + (op3_out >> 2) + 32; + break; + } + + case FM_ALG_7: { + // (4->1)+2+3->out (3 carriers) + int16_t mod1 = ScaleMod(op4_out, op_level[3]); + uint8_t op1_out = RenderWaveform(op_waveform[0], + op_[0].phase + mod1); + uint8_t op2_out = RenderWaveform(op_waveform[1], + op_[1].phase); + uint8_t op3_out = RenderWaveform(op_waveform[2], + op_[2].phase); + out = (op1_out >> 2) + (op2_out >> 2) + (op3_out >> 2) + 32; + break; + } + + case FM_ALG_8: + default: { + // 1+2+3+4->out (all carriers, additive) + uint8_t op1_out = RenderWaveform(op_waveform[0], + op_[0].phase); + uint8_t op2_out = RenderWaveform(op_waveform[1], + op_[1].phase); + uint8_t op3_out = RenderWaveform(op_waveform[2], + op_[2].phase); + out = (op1_out >> 2) + (op2_out >> 2) + + (op3_out >> 2) + (op4_out >> 2); + break; + } + } + *buffer++ = out; + } + } + + // TX81Z frequency ratio table — 64 entries, 8.8 fixed-point. + // Coarse ratio byte (0-63) indexes into this table. + // Values: 0.50, 0.71, 0.78, 0.87, 1.00, 1.41, 1.57, 1.73, 2.00, ... + static const prog_uint16_t tx81z_ratios_[] PROGMEM; + + // Set operator phase increment using TX81Z-style ratio lookup. + void SetOperatorIncrement(uint8_t op_index, uint16_t base_increment, + uint8_t coarse_ratio, int8_t fine_detune) { + // Look up the ratio from the TX81Z table (8.8 fixed-point). + uint8_t idx = coarse_ratio & 0x3F; + uint16_t ratio_fp = ResourcesManager::Lookup( + tx81z_ratios_, idx); + // Multiply base increment by ratio: (base * ratio) >> 8. + uint32_t product = static_cast(base_increment) * ratio_fp; + uint16_t increment = product >> 8; + // Fine detune: small pitch offset. + if (fine_detune > 0) { + increment += (increment >> 8) * fine_detune; + } else if (fine_detune < 0) { + increment -= (increment >> 8) * (-fine_detune); + } + op_[op_index].phase_increment = increment; + } + + FmOperator* mutable_op(uint8_t i) { return &op_[i]; } + + private: + FmOperator op_[4]; + int16_t feedback_state_[2]; + + DISALLOW_COPY_AND_ASSIGN(Fm4Op); +}; + +} // namespace ambika + +#endif // VOICECARD_FM4OP_H_ diff --git a/voicecard/hardware_design/pcb/Voicecard-4P-v01.brd b/voicecard/hardware_design/pcb/Voicecard-4P-v01.brd index f9cb85d..17d1225 100644 Binary files a/voicecard/hardware_design/pcb/Voicecard-4P-v01.brd and b/voicecard/hardware_design/pcb/Voicecard-4P-v01.brd differ diff --git a/voicecard/hardware_design/pcb/Voicecard-4P-v01.sch b/voicecard/hardware_design/pcb/Voicecard-4P-v01.sch index d7782a9..77346d3 100644 Binary files a/voicecard/hardware_design/pcb/Voicecard-4P-v01.sch and b/voicecard/hardware_design/pcb/Voicecard-4P-v01.sch differ diff --git a/voicecard/hardware_design/pcb/Voicecard-SMR-v04.brd b/voicecard/hardware_design/pcb/Voicecard-SMR-v04.brd index 6afaa27..2d17a07 100644 Binary files a/voicecard/hardware_design/pcb/Voicecard-SMR-v04.brd and b/voicecard/hardware_design/pcb/Voicecard-SMR-v04.brd differ diff --git a/voicecard/hardware_design/pcb/Voicecard-SMR-v04.sch b/voicecard/hardware_design/pcb/Voicecard-SMR-v04.sch index 78ce535..e19a304 100644 Binary files a/voicecard/hardware_design/pcb/Voicecard-SMR-v04.sch and b/voicecard/hardware_design/pcb/Voicecard-SMR-v04.sch differ diff --git a/voicecard/hardware_design/pcb/Voicecard-SVF-v05.brd b/voicecard/hardware_design/pcb/Voicecard-SVF-v05.brd index bdd9dcb..265a889 100644 Binary files a/voicecard/hardware_design/pcb/Voicecard-SVF-v05.brd and b/voicecard/hardware_design/pcb/Voicecard-SVF-v05.brd differ diff --git a/voicecard/hardware_design/pcb/Voicecard-SVF-v05.sch b/voicecard/hardware_design/pcb/Voicecard-SVF-v05.sch index f806a85..9213fd3 100644 Binary files a/voicecard/hardware_design/pcb/Voicecard-SVF-v05.sch and b/voicecard/hardware_design/pcb/Voicecard-SVF-v05.sch differ diff --git a/voicecard/hardware_design/pcb/Voicecard-Template.sch b/voicecard/hardware_design/pcb/Voicecard-Template.sch index d5f52af..676ffe4 100644 Binary files a/voicecard/hardware_design/pcb/Voicecard-Template.sch and b/voicecard/hardware_design/pcb/Voicecard-Template.sch differ diff --git a/voicecard/karplus.h b/voicecard/karplus.h new file mode 100644 index 0000000..9d02abc --- /dev/null +++ b/voicecard/karplus.h @@ -0,0 +1,280 @@ +// Copyright 2026 Ambika contributors. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ----------------------------------------------------------------------------- +// +// Karplus-Strong plucked string synthesis. + +#ifndef VOICECARD_KARPLUS_H_ +#define VOICECARD_KARPLUS_H_ + +#include "avrlib/base.h" +#include "avrlib/op.h" +#include "avrlib/random.h" + +using namespace avrlib; + +namespace ambika { + +static const uint16_t kKarplusBufferSize = 192; + +// Excitation types. +enum KsExcitation { + KS_EXC_NOISE, // White noise burst + KS_EXC_CLICK, // Short impulse + KS_EXC_BRIGHT, // Filtered noise (bright) + KS_EXC_DARK, // Filtered noise (dark) + KS_EXC_LAST +}; + +// Patch field mapping for KS mode: +// osc[0].shape (offset 0) = WAVEFORM_KS_PLUCK +// osc[0].parameter (offset 1) = damping (brightness/decay) +// osc[0].range (offset 2) = pitch range +// osc[0].detune (offset 3) = fine tune +// osc[1].shape (offset 4) = excitation type (KsExcitation) +// osc[1].parameter (offset 5) = excitation color (brightness of burst) +// osc[1].range (offset 6) = decay rate modifier +// osc[1].detune (offset 7) = pluck position (changes harmonics) +// mix_balance (offset 8) = body resonance + +class KarplusStrong { + public: + KarplusStrong() { } + + void Init() { + write_pos_ = 0; + delay_length_ = kKarplusBufferSize; + excited_ = 0; + lp_state_ = 0; + for (uint16_t i = 0; i < kKarplusBufferSize; ++i) { + delay_line_[i] = 0; + } + } + + // Excite the string. + void Trigger(uint8_t excitation_type, uint8_t color, uint8_t position) { + // Fill the ENTIRE buffer — SetPitch may change delay_length_ after this. + for (uint16_t i = 0; i < kKarplusBufferSize; ++i) { + int16_t sample; + switch (excitation_type) { + case KS_EXC_CLICK: + // Sharp impulse — very percussive attack. + sample = (i < 2) ? 16383 : ((i < 4) ? -16383 : 0); + break; + case KS_EXC_BRIGHT: + // Full-range noise with extra high frequency content. + sample = (static_cast(Random::GetByte()) - 128) << 7; + sample += (static_cast(Random::GetByte()) - 128) << 5; + break; + case KS_EXC_DARK: + // Heavily filtered noise — warm, muted pluck. + if (i > 0) { + sample = (delay_line_[i - 1] * 3 + + ((static_cast(Random::GetByte()) - 128) << 6)) >> 2; + } else { + sample = (static_cast(Random::GetByte()) - 128) << 5; + } + break; + default: + // Standard noise burst. + sample = (static_cast(Random::GetByte()) - 128) << 7; + break; + } + delay_line_[i] = sample; + } + + // Pluck position comb filter. + if (position > 4) { + uint16_t notch_period = (kKarplusBufferSize * position) >> 7; + if (notch_period > 1 && notch_period < kKarplusBufferSize) { + for (uint16_t i = 0; i < kKarplusBufferSize - notch_period; ++i) { + delay_line_[i] = (delay_line_[i] + delay_line_[i + notch_period]) >> 1; + } + } + } + + // Excitation color low-pass. + uint8_t filter_passes = (127 - color) >> 4; + for (uint8_t pass = 0; pass < filter_passes; ++pass) { + for (uint16_t i = 1; i < kKarplusBufferSize; ++i) { + delay_line_[i] = (delay_line_[i] >> 1) + (delay_line_[i - 1] >> 1); + } + } + + write_pos_ = 0; + excited_ = 1; + } + + // Simple trigger (backwards compatible). + void Trigger() { + Trigger(KS_EXC_NOISE, 127, 0); + } + + void SetPitch(uint16_t phase_increment) { + // delay_length = sample_rate / frequency + // phase_increment represents frequency as a fraction of the sample rate. + // For accurate pitch: len = 65536 / phase_increment (full 16-bit division). + if (phase_increment < 1) { + phase_increment = 1; + } + uint16_t len = static_cast( + static_cast(65536) / phase_increment); + if (len < 2) len = 2; + if (len >= kKarplusBufferSize) len = kKarplusBufferSize - 1; + delay_length_ = len; + } + + // Full render with all parameters. + void Render(uint8_t damping, uint8_t decay, uint8_t body, + uint8_t ens_rate, uint8_t ens_depth, uint8_t ens_spread, + uint8_t ens_mix, uint8_t stiffness, uint8_t feedback, + uint8_t* buffer, uint8_t size) { + if (!excited_) { + while (size--) { + *buffer++ = 128; + } + return; + } + + // Damping: IIR low-pass cutoff in the feedback loop. + // 0 = bright (high cutoff), 127 = very dark (low cutoff). + // The IIR filter creates dramatically different tones vs the 2-tap FIR. + uint8_t lp_cutoff = 255 - (damping << 1); + if (lp_cutoff < 2) lp_cutoff = 2; + // Pitch scaling: less filtering for shorter strings. + if (delay_length_ < 64) { + lp_cutoff = lp_cutoff + ((255 - lp_cutoff) >> 1); // push toward bright + } else if (delay_length_ < 96) { + lp_cutoff = lp_cutoff + ((255 - lp_cutoff) >> 2); + } + + // Decay: energy loss per delay-line cycle. + // 0 = infinite sustain, 127 = fast fade. + uint8_t decay_amount = decay >> 1; + + // Stiffness offset scales with delay length. + uint16_t stiff_offset = (delay_length_ >> 3) + 1; + + // Feedback as sustain: pushes cutoff higher (less loss = longer ring). + lp_cutoff = lp_cutoff + (((255 - lp_cutoff) * feedback) >> 8); + + // Advance ensemble LFO. + uint16_t lfo_inc = static_cast(ens_rate + 1) << 4; + + while (size--) { + ens_lfo_phase_ += lfo_inc; + + uint16_t read_pos = write_pos_ + 1; + if (read_pos >= delay_length_) read_pos = 0; + + int16_t current = delay_line_[read_pos]; + uint16_t next_pos = read_pos + 1; + if (next_pos >= delay_length_) next_pos = 0; + int16_t next = delay_line_[next_pos]; + + // KS core: standard 2-sample average (the string physics). + int16_t avg = (current >> 1) + (next >> 1); + + // Damping: IIR one-pole low-pass. This is what shapes the tone. + // High cutoff = bright (metallic), low cutoff = dark (muted thud). + lp_state_ += (static_cast(avg - lp_state_) * lp_cutoff) >> 8; + int16_t filtered = lp_state_; + + // Stiffness: allpass-like mixing with a point further in the delay. + // Adds inharmonic partials (like piano string stiffness). + if (stiffness > 4) { + uint16_t stiff_pos = read_pos + stiff_offset; + if (stiff_pos >= delay_length_) stiff_pos -= delay_length_; + int16_t stiff_sample = delay_line_[stiff_pos]; + uint8_t sa = stiffness >> 1; // 0-63 range for stability + filtered = (static_cast(filtered) * (256 - sa) + + static_cast(stiff_sample) * sa) >> 8; + } + + // Decay: apply energy loss once per cycle through the buffer. + // Use a counter that wraps at delay_length_. + if (decay_amount && read_pos == 0) { + for (uint16_t i = 0; i < delay_length_; ++i) { + delay_line_[i] -= (delay_line_[i] * decay_amount) >> 9; + } + } + + // Body resonance: comb filter at 1/3 string length. + // Emphasizes the 3rd harmonic and its multiples. + if (body > 4) { + uint16_t body_pos = read_pos + (delay_length_ / 3); + if (body_pos >= delay_length_) body_pos -= delay_length_; + int16_t body_sample = delay_line_[body_pos]; + uint8_t body_amt = body >> 1; // 0-63 range for stability + filtered = (static_cast(filtered) * (256 - body_amt) + + static_cast(body_sample) * body_amt) >> 8; + } + + delay_line_[read_pos] = filtered; + write_pos_ = read_pos; + + // Ensemble: three read heads. + int16_t output = filtered; + if (ens_mix > 0 && ens_depth > 0) { + uint8_t lfo_8 = ens_lfo_phase_ >> 8; + int8_t lfo_val = (ens_lfo_phase_ & 0x8000) + ? static_cast(255 - lfo_8) + : static_cast(lfo_8); + + int16_t offset2 = (static_cast(lfo_val) * ens_depth) >> 7; + int16_t offset3 = -offset2 + (ens_spread >> 1); + + int16_t p2 = (static_cast(read_pos) + offset2) % + static_cast(delay_length_); + if (p2 < 0) p2 += delay_length_; + int16_t p3 = (static_cast(read_pos) + offset3) % + static_cast(delay_length_); + if (p3 < 0) p3 += delay_length_; + + int16_t head2 = delay_line_[static_cast(p2)]; + int16_t head3 = delay_line_[static_cast(p3)]; + + uint8_t dry = 255 - ens_mix; + int32_t mixed = static_cast(filtered) * dry + + (static_cast(head2) + head3) * (ens_mix >> 1); + output = mixed >> 8; + } + + // Truncate to 8-bit at output only (centered at 128). + int16_t out8 = (output >> 7) + 128; + if (out8 > 255) out8 = 255; + if (out8 < 0) out8 = 0; + *buffer++ = static_cast(out8); + } + } + + // Simple render (backwards compatible). + void Render(uint8_t damping, uint8_t* buffer, uint8_t size) { + Render(damping, 0, 0, 0, 0, 0, 0, 0, 0, buffer, size); + } + + private: + int16_t delay_line_[kKarplusBufferSize]; // 16-bit for smooth decay + int16_t lp_state_; // IIR low-pass state for damping + uint16_t write_pos_; + uint16_t delay_length_; + uint8_t excited_; + uint16_t ens_lfo_phase_; + + DISALLOW_COPY_AND_ASSIGN(KarplusStrong); +}; + +} // namespace ambika + +#endif // VOICECARD_KARPLUS_H_ diff --git a/voicecard/leds.cc b/voicecard/leds.cc index e243b1d..bbb1d6f 100755 --- a/voicecard/leds.cc +++ b/voicecard/leds.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/leds.h b/voicecard/leds.h index 4019518..955022c 100755 --- a/voicecard/leds.h +++ b/voicecard/leds.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/makefile b/voicecard/makefile index e9bcf01..de31bc4 100755 --- a/voicecard/makefile +++ b/voicecard/makefile @@ -1,4 +1,4 @@ -# Copyright 2011 Olivier Gillet. +# Copyright 2011 Emilie Gillet. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/voicecard/oscillator.cc b/voicecard/oscillator.cc index a1f22e1..fd27d3e 100755 --- a/voicecard/oscillator.cc +++ b/voicecard/oscillator.cc @@ -1,6 +1,6 @@ -// Copyright 2012 Olivier Gillet. +// Copyright 2012 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -15,6 +15,7 @@ #include "voicecard/oscillator.h" +#include "voicecard/fm4op.h" #include "voicecard/voicecard.h" namespace ambika { @@ -111,23 +112,30 @@ void Oscillator::RenderBandlimitedPwm(uint8_t* buffer) { // The position is determined by the note pitch, to prevent aliasing. void Oscillator::RenderSimpleWavetable(uint8_t* buffer) { + // Pure sine: use 16-bit table for clean output. + if (shape_ == WAVEFORM_SINE) { + BEGIN_SAMPLE_LOOP + UPDATE_PHASE_MORE_REGISTERS + uint16_t index = phase.integral >> 7; + uint8_t frac = (phase.integral & 0x7F) << 1; + uint16_t a = pgm_read_word(&wav_res_sine16[index]); + uint16_t b = pgm_read_word(&wav_res_sine16[index + 1]); + *buffer++ = (a + ((static_cast(b - a) * frac) >> 8)) >> 8; + END_SAMPLE_LOOP + return; + } + uint8_t balance_index = U8Swap4(note_); uint8_t gain_2 = balance_index & 0xf0; uint8_t gain_1 = ~gain_2; - uint8_t wave_1_index, wave_2_index; - if (shape_ != WAVEFORM_SINE) { - uint8_t wave_index = balance_index & 0xf; - uint8_t base_resource_id = shape_ == WAVEFORM_SAW ? - WAV_RES_BANDLIMITED_SAW_0 : - (shape_ == WAVEFORM_SQUARE ? WAV_RES_BANDLIMITED_SQUARE_0 : - WAV_RES_BANDLIMITED_TRIANGLE_0); - wave_1_index = base_resource_id + wave_index; - wave_index = U8AddClip(wave_index, 1, kNumZonesFullSampleRate); - wave_2_index = base_resource_id + wave_index; - } else { - wave_1_index = WAV_RES_SINE; - wave_2_index = WAV_RES_SINE; - } + uint8_t wave_index = balance_index & 0xf; + uint8_t base_resource_id = shape_ == WAVEFORM_SAW ? + WAV_RES_BANDLIMITED_SAW_0 : + (shape_ == WAVEFORM_SQUARE ? WAV_RES_BANDLIMITED_SQUARE_0 : + WAV_RES_BANDLIMITED_TRIANGLE_0); + uint8_t wave_1_index = base_resource_id + wave_index; + wave_index = U8AddClip(wave_index, 1, kNumZonesFullSampleRate); + uint8_t wave_2_index = base_resource_id + wave_index; const prog_uint8_t* wave_1 = waveform_table[wave_1_index]; const prog_uint8_t* wave_2 = waveform_table[wave_2_index]; @@ -143,7 +151,6 @@ void Oscillator::RenderSimpleWavetable(uint8_t* buffer) { *buffer++ = sample; END_SAMPLE_LOOP } else { - // The waveshaper for the triangle is different. BEGIN_SAMPLE_LOOP UPDATE_PHASE_MORE_REGISTERS uint8_t sample = InterpolateTwoTables( @@ -157,214 +164,6 @@ void Oscillator::RenderSimpleWavetable(uint8_t* buffer) { } } -// ------- Casio CZ-like synthesis ------------------------------------------- -void Oscillator::RenderCzSaw(uint8_t* buffer) { - BEGIN_SAMPLE_LOOP - UPDATE_PHASE - uint8_t phi = phase.integral >> 8; - uint8_t clipped_phi = phi < 0x20 ? phi << 3 : 0xff; - // Interpolation causes more aliasing here. - *buffer++ = ReadSample(wav_res_sine, - U8MixU16(phi, clipped_phi, parameter_ << 1)); - END_SAMPLE_LOOP -} - -void Oscillator::RenderCzResoSaw(uint8_t* buffer) { - uint16_t increment = phase_increment_.integral + ( - (phase_increment_.integral * uint32_t(parameter_)) >> 2); - uint8_t type = shape_ - WAVEFORM_CZ_SAW_LP; - uint16_t phase_2 = data_.secondary_phase; - BEGIN_SAMPLE_LOOP - UPDATE_PHASE - if (phase.carry) { - phase_2 = ResourcesManager::Lookup( - lut_res_cz_phase_reset, type & 0x03); - } - phase_2 += increment; - uint8_t carrier = ReadSample(wav_res_sine, phase_2); - uint8_t window = ~(phase.integral >> 8); - if (type & 2) { - *buffer++ = S8U8MulShift8(carrier + 128, window) + 128; - } else { - *buffer++ = U8U8MulShift8(carrier, window); - } - END_SAMPLE_LOOP - data_.secondary_phase = phase_2; -} - -void Oscillator::RenderCzResoPulse(uint8_t* buffer) { - uint16_t increment = phase_increment_.integral + ( - (phase_increment_.integral * uint32_t(parameter_)) >> 2); - uint8_t type = shape_ - WAVEFORM_CZ_SAW_LP; - uint16_t phase_2 = data_.secondary_phase; - BEGIN_SAMPLE_LOOP - UPDATE_PHASE - if (phase.carry) { - phase_2 = ResourcesManager::Lookup( - lut_res_cz_phase_reset, type & 0x03); - } - phase_2 += increment; - uint8_t carrier = ReadSample(wav_res_sine, phase_2); - uint8_t window = 0; - if (phase.integral < 0x4000) { - window = 255; - } else if (phase.integral < 0x8000) { - window = ~(phase.integral - 0x4000) >> 6; - } - if (type == 5) { - carrier >>= 1; - carrier += 128; - } - if (type & 2) { - *buffer++ = S8U8MulShift8(carrier + 128, window) + 128; - } else { - *buffer++ = U8U8MulShift8(carrier, window); - } - END_SAMPLE_LOOP - data_.secondary_phase = phase_2; -} - -void Oscillator::RenderCzResoTri(uint8_t* buffer) { - uint16_t increment = phase_increment_.integral + ( - (phase_increment_.integral * uint32_t(parameter_)) >> 2); - uint8_t type = shape_ - WAVEFORM_CZ_SAW_LP; - uint16_t phase_2 = data_.secondary_phase; - BEGIN_SAMPLE_LOOP - UPDATE_PHASE - if (phase.carry) { - phase_2 = ResourcesManager::Lookup( - lut_res_cz_phase_reset, type & 0x03); - } - phase_2 += increment; - uint8_t carrier = ReadSample(wav_res_sine, phase_2); - uint8_t window = (phase.integral & 0x8000) ? - ~static_cast(phase.integral >> 7) : - phase.integral >> 7; - if (type & 2) { - *buffer++ = S8U8MulShift8(carrier + 128, window) + 128; - } else { - *buffer++ = U8U8MulShift8(carrier, window); - } - END_SAMPLE_LOOP - data_.secondary_phase = phase_2; -} - -// ------- FM ---------------------------------------------------------------- -void Oscillator::RenderFm(uint8_t* buffer) { - uint8_t offset = fm_parameter_; - if (offset < 24) { - offset = 0; - } else if (offset > 48) { - offset = 24; - } else { - offset = offset - 24; - } - uint16_t multiplier = ResourcesManager::Lookup( - lut_res_fm_frequency_ratios, offset); - uint16_t increment = ( - static_cast(phase_increment_.integral) * multiplier) >> 8; - parameter_ <<= 1; - - uint16_t phase_2 = data_.secondary_phase; - BEGIN_SAMPLE_LOOP - UPDATE_PHASE - phase_2 += increment; - uint8_t modulator = InterpolateSample(wav_res_sine, - phase_2); - uint16_t modulation = modulator * parameter_; - *buffer++ = InterpolateSample(wav_res_sine, - phase.integral + modulation); - END_SAMPLE_LOOP - data_.secondary_phase = phase_2; -} - -// ------- 8-bit land -------------------------------------------------------- -void Oscillator::Render8BitLand(uint8_t* buffer) { - BEGIN_SAMPLE_LOOP - UPDATE_PHASE - uint8_t x = parameter_; - *buffer++ = (((phase.integral >> 8) ^ (x << 1)) & (~x)) + (x >> 1); - END_SAMPLE_LOOP -} - -void Oscillator::RenderVowel(uint8_t* buffer) { - data_.vw.update = (data_.vw.update + 1) & 3; - if (!data_.vw.update) { - uint8_t offset_1 = U8ShiftRight4(parameter_); - offset_1 = U8U8Mul(offset_1, 7); - uint8_t offset_2 = offset_1 + 7; - uint8_t balance = parameter_ & 15; - - // Interpolate formant frequencies. - for (uint8_t i = 0; i < 3; ++i) { - data_.vw.formant_increment[i] = U8U4MixU12( - ResourcesManager::Lookup( - wav_res_vowel_data, offset_1 + i), - ResourcesManager::Lookup( - wav_res_vowel_data, offset_2 + i), - balance); - data_.vw.formant_increment[i] <<= 3; - } - - // Interpolate formant amplitudes. - for (uint8_t i = 0; i < 4; ++i) { - uint8_t amplitude_a = ResourcesManager::Lookup( - wav_res_vowel_data, - offset_1 + 3 + i); - uint8_t amplitude_b = ResourcesManager::Lookup( - wav_res_vowel_data, - offset_2 + 3 + i); - data_.vw.formant_amplitude[i] = U8U4MixU8( - amplitude_a, - amplitude_b, balance); - } - } - - int16_t phase_noise = S8S8Mul(Random::state_msb(), data_.vw.noise_modulation); - BEGIN_SAMPLE_LOOP - int8_t result = 0; - uint8_t phaselet; - - data_.vw.formant_phase[0] += data_.vw.formant_increment[0]; - phaselet = (data_.vw.formant_phase[0] >> 8) & 0xf0; - result = ResourcesManager::Lookup( - wav_res_formant_sine, - phaselet | data_.vw.formant_amplitude[0]); - - data_.vw.formant_phase[1] += data_.vw.formant_increment[1]; - phaselet = (data_.vw.formant_phase[1] >> 8) & 0xf0; - result += ResourcesManager::Lookup( - wav_res_formant_sine, - phaselet | data_.vw.formant_amplitude[1]); - - data_.vw.formant_phase[2] += data_.vw.formant_increment[2]; - phaselet = (data_.vw.formant_phase[2] >> 8) & 0xf0; - result += ResourcesManager::Lookup( - wav_res_formant_square, - phaselet | data_.vw.formant_amplitude[2]); - - result = S8U8MulShift8(result, phase.integral >> 8); - phase.integral -= phase_increment_int.integral; - if ((phase.integral + phase_noise) < phase_increment_int.integral) { - data_.vw.formant_phase[0] = 0; - data_.vw.formant_phase[1] = 0; - data_.vw.formant_phase[2] = 0; - } - uint8_t x = S16ClipS8(4 * result) + 128; - *buffer++ = x; - *buffer++ = x; - size--; - END_SAMPLE_LOOP -} - -// ------- Dirty Pwm (kills kittens) ----------------------------------------- -void Oscillator::RenderDirtyPwm(uint8_t* buffer) { - BEGIN_SAMPLE_LOOP - UPDATE_PHASE - *buffer++ = (phase.integral >> 8) < 127 + parameter_ ? 0 : 255; - END_SAMPLE_LOOP -} - // ------- Quad saw (mit aliasing) ------------------------------------------- void Oscillator::RenderQuadSawPad(uint8_t* buffer) { uint16_t phase_spread = ( @@ -421,84 +220,34 @@ void Oscillator::RenderFilteredNoise(uint8_t* buffer) { data_.no.rng_state = rng_state; } -// The position is freely determined by the parameter -void Oscillator::RenderInterpolatedWavetable(uint8_t* buffer) { - // Which wavetable should we play?. - const prog_uint8_t* wavetable_definition = - wav_res_wavetables + U8U8Mul( - shape_ - WAVEFORM_WAVETABLE_1, - 18); - // Get a 8:8 value with the wave index in the first byte, and the - // balance amount in the second byte. - uint8_t num_steps = ResourcesManager::Lookup( - wavetable_definition, - 0); - uint16_t pointer = U8U8Mul(parameter_ << 1, num_steps); - uint16_t wave_index_1 = ResourcesManager::Lookup( - wavetable_definition, - 1 + (pointer >> 8)); - uint16_t wave_index_2 = ResourcesManager::Lookup( - wavetable_definition, - 2 + (pointer >> 8)); - uint8_t gain = pointer & 0xff; - const prog_uint8_t* wave_1 = wav_res_waves + U8U8Mul( - wave_index_1, - 129); - const prog_uint8_t* wave_2 = wav_res_waves + U8U8Mul( - wave_index_2, - 129); - BEGIN_SAMPLE_LOOP - UPDATE_PHASE_MORE_REGISTERS - *buffer++ = InterpolateTwoTables( - wave_1, - wave_2, - phase.integral >> 1, - ~gain, - gain); - END_SAMPLE_LOOP -} - -// The position is freely determined by the parameter -void Oscillator::RenderWavequence(uint8_t* buffer) { - const prog_uint8_t* wave = wav_res_waves + U8U8Mul( - parameter_, - 129); +// ------- Waveshaping synthesis ---------------------------------------- +void Oscillator::RenderWaveshape(uint8_t* buffer) { BEGIN_SAMPLE_LOOP UPDATE_PHASE - *buffer++ = InterpolateSample(wave, phase.integral >> 1); + // Generate a sine wave, then scale it by the parameter to control + // how hard we drive the waveshaper. + uint8_t sine = InterpolateSample(wav_res_sine, phase.integral); + // Scale the sine around center. Higher parameter = more drive. + int16_t driven = (static_cast(sine) - 128) * + (parameter_ + 1); + driven = (driven >> 6) + 128; + if (driven > 255) driven = 255; + if (driven < 0) driven = 0; + // Run through the distortion waveshaper table. + *buffer++ = ResourcesManager::Lookup( + wav_res_distortion, static_cast(driven)); END_SAMPLE_LOOP } /* static */ const Oscillator::RenderFn Oscillator::fn_table_[] PROGMEM = { - &Oscillator::RenderSilence, - - &Oscillator::RenderSimpleWavetable, - &Oscillator::RenderBandlimitedPwm, - &Oscillator::RenderSimpleWavetable, - &Oscillator::RenderSimpleWavetable, - - &Oscillator::RenderCzSaw, - &Oscillator::RenderCzResoSaw, - &Oscillator::RenderCzResoSaw, - &Oscillator::RenderCzResoSaw, - &Oscillator::RenderCzResoSaw, - &Oscillator::RenderCzResoPulse, - &Oscillator::RenderCzResoPulse, - &Oscillator::RenderCzResoPulse, - &Oscillator::RenderCzResoPulse, - &Oscillator::RenderCzResoTri, - - &Oscillator::RenderQuadSawPad, - - &Oscillator::RenderFm, - - &Oscillator::Render8BitLand, - &Oscillator::RenderDirtyPwm, - &Oscillator::RenderFilteredNoise, - &Oscillator::RenderVowel, - - &Oscillator::RenderInterpolatedWavetable + &Oscillator::RenderSilence, // NONE + &Oscillator::RenderSimpleWavetable, // SAW + &Oscillator::RenderBandlimitedPwm, // SQUARE + &Oscillator::RenderSimpleWavetable, // TRIANGLE + &Oscillator::RenderSimpleWavetable, // SINE + &Oscillator::RenderQuadSawPad, // PAD + &Oscillator::RenderFilteredNoise, // NOISE }; -} // namespace \ No newline at end of file +} // namespace diff --git a/voicecard/oscillator.h b/voicecard/oscillator.h index 32b4e99..0034754 100755 --- a/voicecard/oscillator.h +++ b/voicecard/oscillator.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -53,14 +53,6 @@ static inline uint8_t InterpolateTwoTables( static const uint8_t kNumZonesFullSampleRate = 6; static const uint8_t kNumZonesHalfSampleRate = 5; -struct VowelSynthesizerState { - uint16_t formant_increment[3]; - uint16_t formant_phase[3]; - uint8_t formant_amplitude[3]; - uint8_t noise_modulation; - uint8_t update; // Update only every kVowelControlRateDecimation-th call. -}; - struct FilteredNoiseState { uint8_t lp_noise_sample; uint16_t rng_state; @@ -72,10 +64,8 @@ struct QuadSawPadState { }; union OscillatorState { - VowelSynthesizerState vw; FilteredNoiseState no; QuadSawPadState qs; - uint16_t secondary_phase; }; typedef void (*OscRenderFn)(uint8_t* buffer); @@ -110,17 +100,17 @@ class Oscillator { } else { RenderBandlimitedPwm(buffer); } - } else { + } else if (shape_ == WAVEFORM_WAVESHAPE) { + RenderWaveshape(buffer); + } else if (shape_ >= WAVEFORM_FM4OP) { + // Special modes handled in voice.cc; render silence as fallback. + RenderSilence(buffer); + } else if (shape_ < WAVEFORM_LAST) { RenderFn fn; - - uint8_t index = shape_ >= WAVEFORM_WAVETABLE_1 - ? WAVEFORM_WAVETABLE_1 - : shape_; - ResourcesManager::Load(fn_table_, index, &fn); - if (shape_ == WAVEFORM_WAVEQUENCE) { - fn = &Oscillator::RenderWavequence; - } + ResourcesManager::Load(fn_table_, shape_, &fn); (this->*fn)(buffer); + } else { + RenderSilence(buffer); } } @@ -162,18 +152,9 @@ class Oscillator { void RenderSilence(uint8_t* buffer); void RenderBandlimitedPwm(uint8_t* buffer); void RenderSimpleWavetable(uint8_t* buffer); - void RenderCzSaw(uint8_t* buffer); - void RenderCzResoSaw(uint8_t* buffer); - void RenderCzResoPulse(uint8_t* buffer); - void RenderCzResoTri(uint8_t* buffer); - void RenderFm(uint8_t* buffer); - void Render8BitLand(uint8_t* buffer); - void RenderVowel(uint8_t* buffer); - void RenderDirtyPwm(uint8_t* buffer); void RenderQuadSawPad(uint8_t* buffer); void RenderFilteredNoise(uint8_t* buffer); - void RenderInterpolatedWavetable(uint8_t* buffer); - void RenderWavequence(uint8_t* buffer); + void RenderWaveshape(uint8_t* buffer); DISALLOW_COPY_AND_ASSIGN(Oscillator); }; diff --git a/voicecard/resources.cc b/voicecard/resources.cc index 1575e04..2684085 100644 --- a/voicecard/resources.cc +++ b/voicecard/resources.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/resources.h b/voicecard/resources.h index 4084d94..3913672 100644 --- a/voicecard/resources.h +++ b/voicecard/resources.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/resources/lookup_tables.py b/voicecard/resources/lookup_tables.py index 7832e49..471cb31 100755 --- a/voicecard/resources/lookup_tables.py +++ b/voicecard/resources/lookup_tables.py @@ -1,8 +1,8 @@ #!/usr/bin/python2.5 # -# Copyright 2011 Olivier Gillet. +# Copyright 2011 Emilie Gillet. # -# Author: Olivier Gillet (ol.gillet@gmail.com) +# Author: Emilie Gillet (emilie.o.gillet@gmail.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/voicecard/resources/resources.py b/voicecard/resources/resources.py index e39dd93..d1cf5d2 100755 --- a/voicecard/resources/resources.py +++ b/voicecard/resources/resources.py @@ -1,8 +1,8 @@ #!/usr/bin/python2.5 # -# Copyright 2011 Olivier Gillet. +# Copyright 2011 Emilie Gillet. # -# Author: Olivier Gillet (ol.gillet@gmail.com) +# Author: Emilie Gillet (emilie.o.gillet@gmail.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -19,9 +19,9 @@ # # Master resources file. -header = """// Copyright 2011 Olivier Gillet. +header = """// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/resources/waveforms.py b/voicecard/resources/waveforms.py index 91f2b23..6cfa774 100755 --- a/voicecard/resources/waveforms.py +++ b/voicecard/resources/waveforms.py @@ -1,8 +1,8 @@ #!/usr/bin/python2.5 # -# Copyright 2011 Olivier Gillet. +# Copyright 2011 Emilie Gillet. # -# Author: Olivier Gillet (ol.gillet@gmail.com) +# Author: Emilie Gillet (emilie.o.gillet@gmail.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/voicecard/sub_oscillator.h b/voicecard/sub_oscillator.h index fd6f0e7..fe1d76b 100755 --- a/voicecard/sub_oscillator.h +++ b/voicecard/sub_oscillator.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -53,7 +53,8 @@ class SubOscillator { uint8_t tri = phase_.integral >> 7; v = phase_.integral & 0x8000 ? tri : ~tri; } - *buffer++ = U8Mix(*buffer, v, mix_gain, sub_gain); + *buffer = U8Mix(*buffer, v, mix_gain, sub_gain); + ++buffer; } } diff --git a/voicecard/transient_generator.h b/voicecard/transient_generator.h index 03c400b..9dd2ae9 100755 --- a/voicecard/transient_generator.h +++ b/voicecard/transient_generator.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -45,7 +45,8 @@ class TransientGenerator { while (counter_ && size--) { uint8_t value = (*fn)(); uint8_t amplitude = U8U8MulShift8(gain_, amount); - *buffer++ = U8Mix(*buffer, value, amplitude); + *buffer = U8Mix(*buffer, value, amplitude); + ++buffer; } } @@ -68,7 +69,7 @@ class TransientGenerator { static uint8_t RenderBlow() { decimate_ += 2; if (decimate_ >= 16) { - decimate_ -= 17; + decimate_ -= 16; rng_state_ = rng_state_ * 73 + counter_; if (decimate_ == 0) { --counter_; diff --git a/voicecard/voice.cc b/voicecard/voice.cc index d2c1812..4062301 100755 --- a/voicecard/voice.cc +++ b/voicecard/voice.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -20,6 +20,7 @@ #include "voicecard/voice.h" #include "voicecard/audio_out.h" +#include "voicecard/leds.h" #include "voicecard/oscillator.h" #include "voicecard/sub_oscillator.h" #include "voicecard/transient_generator.h" @@ -39,13 +40,13 @@ TransientGenerator transient_generator; /* */ -uint8_t Voice::patch_data_[1]; Patch Voice::patch_; -uint8_t Voice::part_data_[1]; +uint8_t *Voice::patch_data_ = reinterpret_cast (&Voice::patch_); Part Voice::part_; +uint8_t *Voice::part_data_ = reinterpret_cast (&Voice::part_); Lfo Voice::voice_lfo_; -Envelope Voice::envelope_[kNumEnvelopes]; +Envelope Voice::envelope_[kNumTotalEnvelopes]; uint8_t Voice::gate_; int16_t Voice::pitch_increment_; int16_t Voice::pitch_target_; @@ -58,6 +59,101 @@ uint8_t Voice::osc2_buffer_[kAudioBlockSize]; uint8_t Voice::sync_state_[kAudioBlockSize]; uint8_t Voice::no_sync_[kAudioBlockSize]; uint8_t Voice::dummy_sync_state_[kAudioBlockSize]; +Fm4Op Voice::fm4op_; +KarplusStrong Voice::karplus_; +WestCoast Voice::westcoast_; +uint8_t Voice::last_engine_ = 0xFF; + +// TX81Z frequency ratios as 8.8 fixed-point (ratio × 256). +const prog_uint16_t Fm4Op::tx81z_ratios_[] PROGMEM = { + 128, 182, 200, 223, 256, 361, 402, 443, + 512, 722, 768, 804, 886, 1024, 1085, 1206, + 1280, 1329, 1446, 1536, 1608, 1772, 1792, 1810, + 2010, 2048, 2171, 2214, 2304, 2412, 2532, 2560, + 2657, 2813, 2816, 2893, 3072, 3100, 3215, 3256, + 3328, 3543, 3584, 3610, 3617, 3840, 3981, 3986, + 4019, 4342, 4421, 4429, 4703, 4823, 4872, 5064, + 5225, 5315, 5427, 5627, 5757, 6029, 6200, 6643, +}; + +// TX81Z-style exponential output level curve (0.75 dB/step). +// Upper 64 entries (levels 64-127). Levels 0-63 handled by LevelToAmplitude(). +const prog_uint8_t Fm4Op::level_to_amplitude_hi_[64] PROGMEM = { + 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, + 4, 5, 5, 6, 6, 7, 7, 8, 9, 10, 10, 11, 12, 14, 15, 16, + 18, 19, 21, 23, 25, 27, 29, 32, 35, 38, 42, 45, 49, 54, 59, 64, + 70, 76, 83, 90, 99, 108, 117, 128, 139, 152, 166, 181, 197, 215, 234, 255, +}; + +// 512-entry 16-bit sine table for FM synthesis. +// Phase-shifted to match original table (trough at index 0). +const prog_uint16_t wav_res_sine16[] PROGMEM = { + 1, 3, 10, 23, 40, 62, 89, 121, + 158, 200, 247, 299, 355, 417, 483, 554, + 630, 711, 797, 887, 982, 1083, 1187, 1297, + 1411, 1531, 1654, 1783, 1916, 2054, 2196, 2343, + 2495, 2651, 2812, 2977, 3146, 3321, 3499, 3682, + 3870, 4061, 4257, 4458, 4662, 4871, 5084, 5301, + 5523, 5748, 5978, 6211, 6449, 6690, 6936, 7185, + 7438, 7695, 7956, 8221, 8489, 8761, 9036, 9315, + 9598, 9884, 10173, 10466, 10763, 11062, 11365, 11671, + 11980, 12293, 12608, 12927, 13248, 13573, 13900, 14230, + 14563, 14899, 15237, 15578, 15922, 16268, 16617, 16968, + 17321, 17677, 18035, 18395, 18758, 19122, 19489, 19858, + 20228, 20601, 20975, 21351, 21729, 22108, 22489, 22872, + 23256, 23641, 24028, 24416, 24806, 25196, 25588, 25981, + 26375, 26770, 27166, 27562, 27960, 28358, 28756, 29156, + 29556, 29956, 30357, 30758, 31160, 31561, 31963, 32365, + 32768, 33170, 33572, 33974, 34375, 34777, 35178, 35579, + 35979, 36379, 36779, 37177, 37575, 37973, 38369, 38765, + 39160, 39554, 39947, 40339, 40729, 41119, 41507, 41894, + 42279, 42663, 43046, 43427, 43806, 44184, 44560, 44934, + 45307, 45677, 46046, 46413, 46777, 47140, 47500, 47858, + 48214, 48567, 48918, 49267, 49613, 49957, 50298, 50636, + 50972, 51305, 51635, 51962, 52287, 52608, 52927, 53242, + 53555, 53864, 54170, 54473, 54772, 55069, 55362, 55651, + 55937, 56220, 56499, 56774, 57046, 57314, 57579, 57840, + 58097, 58350, 58599, 58845, 59086, 59324, 59557, 59787, + 60012, 60234, 60451, 60664, 60873, 61077, 61278, 61474, + 61665, 61853, 62036, 62214, 62389, 62558, 62723, 62884, + 63040, 63192, 63339, 63481, 63619, 63752, 63881, 64004, + 64124, 64238, 64348, 64452, 64553, 64648, 64738, 64824, + 64905, 64981, 65052, 65118, 65180, 65236, 65288, 65335, + 65377, 65414, 65446, 65473, 65495, 65512, 65525, 65532, + 65535, 65532, 65525, 65512, 65495, 65473, 65446, 65414, + 65377, 65335, 65288, 65236, 65180, 65118, 65052, 64981, + 64905, 64824, 64738, 64648, 64553, 64452, 64348, 64238, + 64124, 64004, 63881, 63752, 63619, 63481, 63339, 63192, + 63040, 62884, 62723, 62558, 62389, 62214, 62036, 61853, + 61665, 61474, 61278, 61077, 60873, 60664, 60451, 60234, + 60012, 59787, 59557, 59324, 59086, 58845, 58599, 58350, + 58097, 57840, 57579, 57314, 57046, 56774, 56499, 56220, + 55937, 55651, 55362, 55069, 54772, 54473, 54170, 53864, + 53555, 53242, 52927, 52608, 52287, 51962, 51635, 51305, + 50972, 50636, 50298, 49957, 49613, 49267, 48918, 48567, + 48214, 47858, 47500, 47140, 46777, 46413, 46046, 45677, + 45307, 44934, 44560, 44184, 43806, 43427, 43046, 42663, + 42279, 41894, 41507, 41119, 40729, 40339, 39947, 39554, + 39160, 38765, 38369, 37973, 37575, 37177, 36779, 36379, + 35979, 35579, 35178, 34777, 34375, 33974, 33572, 33170, + 32768, 32365, 31963, 31561, 31160, 30758, 30357, 29956, + 29556, 29156, 28756, 28358, 27960, 27562, 27166, 26770, + 26375, 25981, 25588, 25196, 24806, 24416, 24028, 23641, + 23256, 22872, 22489, 22108, 21729, 21351, 20975, 20601, + 20228, 19858, 19489, 19122, 18758, 18395, 18035, 17677, + 17321, 16968, 16617, 16268, 15922, 15578, 15237, 14899, + 14563, 14230, 13900, 13573, 13248, 12927, 12608, 12293, + 11980, 11671, 11365, 11062, 10763, 10466, 10173, 9884, + 9598, 9315, 9036, 8761, 8489, 8221, 7956, 7695, + 7438, 7185, 6936, 6690, 6449, 6211, 5978, 5748, + 5523, 5301, 5084, 4871, 4662, 4458, 4257, 4061, + 3870, 3682, 3499, 3321, 3146, 2977, 2812, 2651, + 2495, 2343, 2196, 2054, 1916, 1783, 1654, 1531, + 1411, 1297, 1187, 1083, 982, 887, 797, 711, + 630, 554, 483, 417, 355, 299, 247, 200, + 158, 121, 89, 62, 40, 23, 10, 3, + 1, +}; /* */ static const prog_Patch init_patch PROGMEM = { @@ -87,8 +183,8 @@ static const prog_Patch init_patch PROGMEM = { MOD_SRC_LFO_1, MOD_DST_PARAMETER_2, 0, MOD_SRC_LFO_2, MOD_DST_MIX_BALANCE, 0, MOD_SRC_LFO_4, MOD_DST_PARAMETER_1, 63, - MOD_SRC_SEQ_1, MOD_DST_PARAMETER_1, 0, - MOD_SRC_SEQ_2, MOD_DST_PARAMETER_2, 0, + MOD_SRC_ENV_4, MOD_DST_PARAMETER_1, 0, + MOD_SRC_ENV_5, MOD_DST_PARAMETER_2, 0, MOD_SRC_ENV_2, MOD_DST_VCA, 32, MOD_SRC_VELOCITY, MOD_DST_VCA, 0, MOD_SRC_PITCH_BEND, MOD_DST_OSC_1_2_COARSE, 0, @@ -100,19 +196,20 @@ static const prog_Patch init_patch PROGMEM = { 0, 0, 0, 0, 0, 0, - // Padding - 0, 0, 0, 0, 0, 0, 0, 0, + // Padding: [0]=feedback, [1]=unused, [2]=engine, [3-7]=unused + 0, 0, ENGINE_FM4OP, 0, 0, 0, 0, 0, + + // Extra envelope-LFO settings (env4-7: ADSR + LFO shape/rate/curve/retrigger) + 0, 40, 20, 60, LFO_WAVEFORM_TRIANGLE, 0, 0, 0, + 0, 40, 20, 60, LFO_WAVEFORM_TRIANGLE, 0, 0, 0, + 0, 40, 20, 60, LFO_WAVEFORM_TRIANGLE, 0, 0, 0, + 0, 40, 20, 60, LFO_WAVEFORM_TRIANGLE, 0, 0, 0, }; -/* static */ -void Voice::set_patch_data(uint8_t address, uint8_t value) { - patch_data_[address + 1] = value; -} - /* static */ void Voice::Init() { pitch_value_ = 0; - for (uint8_t i = 0; i < kNumEnvelopes; ++i) { + for (uint8_t i = 0; i < kNumTotalEnvelopes; ++i) { envelope_[i].Init(); } memset(no_sync_, 0, kAudioBlockSize); @@ -142,7 +239,7 @@ void Voice::ResetAllControllers() { /* static */ void Voice::TriggerEnvelope(uint8_t stage) { - for (uint8_t i = 0; i < kNumEnvelopes; ++i) { + for (uint8_t i = 0; i < kNumTotalEnvelopes; ++i) { TriggerEnvelope(i, stage); } } @@ -155,10 +252,26 @@ void Voice::TriggerEnvelope(uint8_t index, uint8_t stage) { /* static */ void Voice::Trigger(uint16_t note, uint8_t velocity, uint8_t legato) { pitch_target_ = note; + + // Slop: per-note random pitch offset (patch_.padding[1]). + uint8_t slop = patch_.padding[1]; + if (slop > 0) { + int8_t rnd = static_cast(Random::GetByte()); + // Scale: slop=127 gives ±~64 units (about ±0.5 semitone). + int16_t offset = (static_cast(rnd) * slop) >> 7; + pitch_target_ += offset; + } + if (!part_.legato || !legato) { gate_ = 255; TriggerEnvelope(ATTACK); transient_generator.Trigger(); + if (patch_.padding[2] == ENGINE_KS_PLUCK) { + karplus_.Trigger( + patch_.osc[1].shape, // excitation type + patch_.osc[1].parameter, // excitation color + patch_.osc[1].detune); // pluck position + } modulation_sources_[MOD_SRC_VELOCITY] = velocity; modulation_sources_[MOD_SRC_RANDOM] = Random::state_msb(); osc_2.Reset(); @@ -193,9 +306,9 @@ inline void Voice::LoadSources() { // Rescale the value of each modulation sources. Envelopes are in the // 0-16383 range ; just like pitch. All are scaled to 0-255. modulation_sources_[MOD_SRC_NOISE] = Random::GetByte(); - modulation_sources_[MOD_SRC_ENV_1] = envelope_[0].Render(); - modulation_sources_[MOD_SRC_ENV_2] = envelope_[1].Render(); - modulation_sources_[MOD_SRC_ENV_3] = envelope_[2].Render(); + for (uint8_t i = 0; i < kNumTotalEnvelopes; ++i) { + modulation_sources_[MOD_SRC_ENV_1 + i] = envelope_[i].Render(); + } modulation_sources_[MOD_SRC_NOTE] = U14ShiftRight6(pitch_value_); modulation_sources_[MOD_SRC_GATE] = gate_; modulation_sources_[MOD_SRC_LFO_4] = voice_lfo_.Render( @@ -334,18 +447,37 @@ inline void Voice::UpdateDestinations() { int8_t attack_mod = U15ShiftRight7(dst_[MOD_DST_ATTACK]) - 64; int8_t decay_mod = U15ShiftRight7(dst_[MOD_DST_DECAY]) - 64; int8_t release_mod = U15ShiftRight7(dst_[MOD_DST_RELEASE]) - 64; - for (int i = 0; i < kNumEnvelopes; ++i) { + + // Envelope slop: per-voice random ADSR variation. + uint8_t slop = patch_.padding[1]; + int8_t env_slop = 0; + if (slop > 0) { + env_slop = static_cast( + (static_cast(modulation_sources_[MOD_SRC_RANDOM]) - 128) * + slop) >> 6; + } + + for (int i = 0; i < kNumEnvLfoSlots; ++i) { int16_t new_attack = patch_.env_lfo[i].attack; - new_attack = Clip(new_attack + attack_mod, 0, 127); + new_attack = Clip(new_attack + attack_mod + env_slop, 0, 127); int16_t new_decay = patch_.env_lfo[i].decay; - new_decay = Clip(new_decay + decay_mod, 0, 127); + new_decay = Clip(new_decay + decay_mod + env_slop, 0, 127); int16_t new_release = patch_.env_lfo[i].release; - new_release = Clip(new_release + release_mod, 0, 127); + new_release = Clip(new_release + release_mod + env_slop, 0, 127); envelope_[i].Update( new_attack, new_decay, patch_.env_lfo[i].sustain, - new_release); + new_release, + patch_.env_lfo[i].envelope_curve); + } + for (int i = 0; i < kNumExtraEnvelopes; ++i) { + envelope_[kNumEnvLfoSlots + i].Update( + patch_.extra_env_lfo[i].attack, + patch_.extra_env_lfo[i].decay, + patch_.extra_env_lfo[i].sustain, + patch_.extra_env_lfo[i].release, + patch_.extra_env_lfo[i].envelope_curve); } voice_lfo_.set_phase_increment( @@ -367,14 +499,145 @@ inline void Voice::RenderOscillators() { // -0.5 / +0.5 semitones by the vibrato and pitch bend (fine). base_pitch += (dst_[MOD_DST_OSC_1_2_COARSE] - 8192) >> 4; base_pitch += (dst_[MOD_DST_OSC_1_2_FINE] - 8192) >> 7; - + + uint8_t engine = patch_.padding[2]; + + // Reset engine state when engine type changes. + // Only init the active engine — karplus_.Init() clears a 384-byte delay + // line which is too expensive to run unconditionally in the audio ISR. + if (engine != last_engine_) { + last_engine_ = engine; + switch (engine) { + case ENGINE_FM4OP: fm4op_.Init(); break; + case ENGINE_KS_PLUCK: karplus_.Init(); break; + case ENGINE_WESTCOAST: westcoast_.Init(); break; + default: break; + } + } + + // --- 4-op FM mode --- + if (engine == ENGINE_FM4OP) { + uint16_t base_increment = ComputePhaseIncrement(base_pitch); + + // Set up operator phase increments from patch fields. + // Coarse ratio is a TX81Z-style index (0-63) into the ratio table. + fm4op_.SetOperatorIncrement(0, base_increment, + static_cast(patch_.osc[0].range), patch_.osc[0].detune); + fm4op_.SetOperatorIncrement(1, base_increment, + static_cast(patch_.osc[1].range), patch_.osc[1].detune); + fm4op_.SetOperatorIncrement(2, base_increment, + patch_.mix_balance, static_cast(patch_.mix_op)); + fm4op_.SetOperatorIncrement(3, base_increment, + patch_.mix_parameter, static_cast(patch_.mix_sub_osc_shape)); + + // Extract operator waveforms from packed nibbles. + uint8_t op_waveform[4]; + op_waveform[0] = patch_.osc[1].shape & 0x0F; + op_waveform[1] = (patch_.osc[1].shape >> 4) & 0x0F; + op_waveform[2] = patch_.osc[1].parameter & 0x0F; + op_waveform[3] = (patch_.osc[1].parameter >> 4) & 0x0F; + // Clamp to valid range. + for (uint8_t i = 0; i < 4; ++i) { + if (op_waveform[i] >= FM_WAVE_LAST) { + op_waveform[i] = FM_WAVE_SINE; + } + } + + // Operator output levels — TX81Z signal chain: + // 1. Exponential curve on raw patch level (TL → amplitude) + // 2. Multiply by per-op envelope (linear amplitude modulation) + uint8_t op_level[4]; + op_level[0] = patch_.mix_sub_osc; + op_level[1] = patch_.mix_noise; + op_level[2] = patch_.mix_fuzz; + op_level[3] = patch_.mix_crush; + for (uint8_t i = 0; i < 4; ++i) { + uint8_t amp = Fm4Op::LevelToAmplitude(op_level[i]); + op_level[i] = U8U8MulShift8(amp, modulation_sources_[MOD_SRC_ENV_4 + i]); + } + + // Algorithm from osc[0].parameter. + uint8_t algorithm = patch_.osc[0].parameter; + if (algorithm >= FM_ALG_LAST) { + algorithm = FM_ALG_1; + } + + // Feedback from padding[0]. + uint8_t feedback = patch_.padding[0]; + + // Render into buffer. + fm4op_.Render(algorithm, op_waveform, op_level, feedback, + base_increment, buffer_, kAudioBlockSize); + + // Fill osc2 buffer with silence so the mixer doesn't add garbage. + memset(osc2_buffer_, 128, kAudioBlockSize); + return; + } + + // --- Karplus-Strong mode --- + if (engine == ENGINE_KS_PLUCK) { + int16_t ks_pitch = base_pitch + S8U8Mul(patch_.osc[0].range, 128) + + patch_.osc[0].detune; + uint16_t ks_increment = ComputePhaseIncrement(ks_pitch); + karplus_.SetPitch(ks_increment); + + // KS parameters from patch fields. + uint8_t damping = U15ShiftRight7(dst_[MOD_DST_PARAMETER_1]); + uint8_t decay = patch_.osc[1].range; // decay rate + uint8_t body = patch_.mix_balance; // body resonance + // Page 2 params: ensemble + stiffness + feedback. + uint8_t ens_rate = patch_.mix_op; // ensemble rate + uint8_t ens_depth = patch_.mix_parameter; // ensemble depth + uint8_t ens_spread = patch_.mix_sub_osc_shape; // ensemble spread + uint8_t ens_mix = patch_.mix_sub_osc; // ensemble wet/dry + uint8_t stiffness = patch_.mix_noise; // string stiffness + uint8_t feedback = patch_.mix_fuzz; // extra feedback + karplus_.Render(damping, decay, body, + ens_rate, ens_depth, ens_spread, ens_mix, + stiffness, feedback, + buffer_, kAudioBlockSize); + + memset(osc2_buffer_, 128, kAudioBlockSize); + return; + } + + // --- West Coast mode --- + if (engine == ENGINE_WESTCOAST) { + int16_t wc_pitch = base_pitch + S8U8Mul(patch_.osc[0].range, 128) + + patch_.osc[0].detune; + uint16_t wc_increment = ComputePhaseIncrement(wc_pitch); + + uint8_t fold = U15ShiftRight7(dst_[MOD_DST_PARAMETER_1]); + uint8_t env_val = modulation_sources_[MOD_SRC_ENV_1]; + westcoast_.Render( + patch_.osc[1].shape, // base waveform (sin/tri) + fold, // fold depth (modulatable) + patch_.osc[1].parameter, // symmetry + patch_.mix_balance, // bias + patch_.osc[1].range, // FM depth + patch_.osc[1].detune, // FM ratio + patch_.mix_op, // drive + patch_.mix_parameter, // color + patch_.mix_sub_osc_shape, // fold stages + patch_.mix_sub_osc, // input gain + patch_.mix_noise, // env-to-fold + patch_.mix_fuzz, // sub level + patch_.mix_crush, // sync amount + env_val, // envelope value for env-to-fold + wc_increment, + buffer_, + kAudioBlockSize); + + memset(osc2_buffer_, 128, kAudioBlockSize); + return; + } + + // --- Normal oscillator rendering --- // Update the oscillator parameters. for (uint8_t i = 0; i < kNumOscillators; ++i) { int16_t pitch = base_pitch; // -36 / +36 semitones by the range controller. - if (patch_.osc[i].shape != WAVEFORM_FM) { - pitch += S8U8Mul(patch_.osc[i].range, 128); - } + pitch += S8U8Mul(patch_.osc[i].range, 128); // -1 / +1 semitones by the detune controller. pitch += patch_.osc[i].detune; // -16 / +16 semitones by the routed modulations. @@ -442,12 +705,21 @@ void Voice::ProcessBlock() { } RenderOscillators(); + + uint8_t is_fm4op = (patch_.padding[2] == ENGINE_FM4OP); + uint8_t is_special = is_fm4op || + (patch_.padding[2] == ENGINE_KS_PLUCK) || + (patch_.padding[2] == ENGINE_WESTCOAST); + + // In FM4OP mode, skip oscillator mixing and sub-osc (those patch fields + // are reinterpreted as FM parameters). + if (!is_special) { uint8_t op = patch_.mix_op; uint8_t osc_2_gain = U14ShiftRight6(dst_[MOD_DST_MIX_BALANCE]); uint8_t osc_1_gain = ~osc_2_gain; uint8_t wet_gain = U14ShiftRight6(dst_[MOD_DST_MIX_PARAM]); uint8_t dry_gain = ~wet_gain; - + // Mix oscillators. switch (op) { case OP_RING_MOD: @@ -512,33 +784,43 @@ void Voice::ProcessBlock() { sub_gain <<= 1; transient_generator.Render(patch_.mix_sub_osc_shape, buffer_, sub_gain); } + } // end if (!is_fm4op) - uint8_t noise = Random::state_msb(); - uint8_t noise_gain = U15ShiftRight7(dst_[MOD_DST_MIX_NOISE]); - uint8_t signal_gain = ~noise_gain; - wet_gain = U14ShiftRight6(dst_[MOD_DST_MIX_FUZZ]); - dry_gain = ~wet_gain; - - // Mix with noise, and apply distortion. The loop processes samples by 2 to - // avoid some of the overhead of audio_buffer.Overwrite() - for (uint8_t i = 0; i < kAudioBlockSize;) { - uint8_t signal_noise_a, signal_noise_b; - noise = (noise * 73) + 1; - signal_noise_a = U8Mix(buffer_[i++], noise, signal_gain, noise_gain); - uint8_t a = U8Mix( - signal_noise_a, - ResourcesManager::Lookup( - wav_res_distortion, signal_noise_a), - dry_gain, wet_gain); - - noise = (noise * 73) + 1; - signal_noise_b = U8Mix(buffer_[i++], noise, signal_gain, noise_gain); - uint8_t b = U8Mix( - signal_noise_b, + // Post-mix processing. + if (is_special) { + // In FM4OP/KS mode, skip noise/fuzz post-processing. + // Write directly to audio buffer. + for (uint8_t i = 0; i < kAudioBlockSize; i += 2) { + audio_buffer.Overwrite2(buffer_[i], buffer_[i + 1]); + } + } else { + uint8_t noise = Random::state_msb(); + uint8_t noise_gain = U15ShiftRight7(dst_[MOD_DST_MIX_NOISE]); + uint8_t signal_gain = ~noise_gain; + uint8_t post_wet = U14ShiftRight6(dst_[MOD_DST_MIX_FUZZ]); + uint8_t post_dry = ~post_wet; + + // Mix with noise, and apply distortion. The loop processes samples by 2 to + // avoid some of the overhead of audio_buffer.Overwrite() + for (uint8_t i = 0; i < kAudioBlockSize;) { + uint8_t signal_noise_a, signal_noise_b; + noise = (noise * 73) + 1; + signal_noise_a = U8Mix(buffer_[i++], noise, signal_gain, noise_gain); + uint8_t a = U8Mix( + signal_noise_a, ResourcesManager::Lookup( - wav_res_distortion, signal_noise_b), - dry_gain, wet_gain); - audio_buffer.Overwrite2(a, b); + wav_res_distortion, signal_noise_a), + post_dry, post_wet); + + noise = (noise * 73) + 1; + signal_noise_b = U8Mix(buffer_[i++], noise, signal_gain, noise_gain); + uint8_t b = U8Mix( + signal_noise_b, + ResourcesManager::Lookup( + wav_res_distortion, signal_noise_b), + post_dry, post_wet); + audio_buffer.Overwrite2(a, b); + } } } diff --git a/voicecard/voice.h b/voicecard/voice.h index 4d9c82a..1fea07f 100755 --- a/voicecard/voice.h +++ b/voicecard/voice.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -24,6 +24,9 @@ #include "common/patch.h" #include "voicecard/envelope.h" +#include "voicecard/fm4op.h" +#include "voicecard/karplus.h" +#include "voicecard/westcoast.h" namespace ambika { @@ -33,6 +36,22 @@ static const int16_t kHighestNote = 120 * 128; static const int16_t kOctave = 12 * 128; static const int16_t kPitchTableStart = 116 * 128; +static inline uint16_t ComputePhaseIncrement(int16_t pitch) { + if (pitch >= kHighestNote) pitch = kHighestNote; + int16_t ref_pitch = pitch - kPitchTableStart; + uint8_t num_shifts = 0; + while (ref_pitch < 0) { + ref_pitch += kOctave; + ++num_shifts; + } + uint16_t increment = ResourcesManager::Lookup( + lut_res_oscillator_increments, ref_pitch >> 1); + while (num_shifts--) { + increment >>= 1; + } + return increment; +} + // This mirrors the beginning of the Part data structure in the controller. struct Part { uint8_t volume; @@ -55,6 +74,12 @@ class Voice { // Move this voice to the release stage. static void Kill() { TriggerEnvelope(DEAD); } + // Force engine re-init on next render (called after bulk patch load). + // The actual Init() runs in RenderOscillators for the active engine only. + static void ResetEngines() { + last_engine_ = 0xFF; + } + static void ProcessBlock(); // Called whenever a write to the CV analog outputs has to be made. @@ -81,13 +106,14 @@ class Voice { modulation_sources_[i] = value; } - static void set_patch_data(uint8_t address, uint8_t value); + static void set_patch_data(uint8_t address, uint8_t value) { + patch_data_[address] = value; + } static void set_part_data(uint8_t address, uint8_t value) { - part_data_[address + 1] = value; + part_data_[address] = value; } - static uint8_t* mutable_patch_data() { - return &patch_data_[1]; + return patch_data_; } static const Patch& patch() { return patch_; } @@ -104,13 +130,13 @@ class Voice { static inline void UpdateDestinations() __attribute__((always_inline)); static inline void RenderOscillators() __attribute__((always_inline)); - static uint8_t patch_data_[1]; + static uint8_t *patch_data_; static Patch patch_; - static uint8_t part_data_[1]; + static uint8_t *part_data_; static Part part_; // Envelope generators. - static Envelope envelope_[kNumEnvelopes]; + static Envelope envelope_[kNumTotalEnvelopes]; static uint8_t gate_; static Lfo voice_lfo_; static uint8_t modulation_sources_[kNumModulationSources]; @@ -130,6 +156,11 @@ class Voice { static uint8_t no_sync_[kAudioBlockSize]; static uint8_t dummy_sync_state_[kAudioBlockSize]; + static Fm4Op fm4op_; + static KarplusStrong karplus_; + static WestCoast westcoast_; + static uint8_t last_engine_; + DISALLOW_COPY_AND_ASSIGN(Voice); }; diff --git a/voicecard/voicecard.cc b/voicecard/voicecard.cc index 73f815c..fb67055 100644 --- a/voicecard/voicecard.cc +++ b/voicecard/voicecard.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/voicecard.h b/voicecard/voicecard.h index a267e9d..dec23de 100644 --- a/voicecard/voicecard.h +++ b/voicecard/voicecard.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -32,7 +32,7 @@ static const uint8_t kControlRate = 40; // The latency is 1ms, with a buffer storing 4ms of audio. static const uint8_t kAudioBlockSize = kControlRate; -const uint8_t kSystemVersion = 0x10; +const uint8_t kSystemVersion = 0x25; static uint8_t* kFirmwareUpdateFlagPtr = (uint8_t*)(E2END); diff --git a/voicecard/voicecard_rx.cc b/voicecard/voicecard_rx.cc index f7b5c4a..754e9d4 100755 --- a/voicecard/voicecard_rx.cc +++ b/voicecard/voicecard_rx.cc @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/voicecard/voicecard_rx.h b/voicecard/voicecard_rx.h index 290305a..cad968d 100755 --- a/voicecard/voicecard_rx.h +++ b/voicecard/voicecard_rx.h @@ -1,6 +1,6 @@ -// Copyright 2011 Olivier Gillet. +// Copyright 2011 Emilie Gillet. // -// Author: Olivier Gillet (ol.gillet@gmail.com) +// Author: Emilie Gillet (emilie.o.gillet@gmail.com) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -117,6 +117,10 @@ class VoicecardProtocolRx { case COMMAND_RETRIGGER_ENVELOPE: case COMMAND_RETRIGGER_ENVELOPE + 1: case COMMAND_RETRIGGER_ENVELOPE + 2: + case COMMAND_RETRIGGER_ENVELOPE + 3: + case COMMAND_RETRIGGER_ENVELOPE + 4: + case COMMAND_RETRIGGER_ENVELOPE + 5: + case COMMAND_RETRIGGER_ENVELOPE + 6: voice.TriggerEnvelope(command_ & 0x0f, ATTACK); break; case COMMAND_RESET_ALL_CONTROLLERS: @@ -136,10 +140,15 @@ class VoicecardProtocolRx { // Stop doing anything else Timer<2>::Stop(); uint8_t size = spi_.Read(); + if (size > sizeof(Patch)) { + size = sizeof(Patch); + } uint8_t* data = data_ptr_ = voice.mutable_patch_data(); while (size--) { *data++ = spi_.Read(); } + // Reset synthesis engine state after receiving a new patch. + voice.ResetEngines(); Timer<2>::Start(); } break; diff --git a/voicecard/westcoast.h b/voicecard/westcoast.h new file mode 100644 index 0000000..2439df4 --- /dev/null +++ b/voicecard/westcoast.h @@ -0,0 +1,229 @@ +// Copyright 2026 Ambika contributors. +// +// West coast (Buchla-style) oscillator with wavefolder and FM. + +#ifndef VOICECARD_WESTCOAST_H_ +#define VOICECARD_WESTCOAST_H_ + +#include "avrlib/base.h" +#include "avrlib/op.h" +#include "voicecard/fm4op.h" +#include "voicecard/resources.h" + +using namespace avrlib; + +namespace ambika { + +enum WcWaveform { + WC_WAVE_SINE, + WC_WAVE_TRIANGLE, + WC_WAVE_LAST +}; + +// Patch field mapping for west coast mode: +// Page 1: +// osc[0].shape (0) = WAVEFORM_WESTCOAST +// osc[0].parameter (1) = fold depth +// osc[0].range (2) = pitch range +// osc[0].detune (3) = fine tune +// osc[1].shape (4) = base waveform +// osc[1].parameter (5) = symmetry +// osc[1].range (6) = FM depth +// osc[1].detune (7) = FM ratio +// mix_balance (8) = bias +// mix_op (9) = drive +// mix_parameter (10) = color +// Page 2: +// mix_sub_osc_shape(11) = fold stages (1-6) +// mix_sub_osc (12) = input gain +// mix_noise (13) = envelope-to-fold amount +// mix_fuzz (14) = sub-harmonic level +// mix_crush (15) = sync amount +// padding[1] (105)= pitch range (moved) + +class WestCoast { + public: + WestCoast() { } + + void Init() { + phase_ = 0; + mod_phase_ = 0; + sub_phase_ = 0; + sync_phase_ = 0; + lp_state1_ = 128; + lp_state2_ = 128; + } + + // Buchla-style wavefolder. + // Quadratic gain curve: gentle at low settings, extreme at high. + // fold_depth 0 = clean sine, 64 = ~4 folds, 127 = ~16 folds. + static inline uint8_t Fold(int16_t input, uint8_t fold_depth, + uint8_t symmetry, uint8_t bias) { + // Bias and symmetry: DC offsets that shift the fold point. + // Symmetry creates even harmonics, bias shifts the waveform. + input += static_cast(bias - 64); + if (symmetry != 64) { + input += (static_cast(symmetry) - 64); + } + + // Quadratic gain: matches real wavefolder knob feel. + // 0→1x, 32→~3x, 64→~9x, 96→~18x, 127→~33x + if (fold_depth > 0) { + uint16_t gain = 128 + (static_cast(fold_depth) * fold_depth >> 2); + int32_t g = static_cast(input) * gain; + input = g >> 7; + } + + // Iterative fold: reflect at ±127 boundaries. + // Each reflection adds harmonic content. + for (uint8_t i = 0; i < 16; ++i) { + if (input > 127) { + input = 254 - input; + } else if (input < -128) { + input = -256 - input; + } else { + break; + } + } + + if (input > 127) input = 127; + if (input < -128) input = -128; + + return static_cast(input + 128); + } + + void Render( + uint8_t base_waveform, + uint8_t fold_depth, + uint8_t symmetry, + uint8_t bias, + uint8_t fm_depth, + int8_t fm_ratio, + uint8_t drive, + uint8_t color, + uint8_t fold_stages, + uint8_t input_gain, + uint8_t env_to_fold, + uint8_t sub_level, + uint8_t sync_amount, + uint8_t env_value, + uint16_t phase_increment, + uint8_t* buffer, + uint8_t size) { + + // FM modulator increment. + uint16_t mod_increment; + if (fm_ratio <= 0) { + uint8_t shift = 1 - fm_ratio; + if (shift > 15) shift = 15; + mod_increment = phase_increment >> shift; + } else if (fm_ratio == 1) { + mod_increment = phase_increment; + } else { + mod_increment = phase_increment * static_cast(fm_ratio); + } + + // Sub-oscillator: one octave down. + uint16_t sub_increment = phase_increment >> 1; + + // Combine fold depth with drive, input gain, and envelope. + uint16_t effective_fold = fold_depth; + // Drive and input gain boost the fold amount. + effective_fold += (drive >> 1) + (input_gain >> 1); + // Envelope-to-fold: the classic Buchla trick. + // Sweeping fold with an envelope creates the plucked timbre. + if (env_to_fold > 0) { + effective_fold += (static_cast(env_value) * env_to_fold) >> 8; + } + if (effective_fold > 127) effective_fold = 127; + + // Sync increment (for self-sync, faster than fundamental). + uint16_t sync_increment = 0; + if (sync_amount > 0) { + sync_increment = phase_increment + + ((static_cast(phase_increment) * sync_amount) >> 5); + } + + // Color: 2-pole low-pass (12dB/oct) for steep rolloff after folder. + // 0 = very dark (only fundamental), 127 = bright (all harmonics). + // Min coeff 16 ensures fundamental passes through at darkest setting. + uint16_t lp16 = 16 + (static_cast(color) << 1); + uint8_t lp_coeff = (lp16 > 255) ? 255 : lp16; + + while (size--) { + // FM modulation. + mod_phase_ += mod_increment; + uint16_t fm_mod = 0; + if (fm_depth > 0) { + uint16_t mod_val = InterpolateSine16(mod_phase_); + fm_mod = (static_cast(mod_val - 32768) * fm_depth) >> 8; + } + + // Self-sync: reset phase when sync oscillator wraps. + if (sync_amount > 0) { + uint16_t old_sync = sync_phase_; + sync_phase_ += sync_increment; + if (sync_phase_ < old_sync) { + phase_ = 0; + } + } + + // Base oscillator. + phase_ += phase_increment; + int16_t sample; + + if (base_waveform == WC_WAVE_TRIANGLE) { + uint16_t tri_phase = phase_ + fm_mod; + if (tri_phase & 0x8000) { + sample = static_cast(tri_phase >> 7) - 128; + } else { + sample = 127 - static_cast(tri_phase >> 7); + } + } else { + uint16_t sine = InterpolateSine16(phase_ + fm_mod); + sample = static_cast(sine >> 8) - 128; + } + + // Apply input gain to the raw waveform before folding. + if (input_gain > 0) { + sample = (sample * (128 + input_gain)) >> 7; + } + + // Wavefolder. + uint8_t folded = Fold(sample, effective_fold, symmetry, bias); + + // Post-fold color: 2-pole IIR for steep rolloff (12dB/oct). + if (color < 124) { + // Pole 1 + lp_state1_ += (static_cast(folded - lp_state1_) * lp_coeff) >> 8; + // Pole 2 + lp_state2_ += (static_cast(lp_state1_ - lp_state2_) * lp_coeff) >> 8; + folded = lp_state2_; + } + + // Mix in sub-harmonic. + if (sub_level > 0) { + sub_phase_ += sub_increment; + uint8_t sub = InterpolateSample(wav_res_sine, sub_phase_); + folded = ((uint16_t)folded * (256 - sub_level) + + (uint16_t)sub * sub_level) >> 8; + } + + *buffer++ = folded; + } + } + + private: + uint16_t phase_; + uint16_t mod_phase_; + uint16_t sub_phase_; + uint16_t sync_phase_; + uint8_t lp_state1_; + uint8_t lp_state2_; + + DISALLOW_COPY_AND_ASSIGN(WestCoast); +}; + +} // namespace ambika + +#endif // VOICECARD_WESTCOAST_H_