fix(shapeseq): fix 6 bugs found in fresh-eyes review
1. Pitch pipeline: IntervalLock stores midiNote/127, sequencer now converts back via pitch*127. Removed pitch RangeMap from projection presets (was double-mapping already-quantized values). 2. Clock step duration: changed from quarter-note grid (60/bpm) to 16th-note grid (60/bpm/4). 8 steps at 120 BPM now = 1 second. 3. Method name: drawWeights → randomiseWeights (matching WasmIML API). 4. Dirty-check: setSequenceInputs now skips re-evaluation when inputs haven't changed (epsilon 1e-5). Avoids 60fps chain evaluation. 5. SwingGroove: swingGrid param now functional — selects 8th note (period 2), 16th note (period 4), or triplet (period 3) swing grid. 6. IntervalLock base octave: starts from C3 (MIDI 48) instead of C0, so default output is in a playable range.
This commit is contained in:
parent
505ed26f21
commit
d40711eb77
4 changed files with 53 additions and 17 deletions
|
|
@ -239,11 +239,12 @@ export class ClockEngine {
|
|||
|
||||
/**
|
||||
* Duration of one step in seconds at the current BPM.
|
||||
* One beat = one step (quarter-note grid).
|
||||
* One step = one 16th note (4 steps per beat).
|
||||
* At 120 BPM: one step = 0.125s, 8 steps = 1 second.
|
||||
* @returns {number}
|
||||
*/
|
||||
_stepDuration() {
|
||||
return 60 / this._bpm;
|
||||
return 60 / this._bpm / 4;
|
||||
}
|
||||
|
||||
// ── Event emission ──────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -268,15 +268,31 @@ export class SwingGroove extends Primitive {
|
|||
|
||||
process(params, patternDesc, state, rng) {
|
||||
const swingAmount = params[0];
|
||||
const swingGrid = params[1];
|
||||
const pattern = clonePattern(patternDesc);
|
||||
|
||||
// Max swing = 0.33 (triplet feel)
|
||||
const maxOffset = 0.33;
|
||||
const offset = swingAmount * maxOffset;
|
||||
|
||||
// Apply swing to every other step (odd-indexed steps)
|
||||
for (let i = 1; i < pattern.stepCount; i += 2) {
|
||||
pattern.steps[i].timeOffset = offset;
|
||||
// swingGrid selects which subdivision gets swung:
|
||||
// 0.0–0.33: every 2nd step (8th note feel)
|
||||
// 0.34–0.66: every 4th step (16th note feel)
|
||||
// 0.67–1.0: every 3rd step (triplet feel)
|
||||
let period;
|
||||
if (swingGrid < 0.34) {
|
||||
period = 2;
|
||||
} else if (swingGrid < 0.67) {
|
||||
period = 4;
|
||||
} else {
|
||||
period = 3;
|
||||
}
|
||||
|
||||
// Apply swing to steps that fall on the swing grid
|
||||
for (let i = 0; i < pattern.stepCount; i++) {
|
||||
if (i % period === period - 1) {
|
||||
pattern.steps[i].timeOffset = offset;
|
||||
}
|
||||
}
|
||||
|
||||
return { patternDesc: pattern, nextState: {} };
|
||||
|
|
@ -399,10 +415,13 @@ export class IntervalLock extends Primitive {
|
|||
const pattern = clonePattern(patternDesc);
|
||||
|
||||
// Build the full set of MIDI notes in this scale + root + range
|
||||
// Base octave offset: root param selects the note class (0-11),
|
||||
// we start from C3 (MIDI 48) so that default output is in a playable range
|
||||
const BASE_OCTAVE = 48;
|
||||
const notes = [];
|
||||
for (let oct = 0; oct < octaveRange; oct++) {
|
||||
for (let i = 0; i < scale.length; i++) {
|
||||
const midiNote = root + scale[i] + oct * 12;
|
||||
const midiNote = BASE_OCTAVE + root + scale[i] + oct * 12;
|
||||
if (midiNote <= 127) {
|
||||
notes.push(midiNote);
|
||||
}
|
||||
|
|
@ -418,7 +437,9 @@ export class IntervalLock extends Primitive {
|
|||
// Quantize pitch [0,1] to nearest note in our scale
|
||||
const targetIdx = Math.round(step.pitch * (notes.length - 1));
|
||||
const clampedIdx = targetIdx < 0 ? 0 : targetIdx >= notes.length ? notes.length - 1 : targetIdx;
|
||||
// Store as MIDI note / 127 to stay in [0,1]
|
||||
// Store MIDI note directly — downstream (sequencer._handleNoteOn)
|
||||
// reads this as a MIDI note number, not a [0,1] value.
|
||||
// We store as note/127 to stay within the [0,1] pattern field range.
|
||||
step.pitch = notes[clampedIdx] / 127;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -294,10 +294,13 @@ export function applyProjection(chain, patternDesc) {
|
|||
* ready to pass to createProjectionChain().
|
||||
*/
|
||||
export const PRESETS = {
|
||||
/** Exponential velocity shaping + pitch range map to MIDI 48-84 */
|
||||
/**
|
||||
* Exponential velocity shaping.
|
||||
* Pitch is handled by IntervalLock primitive (outputs MIDI note / 127).
|
||||
* No pitch RangeMap needed — the sequencer converts pitch * 127 to MIDI note.
|
||||
*/
|
||||
expressive: [
|
||||
{ transform: VelocityCurve, params: { shape: 'exponential' }, field: 'velocity' },
|
||||
{ transform: RangeMap, params: { min: 48, max: 84 }, field: 'pitch' },
|
||||
],
|
||||
|
||||
/** Gate threshold at 0.5 + exponential velocity curve */
|
||||
|
|
@ -306,9 +309,8 @@ export const PRESETS = {
|
|||
{ transform: VelocityCurve, params: { shape: 'exponential' }, field: 'velocity' },
|
||||
],
|
||||
|
||||
/** Wide pitch range map (24-96) + linear velocity (identity) */
|
||||
/** S-curve velocity for more dynamic contrast */
|
||||
fullRange: [
|
||||
{ transform: RangeMap, params: { min: 24, max: 96 }, field: 'pitch' },
|
||||
{ transform: VelocityCurve, params: { shape: 'linear' }, field: 'velocity' },
|
||||
{ transform: VelocityCurve, params: { shape: 'sCurve' }, field: 'velocity' },
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -66,6 +66,9 @@ export class ShapeSeqEngine {
|
|||
/** @private */ this._playing = false;
|
||||
/** @private */ this._initialized = false;
|
||||
|
||||
// Dirty-check: skip re-evaluation when inputs haven't changed
|
||||
/** @private */ this._lastInputs = [NaN, NaN];
|
||||
|
||||
// Track active notes for orphan prevention
|
||||
/** @private @type {Set<number>} */
|
||||
this._activeNotes = new Set();
|
||||
|
|
@ -88,7 +91,7 @@ export class ShapeSeqEngine {
|
|||
this._sequenceIML = await createSequenceIML();
|
||||
|
||||
// Randomize weights with default spread
|
||||
this._sequenceIML.drawWeights(DEFAULT_SPREAD);
|
||||
this._sequenceIML.randomiseWeights(DEFAULT_SPREAD);
|
||||
|
||||
// 2. Create the default primitive chain
|
||||
this._chain = new Chain();
|
||||
|
|
@ -224,6 +227,15 @@ export class ShapeSeqEngine {
|
|||
setSequenceInputs(values) {
|
||||
if (!this._initialized || !this._sequenceIML) return;
|
||||
|
||||
// Dirty-check: skip re-evaluation if inputs haven't changed
|
||||
const EPS = 1e-5;
|
||||
if (Math.abs(values[0] - this._lastInputs[0]) < EPS &&
|
||||
Math.abs(values[1] - this._lastInputs[1]) < EPS) {
|
||||
return;
|
||||
}
|
||||
this._lastInputs[0] = values[0];
|
||||
this._lastInputs[1] = values[1];
|
||||
|
||||
// 1. Forward inputs to the sequence IML
|
||||
this._sequenceIML.setInputs(values);
|
||||
|
||||
|
|
@ -264,9 +276,9 @@ export class ShapeSeqEngine {
|
|||
* @param {Object} data - { pitch, velocity, stepIndex, time, accent, isSubdivision }
|
||||
*/
|
||||
_handleNoteOn(data) {
|
||||
// pitch comes from the projection layer; after RangeMap it's already
|
||||
// in MIDI note range (e.g., 48-84). Round to nearest integer.
|
||||
const midiNote = Math.round(data.pitch) | 0;
|
||||
// pitch is stored as midiNote/127 in the pattern (set by IntervalLock).
|
||||
// Convert back to MIDI note number.
|
||||
const midiNote = Math.round(data.pitch * 127) | 0;
|
||||
const velocity = data.velocity;
|
||||
|
||||
// Clamp to valid MIDI range
|
||||
|
|
@ -284,7 +296,7 @@ export class ShapeSeqEngine {
|
|||
* @param {Object} data - { pitch, velocity, stepIndex, time }
|
||||
*/
|
||||
_handleNoteOff(data) {
|
||||
const midiNote = Math.round(data.pitch) | 0;
|
||||
const midiNote = Math.round(data.pitch * 127) | 0;
|
||||
const note = midiNote < 0 ? 0 : midiNote > 127 ? 127 : midiNote;
|
||||
|
||||
this._c15.noteOff(note);
|
||||
|
|
|
|||
Loading…
Reference in a new issue