diff --git a/packages/codemirror/widget.mjs b/packages/codemirror/widget.mjs index 1d86dc359..72b4ec65d 100644 --- a/packages/codemirror/widget.mjs +++ b/packages/codemirror/widget.mjs @@ -126,3 +126,10 @@ registerWidget('_scope', (id, options = {}, pat) => { const ctx = getCanvasWidget(id, options).getContext('2d'); return pat.tag(id).scope({ ...options, ctx, id }); }); + +registerWidget('_pitchwheel', (id, options = {}, pat) => { + let _size = options.size || 200; + options = { width: _size, height: _size, ...options, size: _size / 5 }; + const ctx = getCanvasWidget(id, options).getContext('2d'); + return pat.pitchwheel({ ...options, ctx, id }); +}); diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index e073dfe83..3a2e6bd6a 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -156,6 +156,7 @@ export function repl({ return pattern; } catch (err) { logger(`[eval] error: ${err.message}`, 'error'); + console.error(err); updateState({ evalError: err, pending: false }); onEvalError?.(err); } diff --git a/packages/draw/index.mjs b/packages/draw/index.mjs index 89cda805e..506c6151d 100644 --- a/packages/draw/index.mjs +++ b/packages/draw/index.mjs @@ -3,3 +3,4 @@ export * from './color.mjs'; export * from './draw.mjs'; export * from './pianoroll.mjs'; export * from './spiral.mjs'; +export * from './pitchwheel.mjs'; diff --git a/packages/draw/pitchwheel.mjs b/packages/draw/pitchwheel.mjs new file mode 100644 index 000000000..8d3cfe61c --- /dev/null +++ b/packages/draw/pitchwheel.mjs @@ -0,0 +1,127 @@ +import { Pattern, midiToFreq, getFrequency } from '@strudel/core'; +import { getTheme, getDrawContext } from './draw.mjs'; + +const c = midiToFreq(36); + +const circlePos = (cx, cy, radius, angle) => { + angle = angle * Math.PI * 2; + const x = Math.sin(angle) * radius + cx; + const y = Math.cos(angle) * radius + cy; + return [x, y]; +}; + +const freq2angle = (freq, root) => { + return 0.5 - (Math.log2(freq / root) % 1); +}; + +export function pitchwheel({ + haps, + ctx, + id, + hapcircles = 1, + circle = 0, + edo = 12, + root = c, + thickness = 3, + hapRadius = 6, + mode = 'flake', + margin = 10, +} = {}) { + const connectdots = mode === 'polygon'; + const centerlines = mode === 'flake'; + const w = ctx.canvas.width; + const h = ctx.canvas.height; + ctx.clearRect(0, 0, w, h); + const color = getTheme().foreground; + + const size = Math.min(w, h); + const radius = size / 2 - thickness / 2 - hapRadius - margin; + const centerX = w / 2; + const centerY = h / 2; + + if (id) { + haps = haps.filter((hap) => hap.hasTag(id)); + } + ctx.strokeStyle = color; + ctx.fillStyle = color; + ctx.globalAlpha = 1; + ctx.lineWidth = thickness; + + if (circle) { + ctx.beginPath(); + ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI); + ctx.stroke(); + } + + if (edo) { + Array.from({ length: edo }, (_, i) => { + const angle = freq2angle(root * Math.pow(2, i / edo), root); + const [x, y] = circlePos(centerX, centerY, radius, angle); + ctx.beginPath(); + ctx.arc(x, y, hapRadius, 0, 2 * Math.PI); + ctx.fill(); + }); + ctx.stroke(); + } + + let shape = []; + ctx.lineWidth = hapRadius; + haps.forEach((hap) => { + let freq; + try { + freq = getFrequency(hap); + } catch (err) { + return; + } + const angle = freq2angle(freq, root); + const [x, y] = circlePos(centerX, centerY, radius, angle); + const hapColor = hap.value.color || color; + ctx.strokeStyle = hapColor; + ctx.fillStyle = hapColor; + const { velocity = 1, gain = 1 } = hap.value || {}; + const alpha = velocity * gain; + ctx.globalAlpha = alpha; + shape.push([x, y, angle, hapColor, alpha]); + ctx.beginPath(); + if (hapcircles) { + ctx.moveTo(x + hapRadius, y); + ctx.arc(x, y, hapRadius, 0, 2 * Math.PI); + ctx.fill(); + } + if (centerlines) { + ctx.moveTo(centerX, centerY); + ctx.lineTo(x, y); + } + ctx.stroke(); + }); + + ctx.strokeStyle = color; + ctx.globalAlpha = 1; + if (connectdots && shape.length) { + shape = shape.sort((a, b) => a[2] - b[2]); + ctx.beginPath(); + ctx.moveTo(shape[0][0], shape[0][1]); + shape.forEach(([x, y, _, color, alpha]) => { + ctx.strokeStyle = color; + ctx.globalAlpha = alpha; + ctx.lineTo(x, y); + }); + ctx.lineTo(shape[0][0], shape[0][1]); + ctx.stroke(); + } + + return; +} + +Pattern.prototype.pitchwheel = function (options = {}) { + let { ctx = getDrawContext(), id = 1 } = options; + return this.tag(id).onPaint((_, time, haps) => + pitchwheel({ + ...options, + time, + ctx, + haps: haps.filter((hap) => hap.isActive(time)), + id, + }), + ); +}; diff --git a/packages/tonal/voicings.mjs b/packages/tonal/voicings.mjs index 66fcd03c1..0a08575db 100644 --- a/packages/tonal/voicings.mjs +++ b/packages/tonal/voicings.mjs @@ -235,6 +235,11 @@ Object.keys(simple).forEach((symbol) => { let alias = symbol.replace('^', 'M'); voicingAlias(symbol, alias, [complex, simple]); } + // add aliases for "+" === "aug" + if (symbol.includes('+')) { + let alias = symbol.replace('+', 'aug'); + voicingAlias(symbol, alias, [complex, simple]); + } }); registerVoicings('ireal', simple);