diff --git a/releases/74_Wild_Pebble/README.md b/releases/74_Wild_Pebble/README.md index 8a80e77e3..9355970f1 100644 --- a/releases/74_Wild_Pebble/README.md +++ b/releases/74_Wild_Pebble/README.md @@ -23,9 +23,9 @@ Wild Pebble listens for USB MIDI realtime clock messages. * MIDI Continue `0xFB` resumes MIDI clock following without resetting the step. * MIDI Stop `0xFC` stops MIDI clock following and releases any active MIDI note. -MIDI clock is 24 PPQN. Wild Pebble advances one sequencer step every 6 MIDI clock ticks, so each step behaves as a 16th note. +MIDI clock is 24 PPQN. Wild Pebble advances one sequencer step every 12 MIDI clock ticks, so incoming MIDI clock drives the sequencer at an 8th-note step feel. -When MIDI clock is active it overrides the internal clock, in the same spirit as the pulse clock input. If no MIDI or pulse clock is active, the Main knob controls the internal clock speed. +When MIDI clock is active it overrides the internal clock. If Pulse Input 1 is active and running faster than MIDI, the pulse clock takes priority. If no MIDI or pulse clock is active, the Main knob controls the internal clock speed. ## MIDI Note Output @@ -33,10 +33,12 @@ The generated melody is sent over USB MIDI as note events. * MIDI channel: 1 * Note source: the current generated `currentMIDINote` -* Gate source: Pulse Output 1 -* Velocity: derived from the current internal energy value +* Trigger source: Pulse Output 1 +* Velocity: shaped by internal energy and tension +* Note length: shaped by the active clock period, energy, and tension +* Retriggers: become more likely as energy and tension rise -Each Pulse Output 1 gate produces a MIDI note on, followed by a note off when the gate ends. The analogue CV and pulse outputs continue to work as in the original card. +This keeps the MIDI line feeling connected to the card's internal motion instead of acting like a rigid copy of the raw pulse width. The analogue CV and pulse outputs continue to work as in the original card. --- @@ -135,15 +137,15 @@ CV Input 2 modulates mutation amount. ### Up -Stable melodic motion, restrained mutation, slower harmonic movement, and tighter rhythms. +Steady mode. Stable melodic motion, restrained mutation, slower harmonic movement, and tighter rhythms. ### Middle -Balanced mutation, moderate swing, evolving melodic variation, and gradual harmonic drift. +Drift mode. Balanced mutation, moderate swing, evolving melodic variation, and gradual harmonic drift. ### Down -Aggressive mutation, strongest swing, wider melodic jumps, more active scale changes, and denser companion rhythms. +Surge mode. Aggressive mutation, strongest swing, wider melodic jumps, more active scale changes, and denser companion rhythms. --- diff --git a/releases/74_Wild_Pebble/UF2/WildPebble.uf2 b/releases/74_Wild_Pebble/UF2/WildPebble.uf2 index c38aad4c8..341404802 100644 Binary files a/releases/74_Wild_Pebble/UF2/WildPebble.uf2 and b/releases/74_Wild_Pebble/UF2/WildPebble.uf2 differ diff --git a/releases/74_Wild_Pebble/WildPebble.cpp b/releases/74_Wild_Pebble/WildPebble.cpp index 82f046058..36f589277 100644 --- a/releases/74_Wild_Pebble/WildPebble.cpp +++ b/releases/74_Wild_Pebble/WildPebble.cpp @@ -42,7 +42,7 @@ static constexpr uint8_t kMidiStop = 0xFC; static constexpr uint8_t kMidiNoteOff = 0x80; static constexpr uint8_t kMidiNoteOn = 0x90; static constexpr uint8_t kMidiChannel = 0; -static constexpr uint8_t kMidiClocksPerStep = 6; +static constexpr uint8_t kMidiClocksPerStep = 12; volatile uint8_t gMidiClockTicksPending = 0; volatile uint8_t gMidiStartPending = 0; @@ -123,11 +123,19 @@ class WildPebble : public ComputerCard uint32_t internalClockPeriod = 4000; bool externalClockActive = false; - uint32_t externalClockTimeout = 0; + uint32_t pulseClockTimeout = 0; + uint32_t midiClockTimeout = 0; + uint32_t lastPulseClockSample = 0; + uint32_t lastMidiStepSample = 0; + uint32_t pulseClockPeriod = 0xFFFFFFFF; + uint32_t midiClockPeriod = 0xFFFFFFFF; bool midiClockRunning = false; uint8_t midiClockDivider = 0; bool midiNoteGateActive = false; uint8_t midiLastNote = 48; + uint32_t midiNoteOffSample = 0; + uint32_t midiNoteOnSample = 0; + uint32_t selectedClockPeriod = 4000; int32_t tension = 64; bool tensionRising = true; @@ -515,6 +523,107 @@ class WildPebble : public ComputerCard } } + uint32_t ActiveStepPeriod() const + { + if(selectedClockPeriod > 0) + { + return selectedClockPeriod; + } + + return internalClockPeriod; + } + + uint8_t MidiVelocity() const + { + int32_t velocity = 28 + (currentEnergy >> 1) + (tension >> 2); + + if(velocity > 127) + { + velocity = 127; + } + + if(velocity < 24) + { + velocity = 24; + } + + return (uint8_t)velocity; + } + + uint32_t MidiNoteLength() const + { + uint32_t stepPeriod = ActiveStepPeriod(); + uint32_t shape = (uint32_t)(80 + (currentEnergy >> 1) + (tension >> 2)); + + if(shape > 240) + { + shape = 240; + } + + uint32_t noteLength = (stepPeriod * shape) >> 8; + + if(noteLength < 120) + { + noteLength = 120; + } + + return noteLength; + } + + bool ShouldRetriggerMidiNote() + { + if(!midiNoteGateActive) + { + return true; + } + + uint32_t minGap = ActiveStepPeriod() >> 3; + + if(minGap < 120) + { + minGap = 120; + } + + if((sampleCounter - midiNoteOnSample) < minGap) + { + return false; + } + + uint32_t retriggerChance = (uint32_t)((currentEnergy >> 1) + (tension >> 2)); + + if(retriggerChance > 220) + { + retriggerChance = 220; + } + + return (Random() & 255) < retriggerChance; + } + + void TriggerMidiNote() + { + if(!ShouldRetriggerMidiNote()) + { + return; + } + + if(midiNoteGateActive) + { + QueueMidiMessage(kMidiNoteOff | kMidiChannel, + midiLastNote, + 0); + } + + midiLastNote = (uint8_t)currentMIDINote; + + QueueMidiMessage(kMidiNoteOn | kMidiChannel, + midiLastNote, + MidiVelocity()); + + midiNoteGateActive = true; + midiNoteOnSample = sampleCounter; + midiNoteOffSample = sampleCounter + MidiNoteLength(); + } + bool ConsumeMidiClock() { bool clockEvent = false; @@ -526,16 +635,13 @@ class WildPebble : public ComputerCard midiClockDivider = 0; currentStep = -1; clockCounter = 0; - externalClockActive = true; - externalClockTimeout = 48000; + lastMidiStepSample = sampleCounter; } if(gMidiContinuePending > 0) { gMidiContinuePending--; midiClockRunning = true; - externalClockActive = true; - externalClockTimeout = 48000; } if(gMidiStopPending > 0) @@ -543,8 +649,8 @@ class WildPebble : public ComputerCard gMidiStopPending--; midiClockRunning = false; midiClockDivider = 0; - externalClockActive = false; - externalClockTimeout = 0; + midiClockTimeout = 0; + midiClockPeriod = 0xFFFFFFFF; if(midiNoteGateActive) { @@ -564,14 +670,15 @@ class WildPebble : public ComputerCard continue; } - externalClockActive = true; - externalClockTimeout = 48000; + midiClockTimeout = 48000; midiClockDivider++; if(midiClockDivider >= kMidiClocksPerStep) { midiClockDivider = 0; + midiClockPeriod = sampleCounter - lastMidiStepSample; + lastMidiStepSample = sampleCounter; clockEvent = true; break; } @@ -582,20 +689,8 @@ class WildPebble : public ComputerCard void UpdateMidiNoteOutput() { - if(pulse1 && !midiNoteGateActive) - { - midiLastNote = (uint8_t)currentMIDINote; - - uint8_t velocity = - (uint8_t)(40 + ((currentEnergy * 87) >> 8)); - - QueueMidiMessage(kMidiNoteOn | kMidiChannel, - midiLastNote, - velocity); - - midiNoteGateActive = true; - } - else if(!pulse1 && midiNoteGateActive) + if(midiNoteGateActive && + ((int32_t)(sampleCounter - midiNoteOffSample) >= 0)) { QueueMidiMessage(kMidiNoteOff | kMidiChannel, midiLastNote, @@ -633,24 +728,58 @@ class WildPebble : public ComputerCard bool freeze = PulseIn2(); - bool clockEvent = ConsumeMidiClock(); + bool midiClockEvent = ConsumeMidiClock(); + bool pulseClockEvent = false; if(PulseIn1RisingEdge()) { - externalClockActive = true; - externalClockTimeout = 48000; - clockEvent = true; + pulseClockPeriod = sampleCounter - lastPulseClockSample; + lastPulseClockSample = sampleCounter; + pulseClockTimeout = 48000; + pulseClockEvent = true; + } + + if(pulseClockTimeout > 0) + { + pulseClockTimeout--; + } + + if(midiClockTimeout > 0) + { + midiClockTimeout--; } - if(externalClockTimeout > 0) + bool pulseClockActive = pulseClockTimeout > 0; + bool midiClockActive = + midiClockRunning && + (midiClockTimeout > 0); + + bool preferPulseClock = + pulseClockActive && + (!midiClockActive || + (pulseClockPeriod < midiClockPeriod)); + + bool clockEvent = false; + + if(preferPulseClock) + { + clockEvent = pulseClockEvent; + selectedClockPeriod = pulseClockPeriod; + } + else if(midiClockActive) { - externalClockTimeout--; + clockEvent = midiClockEvent; + selectedClockPeriod = midiClockPeriod; } else { - externalClockActive = false; + selectedClockPeriod = internalClockPeriod; } + externalClockActive = + pulseClockActive || + midiClockActive; + if(!externalClockActive) { swingOffset = 0; @@ -687,6 +816,11 @@ class WildPebble : public ComputerCard if(clockEvent) { AdvanceStep(density, mode); + + if(pulse1) + { + TriggerMidiNote(); + } // Update S+H only when Pulse2 fires