diff --git a/packages/codemirror/widget.mjs b/packages/codemirror/widget.mjs index 42d3b1512..4df17655c 100644 --- a/packages/codemirror/widget.mjs +++ b/packages/codemirror/widget.mjs @@ -140,3 +140,8 @@ registerWidget('_spectrum', (id, options = {}, pat) => { const ctx = getCanvasWidget(id, options).getContext('2d'); return pat.spectrum({ ...options, ctx, id }); }); + +registerWidget('_cyclecounter', (id, options = {}, pat) => { + const ctx = getCanvasWidget(id, options).getContext('2d'); + return pat.cyclecounter({ ...options, ctx, id }); +}); diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index bd2db1223..e4e82578b 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -23,6 +23,8 @@ export class Cyclist { this.beforeStart = beforeStart; this.cps = 0.5; this.num_ticks_since_cps_change = 0; + this.loopStart = 0; + this.loopLength = 0; this.lastTick = 0; // absolute time when last tick (clock callback) happened this.lastBegin = 0; // query begin of last tick this.lastEnd = 0; // query end of last tick @@ -48,6 +50,10 @@ export class Cyclist { this.lastBegin = begin; const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change; this.lastEnd = end; + if (this.loopLength > 0 && this.lastEnd >= this.loopStart + this.loopLength) { + this.lastEnd = this.loopStart + (this.lastEnd % 1); + this.num_ticks_since_cps_change = 0; + } this.lastTick = phase; if (phase < t) { @@ -116,7 +122,7 @@ export class Cyclist { stop() { logger('[cyclist] stop'); this.clock.stop(); - this.lastEnd = 0; + this.lastEnd = this.loopStart; this.setStarted(false); } async setPattern(pat, autostart = false) { @@ -132,6 +138,19 @@ export class Cyclist { this.cps = cps; this.num_ticks_since_cps_change = 0; } + setLoop(start = 0, length = 0) { + if (start >= 0 && length >= 0) { + start = Math.floor(start); + length = Math.floor(length); + + if (start != this.loopStart || length != this.loopLength) { + this.loopStart = Math.floor(start); + this.loopLength = Math.floor(length); + this.lastEnd = this.loopStart + (this.lastEnd % 1); + this.num_ticks_since_cps_change = 0; + } + } + } log(begin, end, haps) { const onsets = haps.filter((h) => h.hasOnset()); console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`); diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index e703909ff..ae1217ae9 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -86,6 +86,7 @@ export function repl({ const toggle = () => scheduler.toggle(); const setCps = (cps) => scheduler.setCps(cps); const setCpm = (cpm) => scheduler.setCps(cpm / 60); + const setLoop = (start, length) => scheduler.setLoop(start, length); // TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`.. @@ -167,6 +168,8 @@ export function repl({ setcps: setCps, setCpm, setcpm: setCpm, + setLoop, + setloop: setLoop, }); }; diff --git a/packages/draw/cyclecounter.mjs b/packages/draw/cyclecounter.mjs new file mode 100644 index 000000000..d23c3adac --- /dev/null +++ b/packages/draw/cyclecounter.mjs @@ -0,0 +1,16 @@ +import { Pattern } from '@strudel/core'; + +Pattern.prototype.cyclecounter = function (options = {}) { + return this.onPaint((ctx, time, haps, drawTime) => { + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + + ctx.font = options.font || '2em monospace'; + ctx.fillStyle = options.color || 'red'; + ctx.textBaseline = 'bottom'; + ctx.textAlign = 'right'; + + const div = options.div || 1; + + ctx.fillText('Cycle' + (div > 1 ? '/' + div : '') + ' ' + ((time / div) >> 0), ctx.canvas.width, ctx.canvas.height); + }); +}; diff --git a/packages/draw/index.mjs b/packages/draw/index.mjs index 506c6151d..58949acbb 100644 --- a/packages/draw/index.mjs +++ b/packages/draw/index.mjs @@ -4,3 +4,4 @@ export * from './draw.mjs'; export * from './pianoroll.mjs'; export * from './spiral.mjs'; export * from './pitchwheel.mjs'; +export * from './cyclecounter.mjs';