From c7fdf5245a3a20c99e59877e87316d03761d12c6 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 5 Jun 2022 23:37:24 +0200 Subject: [PATCH 1/7] more flexible pianoroll --- packages/tone/draw.mjs | 23 ++++++------ packages/tone/pianoroll.mjs | 72 ++++++++++++++++++++++++++++++------- 2 files changed, 72 insertions(+), 23 deletions(-) diff --git a/packages/tone/draw.mjs b/packages/tone/draw.mjs index d894c547..c0af49b2 100644 --- a/packages/tone/draw.mjs +++ b/packages/tone/draw.mjs @@ -20,7 +20,7 @@ export const getDrawContext = (id = 'test-canvas') => { return canvas.getContext('2d'); }; -Pattern.prototype.draw = function (callback, cycleSpan, lookaheadCycles = 1) { +Pattern.prototype.draw = function (callback, { from, to, onQuery }) { if (window.strudelAnimation) { cancelAnimationFrame(window.strudelAnimation); } @@ -29,19 +29,22 @@ Pattern.prototype.draw = function (callback, cycleSpan, lookaheadCycles = 1) { events = []; const animate = (time) => { const t = Tone.getTransport().seconds; - if (cycleSpan) { - const currentCycle = Math.floor(t / cycleSpan); + if (from !== undefined && to !== undefined) { + const currentCycle = Math.floor(t); if (cycle !== currentCycle) { cycle = currentCycle; - const begin = currentCycle * cycleSpan; - const end = (currentCycle + lookaheadCycles) * cycleSpan; - events = this._asNumber(true) // true = silent error - .query(new State(new TimeSpan(begin, end))) - .filter(Boolean) - .filter((event) => event.part.begin.equals(event.whole.begin)); + const begin = currentCycle + from; + const end = currentCycle + to; + setTimeout(() => { + events = this._asNumber(true) // true = silent error + .query(new State(new TimeSpan(begin, end))) + .filter(Boolean) + .filter((event) => event.part.begin.equals(event.whole.begin)); + onQuery?.(events); + }, 0); } } - callback(ctx, events, t, cycleSpan, time); + callback(ctx, events, t, time); window.strudelAnimation = requestAnimationFrame(animate); }; requestAnimationFrame(animate); diff --git a/packages/tone/pianoroll.mjs b/packages/tone/pianoroll.mjs index dfb5e5cb..210979ec 100644 --- a/packages/tone/pianoroll.mjs +++ b/packages/tone/pianoroll.mjs @@ -7,38 +7,84 @@ This program is free software: you can redistribute it and/or modify it under th import { Pattern } from '@strudel.cycles/core'; Pattern.prototype.pianoroll = function ({ - timeframe = 10, + from = -2, + to = 2, + overscan = 1, inactive = '#C9E597', active = '#FFCA28', - background = '#2A3236', - maxMidi = 90, - minMidi = 0, + // background = '#2A3236', + background = 'transparent', + maxMidi, + minMidi, + timeframe: timeFrameProp, } = {}) { - const w = window.innerWidth; - const h = window.innerHeight; - const midiRange = maxMidi - minMidi + 1; - const height = h / midiRange; + if (timeFrameProp) { + console.warn('timeframe is deprecated! use from/to instead'); + from = 0; + to = timeFrameProp; + } + const ctx = getDrawContext(); + const w = ctx.canvas.width; + const h = ctx.canvas.height; + let midiRange, height; + const autorange = minMidi === undefined || maxMidi === undefined; + if (autorange && (minMidi !== undefined || maxMidi !== undefined)) { + console.warn('pianoroll: minMidi and maxMidi must both be set to have an effect!'); + } + if (!autorange) { + midiRange = maxMidi - minMidi + 1; + height = h / midiRange; + } + const timeframe = to - from; + const t2x = (t) => Math.round(((t - from) / timeframe) * w); + const playheadX = t2x(0); this.draw( (ctx, events, t) => { ctx.fillStyle = background; ctx.clearRect(0, 0, w, h); ctx.fillRect(0, 0, w, h); - events.forEach((event) => { + const inFrame = (event) => event.whole.begin >= 0 && event.whole.begin <= t + to && event.whole.end >= t + from; + events.filter(inFrame).forEach((event) => { const isActive = event.whole.begin <= t && event.whole.end >= t; ctx.fillStyle = event.context?.color || inactive; ctx.strokeStyle = event.context?.color || active; ctx.globalAlpha = event.context.velocity ?? 1; - const x = Math.round((event.whole.begin / timeframe) * w); + ctx.beginPath(); + ctx.moveTo(playheadX, 0); + ctx.lineTo(playheadX, h); + ctx.stroke(); + const x = t2x(event.whole.begin); const width = Math.round(((event.whole.end - event.whole.begin) / timeframe) * w); - const y = Math.round(h - ((Number(event.value) - minMidi) / midiRange) * h); + const y = Math.round(h - ((Number(event.value) - minMidi + 1) / midiRange) * h); const offset = (t / timeframe) * w; const margin = 0; const coords = [x - offset + margin + 1, y + 1, width - 2, height - 2]; isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords); }); }, - timeframe, - 2, // lookaheadCycles + { + from: from - overscan, + to: to + overscan, + onQuery: (events) => { + if (autorange) { + const getValue = (e) => Number(e.value); + const { min, max } = events.reduce( + ({ min, max }, e) => { + const v = getValue(e); + return { + min: v < min ? v : min, + max: v > max ? v : max, + }; + }, + { min: Infinity, max: -Infinity }, + ); + minMidi = min; + maxMidi = max; + midiRange = maxMidi - minMidi + 1; + height = h / midiRange; + } + }, + }, ); return this; }; From 394e77c152fb26fbc715bd7f57040fc4fb469ea6 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 5 Jun 2022 23:37:49 +0200 Subject: [PATCH 2/7] pianoroll: autorange + fold + vertical --- packages/tone/pianoroll.mjs | 117 ++++++++++++++++++++++++------------ 1 file changed, 78 insertions(+), 39 deletions(-) diff --git a/packages/tone/pianoroll.mjs b/packages/tone/pianoroll.mjs index 210979ec..8f16553d 100644 --- a/packages/tone/pianoroll.mjs +++ b/packages/tone/pianoroll.mjs @@ -6,6 +6,8 @@ This program is free software: you can redistribute it and/or modify it under th import { Pattern } from '@strudel.cycles/core'; +const scale = (normalized, min, max) => normalized * (max - min) + min; + Pattern.prototype.pianoroll = function ({ from = -2, to = 2, @@ -14,30 +16,37 @@ Pattern.prototype.pianoroll = function ({ active = '#FFCA28', // background = '#2A3236', background = 'transparent', - maxMidi, - minMidi, - timeframe: timeFrameProp, + minMidi = 10, + maxMidi = 90, + autorange = 1, + timeframe: timeframeProp, + fold = 0, + vertical = 0, } = {}) { - if (timeFrameProp) { - console.warn('timeframe is deprecated! use from/to instead'); - from = 0; - to = timeFrameProp; - } const ctx = getDrawContext(); const w = ctx.canvas.width; const h = ctx.canvas.height; - let midiRange, height; - const autorange = minMidi === undefined || maxMidi === undefined; - if (autorange && (minMidi !== undefined || maxMidi !== undefined)) { - console.warn('pianoroll: minMidi and maxMidi must both be set to have an effect!'); + + if (timeframeProp) { + console.warn('timeframe is deprecated! use from/to instead'); + from = 0; + to = timeframeProp; } - if (!autorange) { - midiRange = maxMidi - minMidi + 1; - height = h / midiRange; + if (!autorange && fold) { + console.warn('disabling autorange has no effect when fold is enabled'); } - const timeframe = to - from; - const t2x = (t) => Math.round(((t - from) / timeframe) * w); - const playheadX = t2x(0); + const timeAxis = vertical ? h : w; + const valueAxis = vertical ? w : h; + // scale normalized value n to max pixels, flippable + const timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time + const timeExtent = to - from; // number of seconds that fit inside the canvas frame + const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values + let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true + let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true + let foldValues = []; + + // duration to px (on timeAxis) + const playhead = scale(-from / timeExtent, ...timeRange); this.draw( (ctx, events, t) => { ctx.fillStyle = background; @@ -50,15 +59,43 @@ Pattern.prototype.pianoroll = function ({ ctx.strokeStyle = event.context?.color || active; ctx.globalAlpha = event.context.velocity ?? 1; ctx.beginPath(); - ctx.moveTo(playheadX, 0); - ctx.lineTo(playheadX, h); + if (vertical) { + ctx.moveTo(0, playhead); + ctx.lineTo(valueAxis, playhead); + } else { + ctx.moveTo(playhead, 0); + ctx.lineTo(playhead, valueAxis); + } ctx.stroke(); - const x = t2x(event.whole.begin); - const width = Math.round(((event.whole.end - event.whole.begin) / timeframe) * w); - const y = Math.round(h - ((Number(event.value) - minMidi + 1) / midiRange) * h); - const offset = (t / timeframe) * w; - const margin = 0; - const coords = [x - offset + margin + 1, y + 1, width - 2, height - 2]; + const timePx = scale((event.whole.begin - from) / timeExtent, ...timeRange); + let durationPx = scale(event.duration / timeExtent, 0, timeAxis); + + const valuePx = scale( + fold ? foldValues.indexOf(event.value) / foldValues.length : (Number(event.value) - minMidi) / valueExtent, + ...valueRange, + ); + let margin = 0; + // apply some pixel adjustments + const offset = scale(t / timeExtent, ...timeRange); + let coords; + if (vertical) { + // console.log('durationPx', durationPx); + // swap x/y and width/height of rect + coords = [ + valuePx + 1, // x + timeAxis - durationPx - offset + timePx + margin + 1, // y + barThickness - 2, // width + durationPx - 2, // height + ]; + // console.log(event.value, 'coords', coords); + } else { + coords = [ + timePx - offset + margin + 1, // x + valuePx - barThickness + 1, // y + durationPx - 2, // widith + barThickness - 2, // height + ]; + } isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords); }); }, @@ -66,23 +103,25 @@ Pattern.prototype.pianoroll = function ({ from: from - overscan, to: to + overscan, onQuery: (events) => { + const getValue = (e) => Number(e.value); + const { min, max, values } = events.reduce( + ({ min, max, values }, e) => { + const v = getValue(e); + return { + min: v < min ? v : min, + max: v > max ? v : max, + values: values.includes(v) ? values : [...values, v], + }; + }, + { min: Infinity, max: -Infinity, values: [] }, + ); if (autorange) { - const getValue = (e) => Number(e.value); - const { min, max } = events.reduce( - ({ min, max }, e) => { - const v = getValue(e); - return { - min: v < min ? v : min, - max: v > max ? v : max, - }; - }, - { min: Infinity, max: -Infinity }, - ); minMidi = min; maxMidi = max; - midiRange = maxMidi - minMidi + 1; - height = h / midiRange; + valueExtent = maxMidi - minMidi + 1; } + foldValues = values.sort((a, b) => a - b); + barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent; }, }, ); From ba5cf5ba8354d17be71371466350e05f21b00692 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 5 Jun 2022 23:38:07 +0200 Subject: [PATCH 3/7] replace from to with cycles / playhead --- packages/tone/pianoroll.mjs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/tone/pianoroll.mjs b/packages/tone/pianoroll.mjs index 8f16553d..f681734f 100644 --- a/packages/tone/pianoroll.mjs +++ b/packages/tone/pianoroll.mjs @@ -9,8 +9,8 @@ import { Pattern } from '@strudel.cycles/core'; const scale = (normalized, min, max) => normalized * (max - min) + min; Pattern.prototype.pianoroll = function ({ - from = -2, - to = 2, + cycles = 4, + playhead = .5, overscan = 1, inactive = '#C9E597', active = '#FFCA28', @@ -26,6 +26,8 @@ Pattern.prototype.pianoroll = function ({ const ctx = getDrawContext(); const w = ctx.canvas.width; const h = ctx.canvas.height; + const from = -cycles * playhead; + const to = cycles * (1 - playhead); if (timeframeProp) { console.warn('timeframe is deprecated! use from/to instead'); @@ -46,7 +48,7 @@ Pattern.prototype.pianoroll = function ({ let foldValues = []; // duration to px (on timeAxis) - const playhead = scale(-from / timeExtent, ...timeRange); + const playheadPosition = scale(-from / timeExtent, ...timeRange); this.draw( (ctx, events, t) => { ctx.fillStyle = background; @@ -60,11 +62,11 @@ Pattern.prototype.pianoroll = function ({ ctx.globalAlpha = event.context.velocity ?? 1; ctx.beginPath(); if (vertical) { - ctx.moveTo(0, playhead); - ctx.lineTo(valueAxis, playhead); + ctx.moveTo(0, playheadPosition); + ctx.lineTo(valueAxis, playheadPosition); } else { - ctx.moveTo(playhead, 0); - ctx.lineTo(playhead, valueAxis); + ctx.moveTo(playheadPosition, 0); + ctx.lineTo(playheadPosition, valueAxis); } ctx.stroke(); const timePx = scale((event.whole.begin - from) / timeExtent, ...timeRange); From ac39a609d73dda5e1198ad9b9690377745fde020 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 5 Jun 2022 23:38:22 +0200 Subject: [PATCH 4/7] pianoroll: flipTime + flipValues --- packages/tone/pianoroll.mjs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/tone/pianoroll.mjs b/packages/tone/pianoroll.mjs index f681734f..f87e9c6b 100644 --- a/packages/tone/pianoroll.mjs +++ b/packages/tone/pianoroll.mjs @@ -10,8 +10,10 @@ const scale = (normalized, min, max) => normalized * (max - min) + min; Pattern.prototype.pianoroll = function ({ cycles = 4, - playhead = .5, + playhead = 0.5, overscan = 1, + flipTime = 0, + flipValues = 0, inactive = '#C9E597', active = '#FFCA28', // background = '#2A3236', @@ -40,12 +42,14 @@ Pattern.prototype.pianoroll = function ({ const timeAxis = vertical ? h : w; const valueAxis = vertical ? w : h; // scale normalized value n to max pixels, flippable - const timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time + let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time const timeExtent = to - from; // number of seconds that fit inside the canvas frame const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true let foldValues = []; + flipTime && timeRange.reverse(); + flipValues && valueRange.reverse(); // duration to px (on timeAxis) const playheadPosition = scale(-from / timeExtent, ...timeRange); @@ -69,7 +73,7 @@ Pattern.prototype.pianoroll = function ({ ctx.lineTo(playheadPosition, valueAxis); } ctx.stroke(); - const timePx = scale((event.whole.begin - from) / timeExtent, ...timeRange); + const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange); let durationPx = scale(event.duration / timeExtent, 0, timeAxis); const valuePx = scale( @@ -81,19 +85,17 @@ Pattern.prototype.pianoroll = function ({ const offset = scale(t / timeExtent, ...timeRange); let coords; if (vertical) { - // console.log('durationPx', durationPx); - // swap x/y and width/height of rect coords = [ - valuePx + 1, // x - timeAxis - durationPx - offset + timePx + margin + 1, // y + valuePx + 1 - (flipValues ? barThickness : 0), // x + timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y barThickness - 2, // width durationPx - 2, // height ]; // console.log(event.value, 'coords', coords); } else { coords = [ - timePx - offset + margin + 1, // x - valuePx - barThickness + 1, // y + timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x + valuePx + 1 - (flipValues ? 0 : barThickness), // y durationPx - 2, // widith barThickness - 2, // height ]; From 68814dd97cc92a5b4e0e423c2df37128724c8882 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 5 Jun 2022 23:38:33 +0200 Subject: [PATCH 5/7] pianoroll: hideNegative flag --- packages/tone/pianoroll.mjs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/tone/pianoroll.mjs b/packages/tone/pianoroll.mjs index f87e9c6b..f195f632 100644 --- a/packages/tone/pianoroll.mjs +++ b/packages/tone/pianoroll.mjs @@ -14,6 +14,7 @@ Pattern.prototype.pianoroll = function ({ overscan = 1, flipTime = 0, flipValues = 0, + hideNegative = false, inactive = '#C9E597', active = '#FFCA28', // background = '#2A3236', @@ -58,9 +59,10 @@ Pattern.prototype.pianoroll = function ({ ctx.fillStyle = background; ctx.clearRect(0, 0, w, h); ctx.fillRect(0, 0, w, h); - const inFrame = (event) => event.whole.begin >= 0 && event.whole.begin <= t + to && event.whole.end >= t + from; + const inFrame = (event) => + (!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.whole.end >= t + from; events.filter(inFrame).forEach((event) => { - const isActive = event.whole.begin <= t && event.whole.end >= t; + const isActive = event.whole.begin <= t && event.whole.end > t; ctx.fillStyle = event.context?.color || inactive; ctx.strokeStyle = event.context?.color || active; ctx.globalAlpha = event.context.velocity ?? 1; From b3d2d3028c42a84fbf0467ee79e62b75277be620 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 5 Jun 2022 23:47:24 +0200 Subject: [PATCH 6/7] disable autorange by default --- packages/tone/pianoroll.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tone/pianoroll.mjs b/packages/tone/pianoroll.mjs index f195f632..a2747fa1 100644 --- a/packages/tone/pianoroll.mjs +++ b/packages/tone/pianoroll.mjs @@ -21,7 +21,7 @@ Pattern.prototype.pianoroll = function ({ background = 'transparent', minMidi = 10, maxMidi = 90, - autorange = 1, + autorange = 0, timeframe: timeframeProp, fold = 0, vertical = 0, From 6d6784d429252d027ef9ec6b85911e8ef71fd032 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 5 Jun 2022 23:59:44 +0200 Subject: [PATCH 7/7] remove old comments --- packages/tone/pianoroll.mjs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/tone/pianoroll.mjs b/packages/tone/pianoroll.mjs index a2747fa1..b77c5dec 100644 --- a/packages/tone/pianoroll.mjs +++ b/packages/tone/pianoroll.mjs @@ -42,7 +42,6 @@ Pattern.prototype.pianoroll = function ({ } const timeAxis = vertical ? h : w; const valueAxis = vertical ? w : h; - // scale normalized value n to max pixels, flippable let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time const timeExtent = to - from; // number of seconds that fit inside the canvas frame const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values @@ -52,7 +51,6 @@ Pattern.prototype.pianoroll = function ({ flipTime && timeRange.reverse(); flipValues && valueRange.reverse(); - // duration to px (on timeAxis) const playheadPosition = scale(-from / timeExtent, ...timeRange); this.draw( (ctx, events, t) => { @@ -83,7 +81,6 @@ Pattern.prototype.pianoroll = function ({ ...valueRange, ); let margin = 0; - // apply some pixel adjustments const offset = scale(t / timeExtent, ...timeRange); let coords; if (vertical) { @@ -93,7 +90,6 @@ Pattern.prototype.pianoroll = function ({ barThickness - 2, // width durationPx - 2, // height ]; - // console.log(event.value, 'coords', coords); } else { coords = [ timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x