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.
|
* 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}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
_stepDuration() {
|
_stepDuration() {
|
||||||
return 60 / this._bpm;
|
return 60 / this._bpm / 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Event emission ──────────────────────────────────────────────────
|
// ── Event emission ──────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -268,16 +268,32 @@ export class SwingGroove extends Primitive {
|
||||||
|
|
||||||
process(params, patternDesc, state, rng) {
|
process(params, patternDesc, state, rng) {
|
||||||
const swingAmount = params[0];
|
const swingAmount = params[0];
|
||||||
|
const swingGrid = params[1];
|
||||||
const pattern = clonePattern(patternDesc);
|
const pattern = clonePattern(patternDesc);
|
||||||
|
|
||||||
// Max swing = 0.33 (triplet feel)
|
// Max swing = 0.33 (triplet feel)
|
||||||
const maxOffset = 0.33;
|
const maxOffset = 0.33;
|
||||||
const offset = swingAmount * maxOffset;
|
const offset = swingAmount * maxOffset;
|
||||||
|
|
||||||
// Apply swing to every other step (odd-indexed steps)
|
// swingGrid selects which subdivision gets swung:
|
||||||
for (let i = 1; i < pattern.stepCount; i += 2) {
|
// 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;
|
pattern.steps[i].timeOffset = offset;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { patternDesc: pattern, nextState: {} };
|
return { patternDesc: pattern, nextState: {} };
|
||||||
}
|
}
|
||||||
|
|
@ -399,10 +415,13 @@ export class IntervalLock extends Primitive {
|
||||||
const pattern = clonePattern(patternDesc);
|
const pattern = clonePattern(patternDesc);
|
||||||
|
|
||||||
// Build the full set of MIDI notes in this scale + root + range
|
// 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 = [];
|
const notes = [];
|
||||||
for (let oct = 0; oct < octaveRange; oct++) {
|
for (let oct = 0; oct < octaveRange; oct++) {
|
||||||
for (let i = 0; i < scale.length; i++) {
|
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) {
|
if (midiNote <= 127) {
|
||||||
notes.push(midiNote);
|
notes.push(midiNote);
|
||||||
}
|
}
|
||||||
|
|
@ -418,7 +437,9 @@ export class IntervalLock extends Primitive {
|
||||||
// Quantize pitch [0,1] to nearest note in our scale
|
// Quantize pitch [0,1] to nearest note in our scale
|
||||||
const targetIdx = Math.round(step.pitch * (notes.length - 1));
|
const targetIdx = Math.round(step.pitch * (notes.length - 1));
|
||||||
const clampedIdx = targetIdx < 0 ? 0 : targetIdx >= notes.length ? notes.length - 1 : targetIdx;
|
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;
|
step.pitch = notes[clampedIdx] / 127;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -294,10 +294,13 @@ export function applyProjection(chain, patternDesc) {
|
||||||
* ready to pass to createProjectionChain().
|
* ready to pass to createProjectionChain().
|
||||||
*/
|
*/
|
||||||
export const PRESETS = {
|
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: [
|
expressive: [
|
||||||
{ transform: VelocityCurve, params: { shape: 'exponential' }, field: 'velocity' },
|
{ transform: VelocityCurve, params: { shape: 'exponential' }, field: 'velocity' },
|
||||||
{ transform: RangeMap, params: { min: 48, max: 84 }, field: 'pitch' },
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/** Gate threshold at 0.5 + exponential velocity curve */
|
/** Gate threshold at 0.5 + exponential velocity curve */
|
||||||
|
|
@ -306,9 +309,8 @@ export const PRESETS = {
|
||||||
{ transform: VelocityCurve, params: { shape: 'exponential' }, field: 'velocity' },
|
{ transform: VelocityCurve, params: { shape: 'exponential' }, field: 'velocity' },
|
||||||
],
|
],
|
||||||
|
|
||||||
/** Wide pitch range map (24-96) + linear velocity (identity) */
|
/** S-curve velocity for more dynamic contrast */
|
||||||
fullRange: [
|
fullRange: [
|
||||||
{ transform: RangeMap, params: { min: 24, max: 96 }, field: 'pitch' },
|
{ transform: VelocityCurve, params: { shape: 'sCurve' }, field: 'velocity' },
|
||||||
{ transform: VelocityCurve, params: { shape: 'linear' }, field: 'velocity' },
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,9 @@ export class ShapeSeqEngine {
|
||||||
/** @private */ this._playing = false;
|
/** @private */ this._playing = false;
|
||||||
/** @private */ this._initialized = 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
|
// Track active notes for orphan prevention
|
||||||
/** @private @type {Set<number>} */
|
/** @private @type {Set<number>} */
|
||||||
this._activeNotes = new Set();
|
this._activeNotes = new Set();
|
||||||
|
|
@ -88,7 +91,7 @@ export class ShapeSeqEngine {
|
||||||
this._sequenceIML = await createSequenceIML();
|
this._sequenceIML = await createSequenceIML();
|
||||||
|
|
||||||
// Randomize weights with default spread
|
// Randomize weights with default spread
|
||||||
this._sequenceIML.drawWeights(DEFAULT_SPREAD);
|
this._sequenceIML.randomiseWeights(DEFAULT_SPREAD);
|
||||||
|
|
||||||
// 2. Create the default primitive chain
|
// 2. Create the default primitive chain
|
||||||
this._chain = new Chain();
|
this._chain = new Chain();
|
||||||
|
|
@ -224,6 +227,15 @@ export class ShapeSeqEngine {
|
||||||
setSequenceInputs(values) {
|
setSequenceInputs(values) {
|
||||||
if (!this._initialized || !this._sequenceIML) return;
|
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
|
// 1. Forward inputs to the sequence IML
|
||||||
this._sequenceIML.setInputs(values);
|
this._sequenceIML.setInputs(values);
|
||||||
|
|
||||||
|
|
@ -264,9 +276,9 @@ export class ShapeSeqEngine {
|
||||||
* @param {Object} data - { pitch, velocity, stepIndex, time, accent, isSubdivision }
|
* @param {Object} data - { pitch, velocity, stepIndex, time, accent, isSubdivision }
|
||||||
*/
|
*/
|
||||||
_handleNoteOn(data) {
|
_handleNoteOn(data) {
|
||||||
// pitch comes from the projection layer; after RangeMap it's already
|
// pitch is stored as midiNote/127 in the pattern (set by IntervalLock).
|
||||||
// in MIDI note range (e.g., 48-84). Round to nearest integer.
|
// Convert back to MIDI note number.
|
||||||
const midiNote = Math.round(data.pitch) | 0;
|
const midiNote = Math.round(data.pitch * 127) | 0;
|
||||||
const velocity = data.velocity;
|
const velocity = data.velocity;
|
||||||
|
|
||||||
// Clamp to valid MIDI range
|
// Clamp to valid MIDI range
|
||||||
|
|
@ -284,7 +296,7 @@ export class ShapeSeqEngine {
|
||||||
* @param {Object} data - { pitch, velocity, stepIndex, time }
|
* @param {Object} data - { pitch, velocity, stepIndex, time }
|
||||||
*/
|
*/
|
||||||
_handleNoteOff(data) {
|
_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;
|
const note = midiNote < 0 ? 0 : midiNote > 127 ? 127 : midiNote;
|
||||||
|
|
||||||
this._c15.noteOff(note);
|
this._c15.noteOff(note);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue