Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 109 additions & 12 deletions releases/84_CosmikC1zzl3/C1ZZL3.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -229,15 +229,22 @@ class C1ZZL3 : public ComputerCard

updateTuringClockState(false);

bool pulse2Trigger = PulseIn2RisingEdge();
bool pulse2GateHigh = PulseIn2();
bool pulse2Trigger = pulse2GateHigh && !pulse2GateWasHigh;
pulse2GateWasHigh = pulse2GateHigh;

if (pulse2Trigger)
{
if (envelopePreset != (uint8_t)EnvelopePreset::Off)
{
syncOscillators();
triggerEnvelope();
triggerEnvelope(true, true);
}
}
else if (pulse2EnvelopeHolding && !pulse2GateHigh)
{
requestEnvelopeRelease();
}

outputSynthVoice(freq, pd, wave, ring, noiseAmt);

Expand Down Expand Up @@ -322,14 +329,22 @@ class C1ZZL3 : public ComputerCard
static constexpr uint8_t EnvelopePresetCount = 9;
static constexpr uint8_t CustomEnvelopePreset = EnvelopePresetCount;
static constexpr uint8_t CustomEnvelopeSlotCount = 8;
static constexpr uint8_t EnvelopeLoopStartStage = 2;
static constexpr uint8_t EnvelopeLoopEndStage = 5;
static constexpr uint16_t EnvelopeLoopLevelThreshold = 128;
static constexpr uint32_t EnvelopeLoopMinStageSamples = 960u;
static constexpr uint32_t MinWebMidiEnvelopeSamples = 960u;
static constexpr uint32_t MaxWebMidiStageSamples = 192000u;
static constexpr uint32_t StartupSelectDelaySamples = 12000u;
static constexpr uint32_t StartupSelectWindowSamples = 24000u;
static constexpr uint32_t SaveMagic = 0x43315A33u; // C1Z3
static constexpr uint16_t SaveVersion = 3;
static constexpr int32_t OutputLowpassAlphaQ12 = 2458; // 7 kHz at 48 kHz.
static constexpr int32_t OutputLowpassAlphaQ12 = 2008; // ~5.2 kHz at 48 kHz.
static constexpr int32_t OutputHighpassAlphaQ12 = 4075; // 40 Hz at 48 kHz.
static constexpr int32_t PdCompensationFloorQ12 = 3000; // Keep high-PD tones present but less inflated.
static constexpr int32_t HighPitchSofteningStart = 28000000;
static constexpr int32_t HighPitchSofteningRange = 42000000;
static constexpr int32_t HighPitchSofteningMaxQ12 = 1200;
static constexpr uint32_t SaveFlashOffset =
(PICO_FLASH_SIZE_BYTES - FLASH_SECTOR_SIZE) &
~(FLASH_SECTOR_SIZE - 1u);
Expand Down Expand Up @@ -482,6 +497,7 @@ class C1ZZL3 : public ComputerCard
osc1 = mix(osc1, ringSig, ringMix);

int32_t ampScale = envelopeAmpScale(envelopeLevel);
ampScale = (ampScale * pdCompensationScale(pd)) >> 12;
ampScale = (ampScale * updateSyncFade()) >> 12;
osc1 = (osc1 * ampScale) >> 12;
osc2 = (osc2 * ampScale) >> 12;
Expand Down Expand Up @@ -537,6 +553,7 @@ class C1ZZL3 : public ComputerCard

int32_t sine = getSine(renderPhase);
int32_t target = morphWave(renderPhase, wave);
target = softenHighPitchTarget(sine, target, freq, pdCurve);

return mix(sine, target, pdCurve);
}
Expand All @@ -558,7 +575,7 @@ class C1ZZL3 : public ComputerCard
phase += (uint32_t)phaseJitter;
}

void triggerEnvelope()
void triggerEnvelope(bool held = false, bool heldByPulse = false)
{
if (envelopePreset == (uint8_t)EnvelopePreset::Off)
return;
Expand All @@ -577,6 +594,9 @@ class C1ZZL3 : public ComputerCard
pdEnvelopeStartLevel = pdStartLevel;
pdEnvelopeLevel = pdStartLevel;
envelopeActive = true;
envelopeHeld = held;
envelopeReleaseRequested = !held;
pulse2EnvelopeHolding = heldByPulse;
}

void triggerTuringEnvelope()
Expand All @@ -591,24 +611,34 @@ class C1ZZL3 : public ComputerCard
return 0;

const EnvelopeProgram& program = envelopeProgram();
bool loopEnabled = envelopeLoopEnabled(program);

bool ampDone = updateEnvelopeRunner(
program.amp,
ampEnvelopeStage,
ampEnvelopeSample,
ampEnvelopeLevel,
ampEnvelopeStartLevel);
ampEnvelopeStartLevel,
envelopeHeld,
envelopeReleaseRequested,
loopEnabled);

bool pdDone = updateEnvelopeRunner(
program.pd,
pdEnvelopeStage,
pdEnvelopeSample,
pdEnvelopeLevel,
pdEnvelopeStartLevel);
pdEnvelopeStartLevel,
envelopeHeld,
envelopeReleaseRequested,
loopEnabled);

if (ampDone && pdDone)
{
envelopeActive = false;
envelopeHeld = false;
envelopeReleaseRequested = false;
pulse2EnvelopeHolding = false;
if (midiNoteReleased)
midiNoteActive = false;
}
Expand Down Expand Up @@ -670,7 +700,10 @@ class C1ZZL3 : public ComputerCard
if (envelopePreset == (uint8_t)EnvelopePreset::Off || !envelopeActive)
midiNoteActive = false;
else
{
midiNoteReleased = true;
requestEnvelopeRelease();
}
}
return;
}
Expand Down Expand Up @@ -736,7 +769,7 @@ class C1ZZL3 : public ComputerCard
if (envelopePreset != (uint8_t)EnvelopePreset::Off)
{
syncOscillators();
triggerEnvelope();
triggerEnvelope(true);
}
}

Expand Down Expand Up @@ -1096,11 +1129,11 @@ class C1ZZL3 : public ComputerCard
}};

static const EnvelopeProgram brass = {{
{4095, 4800}, {3400, 30000}, {0, 18000}, {0, 1},
{0, 1}, {0, 1}, {0, 1}, {0, 1}
{4095, 4800}, {3400, 30000}, {3200, 18000}, {3000, 18000},
{3300, 18000}, {3100, 18000}, {1600, 18000}, {0, 24000}
}, {
{1792, 4800}, {900, 30000}, {0, 18000}, {0, 1},
{0, 1}, {0, 1}, {0, 1}, {0, 1}
{1792, 4800}, {900, 30000}, {1200, 18000}, {1000, 18000},
{1300, 18000}, {900, 18000}, {500, 18000}, {0, 24000}
}};

static const EnvelopeProgram strings = {{
Expand Down Expand Up @@ -1163,11 +1196,17 @@ class C1ZZL3 : public ComputerCard
uint8_t& stage,
uint32_t& sample,
int32_t& level,
int32_t& startLevel)
int32_t& startLevel,
bool held,
bool releaseRequested,
bool loopEnabled)
{
if (stage >= 8)
return true;

if (loopEnabled && stage > EnvelopeLoopEndStage && held && !releaseRequested)
stage = EnvelopeLoopStartStage;

uint32_t time = stages[stage].time;
if (time == 0)
time = 1;
Expand All @@ -1182,11 +1221,38 @@ class C1ZZL3 : public ComputerCard
stage++;
sample = 0;
startLevel = level;

if (loopEnabled && stage > EnvelopeLoopEndStage && held && !releaseRequested)
stage = EnvelopeLoopStartStage;
}

return stage >= 8;
}

bool envelopeLoopEnabled(const EnvelopeProgram& program)
{
return laneHasLoopBody(program.amp) || laneHasLoopBody(program.pd);
}

bool laneHasLoopBody(const EnvelopeStage* stages)
{
for (uint32_t i = EnvelopeLoopStartStage; i <= EnvelopeLoopEndStage; ++i)
{
if (stages[i].level >= EnvelopeLoopLevelThreshold &&
stages[i].time >= EnvelopeLoopMinStageSamples)
return true;
}

return false;
}

void requestEnvelopeRelease()
{
envelopeHeld = false;
envelopeReleaseRequested = true;
pulse2EnvelopeHolding = false;
}

inline int32_t morphWave(uint32_t phase, int32_t wave)
{
uint32_t scaled = ((uint32_t)wave * 7u);
Expand Down Expand Up @@ -2112,6 +2178,33 @@ class C1ZZL3 : public ComputerCard
return (ux * ux) >> 12;
}

int32_t pdCompensationScale(int32_t pd)
{
int32_t pdCurve = responseCurve(pd);
int32_t reduction = (pdCurve * (4095 - PdCompensationFloorQ12)) >> 12;
return 4095 - reduction;
}

int32_t softenHighPitchTarget(
int32_t sine,
int32_t target,
int32_t freq,
int32_t pdCurve)
{
int32_t pitchAmount = freq - HighPitchSofteningStart;
if (pitchAmount <= 0 || pdCurve <= 0)
return target;

if (pitchAmount > HighPitchSofteningRange)
pitchAmount = HighPitchSofteningRange;

int32_t pitchCurve = (pitchAmount << 12) / HighPitchSofteningRange;
int32_t soften =
(((pitchCurve * pdCurve) >> 12) * HighPitchSofteningMaxQ12) >> 12;

return mix(target, sine, soften);
}

uint32_t smoothStep12(uint32_t x)
{
uint32_t x2 = (x * x) >> 12;
Expand Down Expand Up @@ -2153,6 +2246,10 @@ class C1ZZL3 : public ComputerCard
uint8_t ampEnvelopeStage = 8;
uint8_t pdEnvelopeStage = 8;
bool envelopeActive = false;
bool envelopeHeld = false;
bool envelopeReleaseRequested = true;
bool pulse2GateWasHigh = false;
bool pulse2EnvelopeHolding = false;
uint8_t envelopePreset = 0;
uint32_t startupSelectSamples = 0;
uint32_t envelopeSelectHoldSamples = 0;
Expand Down
13 changes: 9 additions & 4 deletions releases/84_CosmikC1zzl3/CARD_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ Put the switch in the middle.
- `Audio/CV In 1` adds pitch at 1V/oct.
- `CV In 1` adds phase distortion.
- `CV In 2` adds wave control.
- `Pulse In 2` triggers the selected envelope and oscillator sync.
- `Pulse In 2` triggers the selected envelope, loops supported envelopes while
the gate is high, exits the loop when the gate falls, and syncs the
oscillators.

The Turing CV and pulse outputs continue running in synth mode, so they can be
used while playing the synth.
Expand Down Expand Up @@ -78,7 +80,11 @@ MIDI CC controls on the selected input channel:
Physical knobs use pickup after MIDI changes, so values do not jump until the
knob is swept through the current setting.

## Web MIDI Editor
MIDI note-on behaves like a held gate. It triggers the selected envelope and
keeps loop-capable envelopes cycling until MIDI note-off lets the envelope
complete naturally.

## Envelope Lab

Hosted editor:

Expand All @@ -94,7 +100,7 @@ choose the C1ZZL3 output, then:
3. Drag points on the graph to change both level and timing.
4. Watch the point numbers. Matching numbers mean the stages are stacked at the same spot.
5. Use the tables below the graph for exact values when you want precise edits.
6. Use the action buttons on the right when you want to send, save, read, or export.
6. Use the action buttons on the right when you want to send, save, read, or reset.

Button quick reference:

Expand All @@ -106,7 +112,6 @@ Button quick reference:
- `Read Envelopes from Card` to load saved card envelopes into the editor.
- `Read Settings from Card` to load the current card settings into the editor.
- `Send Settings` to send performance settings until reset.
- `Export JSON` to download all editor presets.
- `Reset Preset` to restore the selected preset to its factory value.

The card can save up to eight custom envelopes. Factory presets are not
Expand Down
Loading