Compare commits

..

2 Commits

Author SHA1 Message Date
Felix Roos 0d7438b939 Merge branch 'main' into amplitude-modulation 2023-09-17 17:29:56 +02:00
Felix Roos 52de58ef1f begin am feature 2023-08-23 21:55:11 +02:00
41 changed files with 392 additions and 1622 deletions
+1 -2
View File
@@ -18,5 +18,4 @@ vite.config.js
**/*.json
**/dev-dist
**/dist
/src-tauri/target/**/*
reverbGen.mjs
/src-tauri/target/**/*
-1
View File
@@ -1,4 +1,3 @@
export * from './codemirror.mjs';
export * from './highlight.mjs';
export * from './flash.mjs';
export * from './slider.mjs';
-135
View File
@@ -1,135 +0,0 @@
import { ref, pure } from '@strudel.cycles/core';
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
import { StateEffect, StateField } from '@codemirror/state';
export let sliderValues = {};
const getSliderID = (from) => `slider_${from}`;
export class SliderWidget extends WidgetType {
constructor(value, min, max, from, to, step, view) {
super();
this.value = value;
this.min = min;
this.max = max;
this.from = from;
this.originalFrom = from;
this.to = to;
this.step = step;
this.view = view;
}
eq() {
return false;
}
toDOM() {
let wrap = document.createElement('span');
wrap.setAttribute('aria-hidden', 'true');
wrap.className = 'cm-slider'; // inline-flex items-center
let slider = wrap.appendChild(document.createElement('input'));
slider.type = 'range';
slider.min = this.min;
slider.max = this.max;
slider.step = this.step ?? (this.max - this.min) / 1000;
slider.originalValue = this.value;
// to make sure the code stays in sync, let's save the original value
// becuase .value automatically clamps values so it'll desync with the code
slider.value = slider.originalValue;
slider.from = this.from;
slider.originalFrom = this.originalFrom;
slider.to = this.to;
slider.style = 'width:64px;margin-right:4px;transform:translateY(4px)';
this.slider = slider;
slider.addEventListener('input', (e) => {
const next = e.target.value;
let insert = next;
//let insert = next.toFixed(2);
const to = slider.from + slider.originalValue.length;
let change = { from: slider.from, to, insert };
slider.originalValue = insert;
slider.value = insert;
this.view.dispatch({ changes: change });
const id = getSliderID(slider.originalFrom); // matches id generated in transpiler
window.postMessage({ type: 'cm-slider', value: Number(next), id });
});
return wrap;
}
ignoreEvent(e) {
return true;
}
}
export const setWidgets = StateEffect.define();
export const updateWidgets = (view, widgets) => {
view.dispatch({ effects: setWidgets.of(widgets) });
};
function getWidgets(widgetConfigs, view) {
return widgetConfigs.map(({ from, to, value, min, max, step }) => {
return Decoration.widget({
widget: new SliderWidget(value, min, max, from, to, step, view),
side: 0,
}).range(from /* , to */);
});
}
export const sliderPlugin = ViewPlugin.fromClass(
class {
decorations; //: DecorationSet
constructor(view /* : EditorView */) {
this.decorations = Decoration.set([]);
}
update(update /* : ViewUpdate */) {
update.transactions.forEach((tr) => {
if (tr.docChanged) {
this.decorations = this.decorations.map(tr.changes);
const iterator = this.decorations.iter();
while (iterator.value) {
// when the widgets are moved, we need to tell the dom node the current position
// this is important because the updateSliderValue function has to work with the dom node
if (iterator.value?.widget?.slider) {
iterator.value.widget.slider.from = iterator.from;
iterator.value.widget.slider.to = iterator.to;
}
iterator.next();
}
}
for (let e of tr.effects) {
if (e.is(setWidgets)) {
this.decorations = Decoration.set(getWidgets(e.value, update.view));
}
}
});
}
},
{
decorations: (v) => v.decorations,
},
);
export let slider = (value) => {
console.warn('slider will only work when the transpiler is used... passing value as is');
return pure(value);
};
// function transpiled from slider = (value, min, max)
export let sliderWithID = (id, value, min, max) => {
sliderValues[id] = value; // sync state at eval time (code -> state)
return ref(() => sliderValues[id]); // use state at query time
};
// update state when sliders are moved
if (typeof window !== 'undefined') {
window.addEventListener('message', (e) => {
if (e.data.type === 'cm-slider') {
if (sliderValues[e.data.id] !== undefined) {
// update state when slider is moved
sliderValues[e.data.id] = e.data.value;
} else {
console.warn(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`);
}
}
});
}
+6 -57
View File
@@ -139,6 +139,8 @@ const generic_params = [
*
*/
[['fmi', 'fmh'], 'fm'],
['amh'],
['ami'],
// fm envelope
/**
* Ramp type of fm envelope. Exp might be a bit broken..
@@ -655,15 +657,6 @@ const generic_params = [
* .vib("<.5 1 2 4 8 16>:12")
*/
[['vib', 'vibmod'], 'vibrato', 'v'],
/**
* Adds pink noise to the mix
*
* @name noise
* @param {number | Pattern} wet wet amount
* @example
* sound("<white pink brown>/2")
*/
['noise'],
/**
* Sets the vibrato depth in semitones. Only has an effect if `vibrato` | `vib` | `v` is is also set
*
@@ -979,64 +972,20 @@ const generic_params = [
*
*/
[['room', 'size']],
/**
* Reverb lowpass starting frequency (in hertz).
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
*
* @name roomlp
* @synonyms rlp
* @param {number} frequency between 0 and 20000hz
* @example
* s("bd sd").room(0.5).rlp(10000)
* @example
* s("bd sd").room(0.5).rlp(5000)
*/
['roomlp', 'rlp'],
/**
* Reverb lowpass frequency at -60dB (in hertz).
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
*
* @name roomdim
* @synonyms rdim
* @param {number} frequency between 0 and 20000hz
* @example
* s("bd sd").room(0.5).rlp(10000).rdim(8000)
* @example
* s("bd sd").room(0.5).rlp(5000).rdim(400)
*
*/
['roomdim', 'rdim'],
/**
* Reverb fade time (in seconds).
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
*
* @name roomfade
* @synonyms rfade
* @param {number} seconds for the reverb to fade
* @example
* s("bd sd").room(0.5).rlp(10000).rfade(0.5)
* @example
* s("bd sd").room(0.5).rlp(5000).rfade(4)
*
*/
['roomfade', 'rfade'],
/**
* Sets the room size of the reverb, see {@link room}.
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
*
* @name roomsize
* @param {number | Pattern} size between 0 and 10
* @synonyms rsize, sz, size
* @synonyms size, sz
* @example
* s("bd sd").room(.8).rsize(1)
* @example
* s("bd sd").room(.8).rsize(4)
* s("bd sd").room(.8).roomsize("<0 1 2 4 8>")
*
*/
// TODO: find out why :
// s("bd sd").room(.8).roomsize("<0 .2 .4 .6 .8 [1,0]>").osc()
// .. does not work. Is it because room is only one effect?
['roomsize', 'size', 'sz', 'rsize'],
['size', 'sz', 'roomsize'],
// ['sagogo'],
// ['sclap'],
// ['sclaves'],
@@ -1206,7 +1155,7 @@ const generic_params = [
['pitchJump'],
['pitchJumpTime'],
['lfo', 'repeatTime'],
['znoise'], // noise on the frequency or as bubo calls it "frequency fog" :)
['noise'],
['zmod'],
['zcrush'], // like crush but scaled differently
['zdelay'],
-6
View File
@@ -2343,9 +2343,3 @@ export const fit = register('fit', (pat) =>
export const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) {
return _loopAt(factor, pat, cps);
});
/** exposes a custom value at query time. basically allows mutating state without evaluation */
export const ref = (accessor) =>
pure(1)
.withValue(() => reify(accessor()))
.innerJoin();
+2 -5
View File
@@ -16,17 +16,14 @@ export function repl({
transpiler,
onToggle,
editPattern,
createCyclist,
}) {
const cyclistParams = {
const scheduler = new Cyclist({
interval,
onTrigger: getTrigger({ defaultOutput, getTime }),
onError: onSchedulerError,
getTime,
onToggle,
};
const scheduler = createCyclist?.(cyclistParams) || new Cyclist(cyclistParams);
});
const setPattern = (pattern, autostart = true) => {
pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart);
+1 -2
View File
@@ -43,8 +43,7 @@ function createClock(
clear();
};
const getPhase = () => phase;
const setPhase = (_phase) => (phase = _phase);
// setCallback
return { setDuration, start, stop, pause, duration, interval, getPhase, setPhase, minLatency };
return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency };
}
export default createClock;
-77
View File
@@ -1,77 +0,0 @@
import { Cyclist } from '@strudel.cycles/core';
import { logger } from '../core/logger.mjs';
import { Invoke } from '../../website/src/tauri.mjs';
import { listen } from '@tauri-apps/api/event';
export class CyclistBridge extends Cyclist {
constructor(params) {
super(params);
this.start_timer;
this.abeLinkListener = listen('abelink-event', async (e) => {
const payload = e?.payload;
if (payload == null) {
return;
}
const { started, cps, phase, timestamp } = payload;
if (cps !== this.cps) {
this.setCps(cps);
}
// TODO: I'm not sure how to hook this up this phase adjustment in Strudel
// a phase adjustment message is sent every 30 seconds from backend to keep clocks in sync
const phaseDiff = Math.abs(phase - this.clock.getPhase());
if (phaseDiff > 0.1) {
console.log('set phase from', this.clock.getPhase(), 'to', phase);
// hmmm this seems wrong...
//this.clock.setPhase(phase);
}
if (this.started !== started && started != null) {
if (started) {
// the time delay in ms that seems to occur when starting the clock. Unsure if this is standard across all clients
const evaluationTime = 140;
// when start message comes from abelink, delay starting cyclist clock until the start of the next abelink phase
this.start_timer = window.setTimeout(() => {
// TODO: evaluate the code so if another source triggers the play there will not be an error
logger('[cyclist] start');
this.clock.start();
this.setStarted(true);
}, timestamp - Date.now() - evaluationTime);
} else {
this.stop();
}
}
});
}
start() {
if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.');
}
const linkmsg = {
cps: this.cps,
started: true,
timestamp: Date.now(),
phase: this.clock.getPhase(),
};
Invoke('sendabelinkmsg', { linkmsg });
}
stop() {
logger('[cyclist] stop');
this.clock.stop();
this.lastEnd = 0;
this.setStarted(false);
const linkmsg = {
// TODO: change this to value of "main" clock cps
cps: 0,
started: false,
timestamp: Date.now(),
phase: this.clock.getPhase(),
};
Invoke('sendabelinkmsg', { linkmsg });
}
}
-1
View File
@@ -7,4 +7,3 @@ This program is free software: you can redistribute it and/or modify it under th
export * from './midibridge.mjs';
export * from './utils.mjs';
export * from './oscbridge.mjs';
export * from './cyclistbridge.mjs';
+18 -71
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as _WebMidi from 'webmidi';
import { Pattern, isPattern, logger, ref } from '@strudel.cycles/core';
import { Pattern, isPattern, logger } from '@strudel.cycles/core';
import { noteToMidi } from '@strudel.cycles/core';
import { Note } from 'webmidi';
// if you use WebMidi from outside of this package, make sure to import that instance:
@@ -15,8 +15,8 @@ function supportsMidi() {
return typeof navigator.requestMIDIAccess === 'function';
}
function getMidiDeviceNamesString(devices) {
return devices.map((o) => `'${o.name}'`).join(' | ');
function getMidiDeviceNamesString(outputs) {
return outputs.map((o) => `'${o.name}'`).join(' | ');
}
export function enableWebMidi(options = {}) {
@@ -52,41 +52,30 @@ export function enableWebMidi(options = {}) {
});
});
}
// const outputByName = (name: string) => WebMidi.getOutputByName(name);
const outputByName = (name) => WebMidi.getOutputByName(name);
function getDevice(indexOrName, devices) {
if (!devices.length) {
// output?: string | number, outputs: typeof WebMidi.outputs
function getDevice(output, outputs) {
if (!outputs.length) {
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
}
if (typeof indexOrName === 'number') {
return devices[indexOrName];
if (typeof output === 'number') {
return outputs[output];
}
const byName = (name) => devices.find((output) => output.name.includes(name));
if (typeof indexOrName === 'string') {
return byName(indexOrName);
if (typeof output === 'string') {
return outputByName(output);
}
// attempt to default to first IAC device if none is specified
const IACOutput = byName('IAC');
const device = IACOutput ?? devices[0];
const IACOutput = outputs.find((output) => output.name.includes('IAC'));
const device = IACOutput ?? outputs[0];
if (!device) {
throw new Error(
`🔌 MIDI device '${device ? device : ''}' not found. Use one of ${getMidiDeviceNamesString(devices)}`,
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${getMidiDeviceNamesString(WebMidi.outputs)}`,
);
}
return IACOutput ?? devices[0];
}
// send start/stop messages to outputs when repl starts/stops
if (typeof window !== 'undefined') {
window.addEventListener('message', (e) => {
if (!WebMidi?.enabled) {
return;
}
if (e.data === 'strudel-stop') {
WebMidi.outputs.forEach((output) => output.sendStop());
}
// cannot start here, since we have no timing info, see sendStart below
});
return IACOutput ?? outputs[0];
}
Pattern.prototype.midi = function (output) {
@@ -114,7 +103,6 @@ Pattern.prototype.midi = function (output) {
return this.onTrigger((time, hap, currentTime, cps) => {
if (!WebMidi.enabled) {
console.log('not enabled');
return;
}
const device = getDevice(output, WebMidi.outputs);
@@ -125,7 +113,7 @@ Pattern.prototype.midi = function (output) {
const timeOffsetString = `+${offset}`;
// destructure value
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd } = hap.value;
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1 } = hap.value;
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
// note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length
@@ -137,7 +125,7 @@ Pattern.prototype.midi = function (output) {
time: timeOffsetString,
});
}
if (ccv !== undefined && ccn !== undefined) {
if (ccv && ccn) {
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
throw new Error('expected ccv to be a number between 0 and 1');
}
@@ -147,46 +135,5 @@ Pattern.prototype.midi = function (output) {
const scaled = Math.round(ccv * 127);
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
}
if (hap.whole.begin + 0 === 0) {
// we need to start here because we have the timing info
device.sendStart({ time: timeOffsetString });
}
if (['clock', 'midiClock'].includes(midicmd)) {
device.sendClock({ time: timeOffsetString });
} else if (['start'].includes(midicmd)) {
device.sendStart({ time: timeOffsetString });
} else if (['stop'].includes(midicmd)) {
device.sendStop({ time: timeOffsetString });
} else if (['continue'].includes(midicmd)) {
device.sendContinue({ time: timeOffsetString });
}
});
};
let listeners = {};
const refs = {};
export async function midin(input) {
const initial = await enableWebMidi(); // only returns on first init
const device = getDevice(input, WebMidi.inputs);
if (initial) {
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
logger(
`Midi enabled! Using "${device.name}". ${
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
}`,
);
refs[input] = {};
}
const cc = (cc) => ref(() => refs[input][cc] || 0);
listeners[input] && device.removeListener('midimessage', listeners[input]);
listeners[input] = (e) => {
const cc = e.dataBytes[0];
const v = e.dataBytes[1];
refs[input] && (refs[input][cc] = v / 127);
};
device.addListener('midimessage', listeners[input]);
return cc;
}
+191 -246
View File
@@ -200,21 +200,20 @@ function peg$parse(input, options) {
var peg$c23 = "*";
var peg$c24 = "?";
var peg$c25 = ":";
var peg$c26 = "..";
var peg$c27 = "struct";
var peg$c28 = "target";
var peg$c29 = "euclid";
var peg$c30 = "slow";
var peg$c31 = "rotL";
var peg$c32 = "rotR";
var peg$c33 = "fast";
var peg$c34 = "scale";
var peg$c35 = "//";
var peg$c36 = "cat";
var peg$c37 = "$";
var peg$c38 = "setcps";
var peg$c39 = "setbpm";
var peg$c40 = "hush";
var peg$c26 = "struct";
var peg$c27 = "target";
var peg$c28 = "euclid";
var peg$c29 = "slow";
var peg$c30 = "rotL";
var peg$c31 = "rotR";
var peg$c32 = "fast";
var peg$c33 = "scale";
var peg$c34 = "//";
var peg$c35 = "cat";
var peg$c36 = "$";
var peg$c37 = "setcps";
var peg$c38 = "setbpm";
var peg$c39 = "hush";
var peg$r0 = /^[1-9]/;
var peg$r1 = /^[eE]/;
@@ -256,67 +255,64 @@ function peg$parse(input, options) {
var peg$e30 = peg$literalExpectation("*", false);
var peg$e31 = peg$literalExpectation("?", false);
var peg$e32 = peg$literalExpectation(":", false);
var peg$e33 = peg$literalExpectation("..", false);
var peg$e34 = peg$literalExpectation("struct", false);
var peg$e35 = peg$literalExpectation("target", false);
var peg$e36 = peg$literalExpectation("euclid", false);
var peg$e37 = peg$literalExpectation("slow", false);
var peg$e38 = peg$literalExpectation("rotL", false);
var peg$e39 = peg$literalExpectation("rotR", false);
var peg$e40 = peg$literalExpectation("fast", false);
var peg$e41 = peg$literalExpectation("scale", false);
var peg$e42 = peg$literalExpectation("//", false);
var peg$e43 = peg$classExpectation(["\n"], true, false);
var peg$e44 = peg$literalExpectation("cat", false);
var peg$e45 = peg$literalExpectation("$", false);
var peg$e46 = peg$literalExpectation("setcps", false);
var peg$e47 = peg$literalExpectation("setbpm", false);
var peg$e48 = peg$literalExpectation("hush", false);
var peg$e33 = peg$literalExpectation("struct", false);
var peg$e34 = peg$literalExpectation("target", false);
var peg$e35 = peg$literalExpectation("euclid", false);
var peg$e36 = peg$literalExpectation("slow", false);
var peg$e37 = peg$literalExpectation("rotL", false);
var peg$e38 = peg$literalExpectation("rotR", false);
var peg$e39 = peg$literalExpectation("fast", false);
var peg$e40 = peg$literalExpectation("scale", false);
var peg$e41 = peg$literalExpectation("//", false);
var peg$e42 = peg$classExpectation(["\n"], true, false);
var peg$e43 = peg$literalExpectation("cat", false);
var peg$e44 = peg$literalExpectation("$", false);
var peg$e45 = peg$literalExpectation("setcps", false);
var peg$e46 = peg$literalExpectation("setbpm", false);
var peg$e47 = peg$literalExpectation("hush", false);
var peg$f0 = function() { return parseFloat(text()); };
var peg$f1 = function() { return parseInt(text()); };
var peg$f2 = function(chars) { return new AtomStub(chars.join("")) };
var peg$f3 = function(s) { return s };
var peg$f4 = function(s, stepsPerCycle) { s.arguments_.stepsPerCycle = stepsPerCycle ; return s; };
var peg$f5 = function(a) { return a };
var peg$f6 = function(s) { s.arguments_.alignment = 'slowcat'; return s; };
var peg$f7 = function(a) { return x => x.options_['weight'] = a };
var peg$f8 = function(a) { return x => x.options_['reps'] = a };
var peg$f9 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
var peg$f10 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
var peg$f12 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) };
var peg$f13 = function(s) { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) };
var peg$f14 = function(s) { return x => x.options_['ops'].push({ type_: "range", arguments_ :{ element:s } }) };
var peg$f15 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
var peg$f1 = function(chars) { return new AtomStub(chars.join("")) };
var peg$f2 = function(s) { return s };
var peg$f3 = function(s, stepsPerCycle) { s.arguments_.stepsPerCycle = stepsPerCycle ; return s; };
var peg$f4 = function(a) { return a };
var peg$f5 = function(s) { s.arguments_.alignment = 'slowcat'; return s; };
var peg$f6 = function(a) { return x => x.options_['weight'] = a };
var peg$f7 = function(a) { return x => x.options_['reps'] = a };
var peg$f8 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
var peg$f9 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
var peg$f10 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) };
var peg$f12 = function(s) { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) };
var peg$f13 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
for (const op of ops) {
op(result);
}
return result;
};
var peg$f16 = function(s) { return new PatternStub(s, 'fastcat'); };
var peg$f17 = function(tail) { return { alignment: 'stack', list: tail }; };
var peg$f18 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; };
var peg$f19 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } };
var peg$f20 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
var peg$f21 = function(sc) { return sc; };
var peg$f22 = function(s) { return { name: "struct", args: { mini:s }}};
var peg$f23 = function(s) { return { name: "target", args : { name:s}}};
var peg$f24 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
var peg$f25 = function(a) { return { name: "stretch", args :{ amount: a}}};
var peg$f26 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
var peg$f27 = function(a) { return { name: "shift", args :{ amount: a}}};
var peg$f28 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
var peg$f29 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
var peg$f30 = function(s, v) { return v};
var peg$f31 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
var peg$f32 = function(sg) {return sg};
var peg$f33 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
var peg$f34 = function(sc) { return sc };
var peg$f35 = function(c) { return c };
var peg$f36 = function(v) { return new CommandStub("setcps", { value: v})};
var peg$f37 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
var peg$f38 = function() { return new CommandStub("hush")};
var peg$f14 = function(s) { return new PatternStub(s, 'fastcat'); };
var peg$f15 = function(tail) { return { alignment: 'stack', list: tail }; };
var peg$f16 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; };
var peg$f17 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } };
var peg$f18 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
var peg$f19 = function(sc) { return sc; };
var peg$f20 = function(s) { return { name: "struct", args: { mini:s }}};
var peg$f21 = function(s) { return { name: "target", args : { name:s}}};
var peg$f22 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
var peg$f23 = function(a) { return { name: "stretch", args :{ amount: a}}};
var peg$f24 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
var peg$f25 = function(a) { return { name: "shift", args :{ amount: a}}};
var peg$f26 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
var peg$f27 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
var peg$f28 = function(s, v) { return v};
var peg$f29 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
var peg$f30 = function(sg) {return sg};
var peg$f31 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
var peg$f32 = function(sc) { return sc };
var peg$f33 = function(c) { return c };
var peg$f34 = function(v) { return new CommandStub("setcps", { value: v})};
var peg$f35 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
var peg$f36 = function() { return new CommandStub("hush")};
var peg$currPos = 0;
var peg$savedPos = 0;
var peg$posDetailsCache = [{ line: 1, column: 1 }];
@@ -655,26 +651,6 @@ function peg$parse(input, options) {
return s0;
}
function peg$parseintneg() {
var s0, s1, s2;
s0 = peg$currPos;
s1 = peg$parseminus();
if (s1 === peg$FAILED) {
s1 = null;
}
s2 = peg$parseint();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f1();
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parseminus() {
var s0;
@@ -908,7 +884,7 @@ function peg$parse(input, options) {
if (s2 !== peg$FAILED) {
s3 = peg$parsews();
peg$savedPos = s0;
s0 = peg$f2(s2);
s0 = peg$f1(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -944,7 +920,7 @@ function peg$parse(input, options) {
if (s6 !== peg$FAILED) {
s7 = peg$parsews();
peg$savedPos = s0;
s0 = peg$f3(s4);
s0 = peg$f2(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -992,7 +968,7 @@ function peg$parse(input, options) {
}
s8 = peg$parsews();
peg$savedPos = s0;
s0 = peg$f4(s4, s7);
s0 = peg$f3(s4, s7);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1024,7 +1000,7 @@ function peg$parse(input, options) {
s2 = peg$parseslice();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f5(s2);
s0 = peg$f4(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1064,7 +1040,7 @@ function peg$parse(input, options) {
if (s6 !== peg$FAILED) {
s7 = peg$parsews();
peg$savedPos = s0;
s0 = peg$f6(s4);
s0 = peg$f5(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1114,9 +1090,6 @@ function peg$parse(input, options) {
s0 = peg$parseop_degrade();
if (s0 === peg$FAILED) {
s0 = peg$parseop_tail();
if (s0 === peg$FAILED) {
s0 = peg$parseop_range();
}
}
}
}
@@ -1142,7 +1115,7 @@ function peg$parse(input, options) {
s2 = peg$parsenumber();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f7(s2);
s0 = peg$f6(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1170,7 +1143,7 @@ function peg$parse(input, options) {
s2 = peg$parsenumber();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f8(s2);
s0 = peg$f7(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1224,7 +1197,7 @@ function peg$parse(input, options) {
}
if (s13 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f9(s3, s7, s11);
s0 = peg$f8(s3, s7, s11);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1264,7 +1237,7 @@ function peg$parse(input, options) {
s2 = peg$parseslice();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f10(s2);
s0 = peg$f9(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1292,7 +1265,7 @@ function peg$parse(input, options) {
s2 = peg$parseslice();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f11(s2);
s0 = peg$f10(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1322,7 +1295,7 @@ function peg$parse(input, options) {
s2 = null;
}
peg$savedPos = s0;
s0 = peg$f12(s2);
s0 = peg$f11(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1346,35 +1319,7 @@ function peg$parse(input, options) {
s2 = peg$parseslice();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f13(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parseop_range() {
var s0, s1, s2;
s0 = peg$currPos;
if (input.substr(peg$currPos, 2) === peg$c26) {
s1 = peg$c26;
peg$currPos += 2;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e33); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parseslice();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f14(s2);
s0 = peg$f12(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1400,7 +1345,7 @@ function peg$parse(input, options) {
s3 = peg$parseslice_op();
}
peg$savedPos = s0;
s0 = peg$f15(s1, s2);
s0 = peg$f13(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1425,7 +1370,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f16(s1);
s1 = peg$f14(s1);
}
s0 = s1;
@@ -1474,7 +1419,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f17(s1);
s1 = peg$f15(s1);
}
s0 = s1;
@@ -1523,7 +1468,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f18(s1);
s1 = peg$f16(s1);
}
s0 = s1;
@@ -1544,7 +1489,7 @@ function peg$parse(input, options) {
s2 = null;
}
peg$savedPos = s0;
s0 = peg$f19(s1, s2);
s0 = peg$f17(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1564,7 +1509,7 @@ function peg$parse(input, options) {
s2 = null;
}
peg$savedPos = s0;
s0 = peg$f20(s1, s2);
s0 = peg$f18(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1587,7 +1532,7 @@ function peg$parse(input, options) {
s6 = peg$parsequote();
if (s6 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f21(s4);
s0 = peg$f19(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1637,19 +1582,19 @@ function peg$parse(input, options) {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (input.substr(peg$currPos, 6) === peg$c27) {
s1 = peg$c27;
if (input.substr(peg$currPos, 6) === peg$c26) {
s1 = peg$c26;
peg$currPos += 6;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e34); }
if (peg$silentFails === 0) { peg$fail(peg$e33); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
s3 = peg$parsemini_or_operator();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f22(s3);
s0 = peg$f20(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1666,12 +1611,12 @@ function peg$parse(input, options) {
var s0, s1, s2, s3, s4, s5;
s0 = peg$currPos;
if (input.substr(peg$currPos, 6) === peg$c28) {
s1 = peg$c28;
if (input.substr(peg$currPos, 6) === peg$c27) {
s1 = peg$c27;
peg$currPos += 6;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e35); }
if (peg$silentFails === 0) { peg$fail(peg$e34); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1682,7 +1627,7 @@ function peg$parse(input, options) {
s5 = peg$parsequote();
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f23(s4);
s0 = peg$f21(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1707,12 +1652,12 @@ function peg$parse(input, options) {
var s0, s1, s2, s3, s4, s5, s6, s7;
s0 = peg$currPos;
if (input.substr(peg$currPos, 6) === peg$c29) {
s1 = peg$c29;
if (input.substr(peg$currPos, 6) === peg$c28) {
s1 = peg$c28;
peg$currPos += 6;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e36); }
if (peg$silentFails === 0) { peg$fail(peg$e35); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1727,7 +1672,7 @@ function peg$parse(input, options) {
s7 = null;
}
peg$savedPos = s0;
s0 = peg$f24(s3, s5, s7);
s0 = peg$f22(s3, s5, s7);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1747,6 +1692,35 @@ function peg$parse(input, options) {
function peg$parseslow() {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (input.substr(peg$currPos, 4) === peg$c29) {
s1 = peg$c29;
peg$currPos += 4;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e36); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f23(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parserotL() {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (input.substr(peg$currPos, 4) === peg$c30) {
s1 = peg$c30;
@@ -1755,6 +1729,35 @@ function peg$parse(input, options) {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e37); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f24(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parserotR() {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (input.substr(peg$currPos, 4) === peg$c31) {
s1 = peg$c31;
peg$currPos += 4;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e38); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
s3 = peg$parsenumber();
@@ -1773,16 +1776,16 @@ function peg$parse(input, options) {
return s0;
}
function peg$parserotL() {
function peg$parsefast() {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (input.substr(peg$currPos, 4) === peg$c31) {
s1 = peg$c31;
if (input.substr(peg$currPos, 4) === peg$c32) {
s1 = peg$c32;
peg$currPos += 4;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e38); }
if (peg$silentFails === 0) { peg$fail(peg$e39); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1802,74 +1805,16 @@ function peg$parse(input, options) {
return s0;
}
function peg$parserotR() {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (input.substr(peg$currPos, 4) === peg$c32) {
s1 = peg$c32;
peg$currPos += 4;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e39); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f27(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parsefast() {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (input.substr(peg$currPos, 4) === peg$c33) {
s1 = peg$c33;
peg$currPos += 4;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e40); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f28(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parsescale() {
var s0, s1, s2, s3, s4, s5;
s0 = peg$currPos;
if (input.substr(peg$currPos, 5) === peg$c34) {
s1 = peg$c34;
if (input.substr(peg$currPos, 5) === peg$c33) {
s1 = peg$c33;
peg$currPos += 5;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e41); }
if (peg$silentFails === 0) { peg$fail(peg$e40); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1889,7 +1834,7 @@ function peg$parse(input, options) {
s5 = peg$parsequote();
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f29(s4);
s0 = peg$f27(s4);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -1914,12 +1859,12 @@ function peg$parse(input, options) {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (input.substr(peg$currPos, 2) === peg$c35) {
s1 = peg$c35;
if (input.substr(peg$currPos, 2) === peg$c34) {
s1 = peg$c34;
peg$currPos += 2;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e42); }
if (peg$silentFails === 0) { peg$fail(peg$e41); }
}
if (s1 !== peg$FAILED) {
s2 = [];
@@ -1928,7 +1873,7 @@ function peg$parse(input, options) {
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e43); }
if (peg$silentFails === 0) { peg$fail(peg$e42); }
}
while (s3 !== peg$FAILED) {
s2.push(s3);
@@ -1937,7 +1882,7 @@ function peg$parse(input, options) {
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e43); }
if (peg$silentFails === 0) { peg$fail(peg$e42); }
}
}
s1 = [s1, s2];
@@ -1954,12 +1899,12 @@ function peg$parse(input, options) {
var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9;
s0 = peg$currPos;
if (input.substr(peg$currPos, 3) === peg$c36) {
s1 = peg$c36;
if (input.substr(peg$currPos, 3) === peg$c35) {
s1 = peg$c35;
peg$currPos += 3;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e44); }
if (peg$silentFails === 0) { peg$fail(peg$e43); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
@@ -1981,7 +1926,7 @@ function peg$parse(input, options) {
s9 = peg$parsemini_or_operator();
if (s9 !== peg$FAILED) {
peg$savedPos = s7;
s7 = peg$f30(s5, s9);
s7 = peg$f28(s5, s9);
} else {
peg$currPos = s7;
s7 = peg$FAILED;
@@ -1998,7 +1943,7 @@ function peg$parse(input, options) {
s9 = peg$parsemini_or_operator();
if (s9 !== peg$FAILED) {
peg$savedPos = s7;
s7 = peg$f30(s5, s9);
s7 = peg$f28(s5, s9);
} else {
peg$currPos = s7;
s7 = peg$FAILED;
@@ -2018,7 +1963,7 @@ function peg$parse(input, options) {
}
if (s8 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f31(s5, s6);
s0 = peg$f29(s5, s6);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2064,7 +2009,7 @@ function peg$parse(input, options) {
s4 = peg$parsecomment();
}
peg$savedPos = s0;
s0 = peg$f32(s1);
s0 = peg$f30(s1);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2075,18 +2020,18 @@ function peg$parse(input, options) {
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
if (input.charCodeAt(peg$currPos) === 36) {
s3 = peg$c37;
s3 = peg$c36;
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e45); }
if (peg$silentFails === 0) { peg$fail(peg$e44); }
}
if (s3 !== peg$FAILED) {
s4 = peg$parsews();
s5 = peg$parsemini_or_operator();
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f33(s1, s5);
s0 = peg$f31(s1, s5);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2111,7 +2056,7 @@ function peg$parse(input, options) {
s1 = peg$parsemini_or_operator();
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f34(s1);
s1 = peg$f32(s1);
}
s0 = s1;
if (s0 === peg$FAILED) {
@@ -2144,7 +2089,7 @@ function peg$parse(input, options) {
if (s2 !== peg$FAILED) {
s3 = peg$parsews();
peg$savedPos = s0;
s0 = peg$f35(s2);
s0 = peg$f33(s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2157,19 +2102,19 @@ function peg$parse(input, options) {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (input.substr(peg$currPos, 6) === peg$c38) {
s1 = peg$c38;
if (input.substr(peg$currPos, 6) === peg$c37) {
s1 = peg$c37;
peg$currPos += 6;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e46); }
if (peg$silentFails === 0) { peg$fail(peg$e45); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f36(s3);
s0 = peg$f34(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2186,19 +2131,19 @@ function peg$parse(input, options) {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (input.substr(peg$currPos, 6) === peg$c39) {
s1 = peg$c39;
if (input.substr(peg$currPos, 6) === peg$c38) {
s1 = peg$c38;
peg$currPos += 6;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e47); }
if (peg$silentFails === 0) { peg$fail(peg$e46); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parsews();
s3 = peg$parsenumber();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f37(s3);
s0 = peg$f35(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@@ -2215,16 +2160,16 @@ function peg$parse(input, options) {
var s0, s1;
s0 = peg$currPos;
if (input.substr(peg$currPos, 4) === peg$c40) {
s1 = peg$c40;
if (input.substr(peg$currPos, 4) === peg$c39) {
s1 = peg$c39;
peg$currPos += 4;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e48); }
if (peg$silentFails === 0) { peg$fail(peg$e47); }
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f38();
s1 = peg$f36();
}
s0 = s1;
+1 -7
View File
@@ -79,9 +79,6 @@ frac
int
= zero / (digit1_9 DIGIT*)
intneg
= minus? int { return parseInt(text()); }
minus
= "-"
@@ -126,7 +123,7 @@ slice = step / sub_cycle / polymeter / slow_sequence
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
// at this point, we assume we can represent them as regular sequence operators
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade / op_tail / op_range
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade / op_tail
op_weight = "@" a:number
{ return x => x.options_['weight'] = a }
@@ -149,9 +146,6 @@ op_degrade = "?"a:number?
op_tail = ":" s:slice
{ return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) }
op_range = ".." s:slice
{ return x => x.options_['ops'].push({ type_: "range", arguments_ :{ element:s } }) }
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
slice_with_ops = s:slice ops:slice_op*
{ const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
-11
View File
@@ -45,17 +45,6 @@ const applyOptions = (parent, enter) => (pat, i) => {
pat = pat.fmap((a) => (b) => Array.isArray(a) ? [...a, b] : [a, b]).appLeft(friend);
break;
}
case 'range': {
const friend = enter(op.arguments_.element);
pat = strudel.reify(pat);
const arrayRange = (start, stop, step = 1) =>
Array.from({ length: Math.abs(stop - start) / step + 1 }, (value, index) =>
start < stop ? start + index * step : start - index * step,
);
let range = (apat, bpat) => apat.squeezeBind((a) => bpat.bind((b) => strudel.fastcat(...arrayRange(a, b))));
pat = range(pat, friend);
break;
}
default: {
console.warn(`operator "${op.type_}" not implemented`);
}
-6
View File
@@ -184,12 +184,6 @@ describe('mini', () => {
it('supports lists', () => {
expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']);
});
it('supports ranges', () => {
expect(minV('0 .. 4')).toEqual([0, 1, 2, 3, 4]);
});
it('supports patterned ranges', () => {
expect(minS('[<0 1> .. <2 4>]*2')).toEqual(minS('[0 1 2] [1 2 3 4]'));
});
});
describe('getLeafLocation', () => {
@@ -15,11 +15,10 @@ import {
updateMiniLocations,
} from '@strudel/codemirror';
import './style.css';
import { sliderPlugin } from '@strudel/codemirror/slider.mjs';
export { flash, highlightMiniLocations, updateMiniLocations };
const staticExtensions = [javascript(), flashField, highlightExtension, sliderPlugin];
const staticExtensions = [javascript(), flashField, highlightExtension];
export default function CodeMirror({
value,
-2
View File
@@ -19,7 +19,6 @@ function useStrudel({
drawContext,
drawTime = [-2, 2],
paintOptions = {},
createCyclist, // (params) => Cyclist
}) {
const id = useMemo(() => s4(), []);
canvasId = canvasId || `canvas-${id}`;
@@ -46,7 +45,6 @@ function useStrudel({
onEvalError?.(err);
},
getTime,
createCyclist,
drawContext,
transpiler,
editPattern,
-13
View File
@@ -1,13 +0,0 @@
import { useEffect, useState } from 'react';
import { updateWidgets } from '@strudel/codemirror';
// i know this is ugly.. in the future, repl needs to run without react
export function useWidgets(view) {
const [widgets, setWidgets] = useState([]);
useEffect(() => {
if (view) {
updateWidgets(view, widgets);
}
}, [view, widgets]);
return { widgets, setWidgets };
}
-79
View File
@@ -1,79 +0,0 @@
import { getAudioContext } from './superdough.mjs';
let worklet;
export async function dspWorklet(ac, code) {
const name = `dsp-worklet-${Date.now()}`;
const workletCode = `${code}
let __q = []; // trigger queue
class MyProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.t = 0;
this.stopped = false;
this.port.onmessage = (e) => {
if(e.data==='stop') {
this.stopped = true;
} else if(e.data?.dough) {
__q.push(e.data)
} else {
msg?.(e.data)
}
};
}
process(inputs, outputs, parameters) {
const output = outputs[0];
if(__q.length) {
for(let i=0;i<__q.length;++i) {
const deadline = __q[i].time-currentTime;
if(deadline<=0) {
trigger(__q[i].dough)
__q.splice(i,1)
}
}
}
for (let i = 0; i < output[0].length; i++) {
const out = dsp(this.t / sampleRate);
output.forEach((channel) => {
channel[i] = out;
});
this.t++;
}
return !this.stopped;
}
}
registerProcessor('${name}', MyProcessor);
`;
const base64String = btoa(workletCode);
const dataURL = `data:text/javascript;base64,${base64String}`;
await ac.audioWorklet.addModule(dataURL);
const node = new AudioWorkletNode(ac, name);
const stop = () => node.port.postMessage('stop');
return { node, stop };
}
const stop = () => {
if (worklet) {
worklet?.stop();
worklet?.node?.disconnect();
}
};
if (typeof window !== 'undefined') {
window.addEventListener('message', (e) => {
if (e.data === 'strudel-stop') {
stop();
} else if (e.data?.dough) {
worklet?.node.port.postMessage(e.data);
}
});
}
export const dough = async (code) => {
const ac = getAudioContext();
stop();
worklet = await dspWorklet(ac, code);
worklet.node.connect(ac.destination);
};
export function doughTrigger(t, hap, currentTime, duration, cps) {
window.postMessage({ time: t, dough: hap.value, currentTime, duration, cps });
}
+13 -22
View File
@@ -78,6 +78,19 @@ export const getParamADSR = (param, attack, decay, sustain, release, min, max, b
param.linearRampToValueAtTime(min, end + Math.max(release, 0.1));
};
export const getAM = (freq, harmonicityRatio, amount, wave = 'sine') => {
const carrfreq = freq;
const modfreq = carrfreq * harmonicityRatio;
const { node: modulator, stop } = mod(modfreq, amount, wave);
const g = gainNode(1);
modulator.connect(g.gain);
return g; /*
const c = 1 / 1 ** amount;
console.log('c', c);
const compensate = gainNode(1/amount);
g.connect(compensate);
return compensate; */
};
export function createFilter(
context,
type,
@@ -112,25 +125,3 @@ export function createFilter(
return filter;
}
// stays 1 until .5, then fades out
let wetfade = (d) => (d < 0.5 ? 1 : 1 - (d - 0.5) / 0.5);
// mix together dry and wet nodes. 0 = only dry 1 = only wet
// still not too sure about how this could be used more generally...
export function drywet(dry, wet, wetAmount = 0) {
const ac = getAudioContext();
if (!wetAmount) {
return dry;
}
let dry_gain = ac.createGain();
let wet_gain = ac.createGain();
dry.connect(dry_gain);
wet.connect(wet_gain);
dry_gain.gain.value = wetfade(wetAmount);
wet_gain.gain.value = wetfade(1 - wetAmount);
let mix = ac.createGain();
dry_gain.connect(mix);
wet_gain.connect(mix);
return mix;
}
-1
View File
@@ -10,4 +10,3 @@ export * from './helpers.mjs';
export * from './synth.mjs';
export * from './zzfx.mjs';
export * from './logger.mjs';
export * from './dspworklet.mjs';
-63
View File
@@ -1,63 +0,0 @@
import { drywet } from './helpers.mjs';
import { getAudioContext } from './superdough.mjs';
let noiseCache = {};
// lazy generates noise buffers and keeps them forever
function getNoiseBuffer(type) {
const ac = getAudioContext();
if (noiseCache[type]) {
return noiseCache[type];
}
const bufferSize = 2 * ac.sampleRate;
const noiseBuffer = ac.createBuffer(1, bufferSize, ac.sampleRate);
const output = noiseBuffer.getChannelData(0);
let lastOut = 0;
let b0, b1, b2, b3, b4, b5, b6;
b0 = b1 = b2 = b3 = b4 = b5 = b6 = 0.0;
for (let i = 0; i < bufferSize; i++) {
if (type === 'white') {
output[i] = Math.random() * 2 - 1;
} else if (type === 'brown') {
let white = Math.random() * 2 - 1;
output[i] = (lastOut + 0.02 * white) / 1.02;
lastOut = output[i];
} else if (type === 'pink') {
let white = Math.random() * 2 - 1;
b0 = 0.99886 * b0 + white * 0.0555179;
b1 = 0.99332 * b1 + white * 0.0750759;
b2 = 0.969 * b2 + white * 0.153852;
b3 = 0.8665 * b3 + white * 0.3104856;
b4 = 0.55 * b4 + white * 0.5329522;
b5 = -0.7616 * b5 - white * 0.016898;
output[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362;
output[i] *= 0.11;
b6 = white * 0.115926;
}
}
noiseCache[type] = noiseBuffer;
return noiseBuffer;
}
// expects one of noises as type
export function getNoiseOscillator(type = 'white', t) {
const ac = getAudioContext();
const o = ac.createBufferSource();
o.buffer = getNoiseBuffer(type);
o.loop = true;
o.start(t);
return {
node: o,
stop: (time) => o.stop(time),
};
}
export function getNoiseMix(inputNode, wet, t) {
const noiseOscillator = getNoiseOscillator('pink', t);
const noiseMix = drywet(inputNode, noiseOscillator.node, wet);
return {
node: noiseMix,
stop: (time) => noiseOscillator?.stop(time),
};
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "superdough",
"version": "0.9.9",
"version": "0.9.8",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",
+17 -24
View File
@@ -1,30 +1,23 @@
import reverbGen from './reverbGen.mjs';
if (typeof AudioContext !== 'undefined') {
AudioContext.prototype.generateReverb = reverbGen.generateReverb;
AudioContext.prototype.createReverb = function (duration, fade, lp, dim) {
AudioContext.prototype.impulseResponse = function (duration, channels = 1) {
const length = this.sampleRate * duration;
const impulse = this.createBuffer(channels, length, this.sampleRate);
const IR = impulse.getChannelData(0);
for (let i = 0; i < length; i++) IR[i] = (2 * Math.random() - 1) * Math.pow(1 - i / length, duration);
return impulse;
};
AudioContext.prototype.createReverb = function (duration) {
const convolver = this.createConvolver();
convolver.generate = (d, fade, lp, dim) => {
this.generateReverb(
{
audioContext: this,
sampleRate: 44100,
numChannels: 2,
decayTime: d,
fadeInTime: fade,
lpFreqStart: lp,
lpFreqEnd: dim,
},
(buffer) => {
convolver.buffer = buffer;
},
);
convolver.duration = d;
convolver.fade = fade;
convolver.lp = lp;
convolver.dim = dim;
convolver.setDuration = (d) => {
convolver.buffer = this.impulseResponse(d);
convolver.duration = duration;
return convolver;
};
convolver.generate(duration, fade, lp, dim);
convolver.setDuration(duration);
return convolver;
};
}
// TODO: make the reverb more exciting
// check out https://blog.gskinner.com/archives/2019/02/reverb-web-audio-api.html
-129
View File
@@ -1,129 +0,0 @@
// Copyright 2014 Alan deLespinasse
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
var reverbGen = {};
/** Generates a reverb impulse response.
@param {!Object} params TODO: Document the properties.
@param {!function(!AudioBuffer)} callback Function to call when
the impulse response has been generated. The impulse response
is passed to this function as its parameter. May be called
immediately within the current execution context, or later. */
reverbGen.generateReverb = function (params, callback) {
var audioContext = params.audioContext || new AudioContext();
var sampleRate = params.sampleRate || 44100;
var numChannels = params.numChannels || 2;
// params.decayTime is the -60dB fade time. We let it go 50% longer to get to -90dB.
var totalTime = params.decayTime * 1.5;
var decaySampleFrames = Math.round(params.decayTime * sampleRate);
var numSampleFrames = Math.round(totalTime * sampleRate);
var fadeInSampleFrames = Math.round((params.fadeInTime || 0) * sampleRate);
// 60dB is a factor of 1 million in power, or 1000 in amplitude.
var decayBase = Math.pow(1 / 1000, 1 / decaySampleFrames);
var reverbIR = audioContext.createBuffer(numChannels, numSampleFrames, sampleRate);
for (var i = 0; i < numChannels; i++) {
var chan = reverbIR.getChannelData(i);
for (var j = 0; j < numSampleFrames; j++) {
chan[j] = randomSample() * Math.pow(decayBase, j);
}
for (var j = 0; j < fadeInSampleFrames; j++) {
chan[j] *= j / fadeInSampleFrames;
}
}
applyGradualLowpass(reverbIR, params.lpFreqStart || 0, params.lpFreqEnd || 0, params.decayTime, callback);
};
/** Creates a canvas element showing a graph of the given data.
@param {!Float32Array} data An array of numbers, or a Float32Array.
@param {number} width Width in pixels of the canvas.
@param {number} height Height in pixels of the canvas.
@param {number} min Minimum value of data for the graph (lower edge).
@param {number} max Maximum value of data in the graph (upper edge).
@return {!CanvasElement} The generated canvas element. */
reverbGen.generateGraph = function (data, width, height, min, max) {
var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
var gc = canvas.getContext('2d');
gc.fillStyle = '#000';
gc.fillRect(0, 0, canvas.width, canvas.height);
gc.fillStyle = '#fff';
var xscale = width / data.length;
var yscale = height / (max - min);
for (var i = 0; i < data.length; i++) {
gc.fillRect(i * xscale, height - (data[i] - min) * yscale, 1, 1);
}
return canvas;
};
/** Applies a constantly changing lowpass filter to the given sound.
@private
@param {!AudioBuffer} input
@param {number} lpFreqStart
@param {number} lpFreqEnd
@param {number} lpFreqEndAt
@param {!function(!AudioBuffer)} callback May be called
immediately within the current execution context, or later.*/
var applyGradualLowpass = function (input, lpFreqStart, lpFreqEnd, lpFreqEndAt, callback) {
if (lpFreqStart == 0) {
callback(input);
return;
}
var channelData = getAllChannelData(input);
var context = new OfflineAudioContext(input.numberOfChannels, channelData[0].length, input.sampleRate);
var player = context.createBufferSource();
player.buffer = input;
var filter = context.createBiquadFilter();
lpFreqStart = Math.min(lpFreqStart, input.sampleRate / 2);
lpFreqEnd = Math.min(lpFreqEnd, input.sampleRate / 2);
filter.type = 'lowpass';
filter.Q.value = 0.0001;
filter.frequency.setValueAtTime(lpFreqStart, 0);
filter.frequency.linearRampToValueAtTime(lpFreqEnd, lpFreqEndAt);
player.connect(filter);
filter.connect(context.destination);
player.start();
context.oncomplete = function (event) {
callback(event.renderedBuffer);
};
context.startRendering();
window.filterNode = filter;
};
/** @private
@param {!AudioBuffer} buffer
@return {!Array.<!Float32Array>} An array containing the Float32Array of each channel's samples. */
var getAllChannelData = function (buffer) {
var channels = [];
for (var i = 0; i < buffer.numberOfChannels; i++) {
channels[i] = buffer.getChannelData(i);
}
return channels;
};
/** @private
@return {number} A random number from -1 to 1. */
var randomSample = function () {
return Math.random() * 2 - 1;
};
export default reverbGen;
+14 -20
View File
@@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
import './feedbackdelay.mjs';
import './reverb.mjs';
import './vowel.mjs';
import { clamp } from './util.mjs';
import { clamp, getFrequency } from './util.mjs';
import workletsUrl from './worklets.mjs?url';
import { createFilter, gainNode } from './helpers.mjs';
import { map } from 'nanostores';
@@ -107,25 +107,17 @@ function getDelay(orbit, delaytime, delayfeedback, t) {
}
let reverbs = {};
function getReverb(orbit, duration = 2, fade, lp, dim) {
// If no reverb has been created for a given orbit, create one
function getReverb(orbit, duration = 2) {
if (!reverbs[orbit]) {
const ac = getAudioContext();
const reverb = ac.createReverb(duration, fade, lp, dim);
const reverb = ac.createReverb(duration);
reverb.connect(getDestination());
reverbs[orbit] = reverb;
}
if (
reverbs[orbit].duration !== duration ||
reverbs[orbit].fade !== fade ||
reverbs[orbit].lp !== lp ||
reverbs[orbit].dim !== dim
) {
reverbs[orbit].generate(duration, fade, lp, dim);
if (reverbs[orbit].duration !== duration) {
reverbs[orbit] = reverbs[orbit].setDuration(duration);
reverbs[orbit].duration = duration;
}
return reverbs[orbit];
}
@@ -223,11 +215,10 @@ export const superdough = async (value, deadline, hapDuration) => {
delaytime = 0.25,
orbit = 1,
room,
roomfade = 0.1,
roomlp = 15000,
roomdim = 1000,
roomsize = 2,
size = 2,
velocity = 1,
amh, // amplitude modulation harmonicity
ami = 4, // amplitude modulation index
analyze, // analyser wet
fft = 8, // fftSize 0 - 10
} = value;
@@ -268,6 +259,9 @@ export const superdough = async (value, deadline, hapDuration) => {
// gain stage
chain.push(gainNode(gain));
amh !== undefined && chain.push(getAM(getFrequency(value), amh, ami));
// filters
if (cutoff !== undefined) {
let lp = () =>
createFilter(
@@ -364,8 +358,8 @@ export const superdough = async (value, deadline, hapDuration) => {
}
// reverb
let reverbSend;
if (room > 0 && roomsize > 0) {
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim);
if (room > 0 && size > 0) {
const reverbNode = getReverb(orbit, size);
reverbSend = effectSend(post, reverbNode, room);
}
+75 -98
View File
@@ -1,9 +1,8 @@
import { midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext } from './superdough.mjs';
import { gainNode, getEnvelope, getExpEnvelope } from './helpers.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
const mod = (freq, range = 1, type = 'sine') => {
export const mod = (freq, range = 1, type = 'sine') => {
const ctx = getAudioContext();
const osc = ctx.createOscillator();
osc.type = type;
@@ -21,25 +20,74 @@ const fm = (osc, harmonicityRatio, modulationIndex, wave = 'sine') => {
return mod(modfreq, modgain, wave);
};
const waveforms = ['sine', 'square', 'triangle', 'sawtooth'];
const noises = ['pink', 'white', 'brown'];
export function registerSynthSounds() {
[...waveforms, ...noises].forEach((s) => {
['sine', 'square', 'triangle', 'sawtooth'].forEach((wave) => {
registerSound(
s,
wave,
(t, value, onended) => {
// destructure adsr here, because the default should be different for synths and samples
let { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = value;
let sound;
if (waveforms.includes(s)) {
sound = getOscillator(s, t, value);
} else {
sound = getNoiseOscillator(s, t);
let {
attack = 0.001,
decay = 0.05,
sustain = 0.6,
release = 0.01,
fmh: fmHarmonicity = 1,
fmi: fmModulationIndex,
fmenv: fmEnvelopeType = 'lin',
fmattack: fmAttack,
fmdecay: fmDecay,
fmsustain: fmSustain,
fmrelease: fmRelease,
fmvelocity: fmVelocity,
fmwave: fmWaveform = 'sine',
vib = 0,
vibmod = 0.5,
} = value;
let { n, note, freq } = value;
// with synths, n and note are the same thing
note = note || 36;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
// maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default)
// make oscillator
const { node: o, stop } = getOscillator({
t,
s: wave,
freq,
vib,
vibmod,
partials: n,
});
let { node: o, stop, triggerRelease } = sound;
// FM + FM envelope
let stopFm, fmEnvelope;
if (fmModulationIndex) {
const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex, fmWaveform);
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
// no envelope by default
modulator.connect(o.frequency);
} else {
fmAttack = fmAttack ?? 0.001;
fmDecay = fmDecay ?? 0.001;
fmSustain = fmSustain ?? 1;
fmRelease = fmRelease ?? 0.001;
fmVelocity = fmVelocity ?? 1;
fmEnvelope = getEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
if (fmEnvelopeType === 'exp') {
fmEnvelope = getExpEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
fmEnvelope.node.maxValue = fmModulationIndex * 2;
fmEnvelope.node.minValue = 0.00001;
}
modulator.connect(fmEnvelope.node);
fmEnvelope.node.connect(o.frequency);
}
stopFm = stop;
}
// turn down
const g = gainNode(0.3);
@@ -56,9 +104,10 @@ export function registerSynthSounds() {
node: o.connect(g).connect(envelope),
stop: (releaseTime) => {
releaseEnvelope(releaseTime);
triggerRelease?.(releaseTime);
fmEnvelope?.stop(releaseTime);
let end = releaseTime + release;
stop(end);
stopFm?.(end);
},
};
},
@@ -97,108 +146,36 @@ export function waveformN(partials, type) {
return osc;
}
// expects one of waveforms as s
export function getOscillator(
s,
t,
{
n: partials,
note,
freq,
vib = 0,
vibmod = 0.5,
noise = 0,
// fm
fmh: fmHarmonicity = 1,
fmi: fmModulationIndex,
fmenv: fmEnvelopeType = 'lin',
fmattack: fmAttack,
fmdecay: fmDecay,
fmsustain: fmSustain,
fmrelease: fmRelease,
fmvelocity: fmVelocity,
fmwave: fmWaveform = 'sine',
},
) {
let ac = getAudioContext();
export function getOscillator({ s, freq, t, vib, vibmod, partials }) {
// Make oscillator with partial count
let o;
// If no partials are given, use stock waveforms
if (!partials || s === 'sine') {
o = getAudioContext().createOscillator();
o.type = s || 'triangle';
}
// generate custom waveform if partials are given
else {
} else {
o = waveformN(partials, s);
}
// get frequency from note...
note = note || 36;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
// set frequency
o.frequency.value = Number(freq);
o.start(t);
// FM
let stopFm, fmEnvelope;
if (fmModulationIndex) {
const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex, fmWaveform);
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
// no envelope by default
modulator.connect(o.frequency);
} else {
fmAttack = fmAttack ?? 0.001;
fmDecay = fmDecay ?? 0.001;
fmSustain = fmSustain ?? 1;
fmRelease = fmRelease ?? 0.001;
fmVelocity = fmVelocity ?? 1;
fmEnvelope = getEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
if (fmEnvelopeType === 'exp') {
fmEnvelope = getExpEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
fmEnvelope.node.maxValue = fmModulationIndex * 2;
fmEnvelope.node.minValue = 0.00001;
}
modulator.connect(fmEnvelope.node);
fmEnvelope.node.connect(o.frequency);
}
stopFm = stop;
}
// Additional oscillator for vibrato effect
let vibratoOscillator;
let vibrato_oscillator;
if (vib > 0) {
vibratoOscillator = getAudioContext().createOscillator();
vibratoOscillator.frequency.value = vib;
vibrato_oscillator = getAudioContext().createOscillator();
vibrato_oscillator.frequency.value = vib;
const gain = getAudioContext().createGain();
// Vibmod is the amount of vibrato, in semitones
gain.gain.value = vibmod * 100;
vibratoOscillator.connect(gain);
vibrato_oscillator.connect(gain);
gain.connect(o.detune);
vibratoOscillator.start(t);
}
let noiseMix;
if (noise) {
noiseMix = getNoiseMix(o, noise, t);
vibrato_oscillator.start(t);
}
return {
node: noiseMix?.node || o,
node: o,
stop: (time) => {
vibratoOscillator?.stop(time);
noiseMix?.stop(time);
stopFm?.(time);
vibrato_oscillator?.stop(time);
o.stop(time);
},
triggerRelease: (time) => {
fmEnvelope?.stop(time);
},
};
}
+18
View File
@@ -51,3 +51,21 @@ export const valueToMidi = (value, fallbackValue) => {
}
return fallbackValue;
};
export const getFrequency = (value) => {
// if value is number => interpret as midi number as long as its not marked as frequency
if (typeof value === 'object') {
if (value.freq) {
return value.freq;
}
return getFreq(value.note || value.n || value.value || 36);
}
if (typeof value === 'number' && context.type !== 'frequency') {
value = midiToFreq(hap.value);
} else if (typeof value === 'string' && isNote(value)) {
value = midiToFreq(noteToMidi(hap.value));
} else if (typeof value !== 'number') {
throw new Error('not a note or frequency: ' + value);
}
return value;
};
+2 -2
View File
@@ -20,7 +20,7 @@ export const getZZFX = (value, t) => {
pitchJump = 0,
pitchJumpTime = 0,
lfo = 0,
znoise = 0,
noise = 0,
zmod = 0,
zcrush = 0,
zdelay = 0,
@@ -54,7 +54,7 @@ export const getZZFX = (value, t) => {
pitchJump,
pitchJumpTime,
lfo,
znoise,
noise,
zmod,
zcrush,
zdelay,
+6 -36
View File
@@ -5,7 +5,7 @@ import { isNoteWithOctave } from '@strudel.cycles/core';
import { getLeafLocations } from '@strudel.cycles/mini';
export function transpiler(input, options = {}) {
const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options;
const { wrapAsync = false, addReturn = true, emitMiniLocations = true } = options;
let ast = parse(input, {
ecmaVersion: 2022,
@@ -16,9 +16,9 @@ export function transpiler(input, options = {}) {
let miniLocations = [];
const collectMiniLocations = (value, node) => {
const leafLocs = getLeafLocations(`"${value}"`, node.start); // stimmt!
//const withOffset = leafLocs.map((offsets) => offsets.map((o) => o + node.start));
miniLocations = miniLocations.concat(leafLocs);
};
let widgets = [];
walk(ast, {
enter(node, parent /* , prop, index */) {
@@ -35,18 +35,6 @@ export function transpiler(input, options = {}) {
emitMiniLocations && collectMiniLocations(value, node);
return this.replace(miniWithLocation(value, node));
}
if (isWidgetFunction(node)) {
emitWidgets &&
widgets.push({
from: node.arguments[0].start,
to: node.arguments[0].end,
value: node.arguments[0].raw, // don't use value!
min: node.arguments[1]?.value ?? 0,
max: node.arguments[2]?.value ?? 1,
step: node.arguments[3]?.value,
});
return this.replace(widgetWithLocation(node));
}
// TODO: remove pseudo note variables?
if (node.type === 'Identifier' && isNoteWithOctave(node.name)) {
this.skip();
@@ -76,14 +64,15 @@ export function transpiler(input, options = {}) {
if (!emitMiniLocations) {
return { output };
}
return { output, miniLocations, widgets };
return { output, miniLocations };
}
function isStringWithDoubleQuotes(node, locations, code) {
if (node.type !== 'Literal') {
const { raw, type } = node;
if (type !== 'Literal') {
return false;
}
return node.raw[0] === '"';
return raw[0] === '"';
}
function isBackTickString(node, parent) {
@@ -105,22 +94,3 @@ function miniWithLocation(value, node) {
optional: false,
};
}
// these functions are connected to @strudel/codemirror -> slider.mjs
// maybe someday there will be pluggable transpiler functions, then move this there
function isWidgetFunction(node) {
return node.type === 'CallExpression' && node.callee.name === 'slider';
}
function widgetWithLocation(node) {
const id = 'slider_' + node.arguments[0].start; // use loc of first arg for id
// add loc as identifier to first argument
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
node.arguments.unshift({
type: 'Literal',
value: id,
raw: id,
});
node.callee.name = 'sliderWithID';
return node;
}
+1 -5
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as strudel from '@strudel.cycles/core';
import { superdough, getAudioContext, setLogger, doughTrigger } from 'superdough';
import { superdough, getAudioContext, setLogger } from 'superdough';
const { Pattern, logger } = strudel;
setLogger(logger);
@@ -35,7 +35,3 @@ export function webaudioScheduler(options = {}) {
onTrigger: strudel.getTrigger({ defaultOutput, getTime }),
});
}
Pattern.prototype.dough = function () {
return this.onTrigger(doughTrigger, 1);
};
-18
View File
@@ -588,9 +588,6 @@ importers:
'@strudel.cycles/xen':
specifier: workspace:*
version: link:../packages/xen
'@strudel/codemirror':
specifier: workspace:*
version: link:../packages/codemirror
'@strudel/desktopbridge':
specifier: workspace:*
version: link:../packages/desktopbridge
@@ -1429,7 +1426,6 @@ packages:
/@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.21.5):
resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==}
engines: {node: '>=6.9.0'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -1445,7 +1441,6 @@ packages:
/@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.21.5):
resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==}
engines: {node: '>=6.9.0'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -1459,7 +1454,6 @@ packages:
/@babel/plugin-proposal-class-static-block@7.20.7(@babel/core@7.21.5):
resolution: {integrity: sha512-AveGOoi9DAjUYYuUAG//Ig69GlazLnoyzMw68VCDux+c1tsnnH/OkYcpz/5xzMkEFC6UxjR5Gw1c+iY2wOGVeQ==}
engines: {node: '>=6.9.0'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-static-block instead.
peerDependencies:
'@babel/core': ^7.12.0
dependencies:
@@ -1474,7 +1468,6 @@ packages:
/@babel/plugin-proposal-dynamic-import@7.18.6(@babel/core@7.21.5):
resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==}
engines: {node: '>=6.9.0'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-dynamic-import instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -1486,7 +1479,6 @@ packages:
/@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.21.5):
resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==}
engines: {node: '>=6.9.0'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -1498,7 +1490,6 @@ packages:
/@babel/plugin-proposal-json-strings@7.18.6(@babel/core@7.21.5):
resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==}
engines: {node: '>=6.9.0'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-json-strings instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -1510,7 +1501,6 @@ packages:
/@babel/plugin-proposal-logical-assignment-operators@7.20.7(@babel/core@7.21.5):
resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==}
engines: {node: '>=6.9.0'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -1522,7 +1512,6 @@ packages:
/@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.21.5):
resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==}
engines: {node: '>=6.9.0'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -1534,7 +1523,6 @@ packages:
/@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.21.5):
resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==}
engines: {node: '>=6.9.0'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -1546,7 +1534,6 @@ packages:
/@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.21.5):
resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==}
engines: {node: '>=6.9.0'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -1561,7 +1548,6 @@ packages:
/@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.21.5):
resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==}
engines: {node: '>=6.9.0'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -1573,7 +1559,6 @@ packages:
/@babel/plugin-proposal-optional-chaining@7.20.7(@babel/core@7.21.5):
resolution: {integrity: sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ==}
engines: {node: '>=6.9.0'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -1586,7 +1571,6 @@ packages:
/@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.21.5):
resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==}
engines: {node: '>=6.9.0'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -1600,7 +1584,6 @@ packages:
/@babel/plugin-proposal-private-property-in-object@7.20.5(@babel/core@7.21.5):
resolution: {integrity: sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ==}
engines: {node: '>=6.9.0'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -1616,7 +1599,6 @@ packages:
/@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.21.5):
resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==}
engines: {node: '>=4'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
-113
View File
@@ -99,7 +99,6 @@ version = "0.1.0"
dependencies = [
"midir",
"rosc",
"rusty_link",
"serde",
"serde_json",
"tauri",
@@ -164,28 +163,6 @@ version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
[[package]]
name = "bindgen"
version = "0.63.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36d860121800b2a9a94f9b5604b332d5cffb234ce17609ea479d723dbc9d3885"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"lazy_static",
"lazycell",
"log",
"peeking_take_while",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn 1.0.109",
"which",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -308,15 +285,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfb"
version = "0.7.3"
@@ -366,26 +334,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "clang-sys"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "cmake"
version = "0.1.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130"
dependencies = [
"cc",
]
[[package]]
name = "cocoa"
version = "0.24.1"
@@ -689,12 +637,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
[[package]]
name = "either"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "embed-resource"
version = "2.1.1"
@@ -1489,28 +1431,12 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.146"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b"
[[package]]
name = "libloading"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
dependencies = [
"cfg-if",
"winapi",
]
[[package]]
name = "line-wrap"
version = "0.1.1"
@@ -1888,12 +1814,6 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "peeking_take_while"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
[[package]]
name = "percent-encoding"
version = "2.3.0"
@@ -2286,12 +2206,6 @@ version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_version"
version = "0.4.0"
@@ -2321,16 +2235,6 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06"
[[package]]
name = "rusty_link"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "212e275351a46badd88b67ab8a4a8e17580317c728c361299b245cb51debe81c"
dependencies = [
"bindgen",
"cmake",
]
[[package]]
name = "ryu"
version = "1.0.13"
@@ -2524,12 +2428,6 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380"
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
@@ -3477,17 +3375,6 @@ dependencies = [
"windows-metadata",
]
[[package]]
name = "which"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269"
dependencies = [
"either",
"libc",
"once_cell",
]
[[package]]
name = "winapi"
version = "0.3.9"
-1
View File
@@ -21,7 +21,6 @@ tauri = { version = "1.4.0", features = ["fs-all"] }
midir = "0.9.1"
tokio = { version = "1.29.0", features = ["full"] }
rosc = "0.10.1"
rusty_link = "0.3.6"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
+1 -2
View File
@@ -4,8 +4,7 @@ Rust source files for building native desktop apps using Tauri
## Usage
- Install [Rust](https://rustup.rs/) on your system.
- Install `cmake` on your system. OSX: `brew install cmake`, Linux: `sudo apt-get install cmake`
Install [Rust](https://rustup.rs/) on your system.
From the project root:
-175
View File
@@ -1,175 +0,0 @@
use std::time::{ Duration, SystemTime, UNIX_EPOCH };
use rusty_link::{ AblLink, SessionState };
use std::sync::Arc;
use tokio::sync::Mutex;
use serde::Deserialize;
use std::thread::sleep;
use crate::loggerbridge::Logger;
use tauri::Window;
fn bpm_to_cps(bpm: f64) -> f64 {
let cpm = bpm / 4.0;
return cpm / 60.0;
}
fn cps_to_bpm(cps: f64) -> f64 {
let cpm = cps * 60.0;
return cpm * 4.0;
}
fn current_unix_time() -> Duration {
let current_unix_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
return current_unix_time;
}
#[derive(Deserialize, Clone, serde::Serialize)]
pub struct LinkMsg {
pub started: bool,
pub cps: f64,
pub timestamp: u64,
pub phase: f64,
}
pub struct AbeLinkStateContainer {
pub abelink: Arc<Mutex<AbeLinkState>>,
}
pub struct AbeLinkState {
pub link: AblLink,
pub session_state: SessionState,
pub running: bool,
pub quantum: f64,
pub window: Arc<Window>,
}
impl AbeLinkState {
pub fn new(window: Arc<Window>) -> Self {
Self {
link: AblLink::new(120.0),
session_state: SessionState::new(),
running: true,
quantum: 4.0,
window,
}
}
pub fn unix_time_at_next_phase(&self) -> u64 {
let link_time_stamp = self.link.clock_micros();
let quantum = self.quantum;
let beat = self.session_state.beat_at_time(link_time_stamp, quantum);
let phase = self.session_state.phase_at_time(link_time_stamp, quantum);
let internal_time_at_next_phase = self.session_state.time_at_beat(beat + (quantum - phase), quantum);
let time_offset = Duration::from_micros((internal_time_at_next_phase - link_time_stamp) as u64);
let current_unix_time = current_unix_time();
let unix_time_at_next_phase = (current_unix_time + time_offset).as_millis();
return unix_time_at_next_phase as u64;
}
pub fn cps(&self) -> f64 {
let bpm = self.session_state.tempo();
let cps = bpm_to_cps(bpm);
return cps;
}
pub fn capture_app_state(&mut self) {
self.link.capture_app_session_state(&mut self.session_state);
}
pub fn commit_app_state(&mut self) {
self.link.commit_app_session_state(&self.session_state);
}
pub fn send(&self, payload: LinkMsg) {
let _ = self.window.emit("abelink-event", payload);
}
pub fn send_started(&self) {
let cps = self.cps();
let started = self.session_state.is_playing();
let payload = LinkMsg {
cps,
started,
timestamp: self.unix_time_at_next_phase(),
phase: 0.0,
};
self.send(payload);
}
pub fn send_cps(&self) {
let cps = self.cps();
let started = self.session_state.is_playing();
let phase = self.session_state.phase_at_time(self.link.clock_micros(), self.quantum);
let payload = LinkMsg {
cps,
started,
timestamp: current_unix_time().as_millis() as u64,
phase,
};
self.send(payload);
}
pub fn send_phase(&self) {
self.send_started();
}
}
pub fn init(_logger: Logger, abelink: Arc<Mutex<AbeLinkState>>) {
tauri::async_runtime::spawn(async move {
let mut prev_is_started = false;
let mut prev_cps = 0.0;
let mut time_since_last_phase_send = 0;
let sleep_time = 10;
/* .......................................................................
Evaluate Abelink State and send messages back to JS side when needed.
........................................................................*/
loop {
let mut state = abelink.lock().await;
state.capture_app_state();
if state.link.is_enabled() == false {
state.link.enable(true);
state.link.enable_start_stop_sync(true);
}
let started = state.session_state.is_playing();
if started != prev_is_started {
state.send_started();
prev_is_started = started;
} else if state.cps() != prev_cps && state.cps() != 0.0 {
state.send_cps();
prev_cps = state.cps();
// a phase sync message needs to be sent to strudel every 30 seconds to keep clock drift at bay
} else if time_since_last_phase_send > 30000 {
state.send_phase();
time_since_last_phase_send = 0;
}
drop(state);
sleep(Duration::from_millis(sleep_time));
time_since_last_phase_send = time_since_last_phase_send + sleep_time;
}
});
}
// Called from JS
#[tauri::command]
pub async fn sendabelinkmsg(linkmsg: LinkMsg, state: tauri::State<'_, AbeLinkStateContainer>) -> Result<(), String> {
let mut abelink = state.abelink.lock().await;
abelink.capture_app_state();
let started = abelink.session_state.is_playing();
let time_stamp = abelink.link.clock_micros();
let quantum = abelink.quantum;
let linkmsg_bpm = cps_to_bpm(linkmsg.cps);
if linkmsg.started != started {
abelink.session_state.set_is_playing_and_request_beat_at_time(linkmsg.started, time_stamp as u64, 0.0, quantum);
}
if linkmsg_bpm != abelink.session_state.tempo() && linkmsg_bpm != 0.0 {
abelink.session_state.set_tempo(linkmsg_bpm, time_stamp);
}
abelink.commit_app_state();
drop(abelink);
Ok(())
}
+3 -20
View File
@@ -4,10 +4,8 @@
mod midibridge;
mod oscbridge;
mod loggerbridge;
mod ablelinkbridge;
use std::sync::Arc;
use ablelinkbridge::AbeLinkState;
use loggerbridge::Logger;
use tauri::Manager;
use tokio::sync::mpsc;
@@ -23,7 +21,6 @@ fn main() {
let (async_output_transmitter_midi, async_output_receiver_midi) = mpsc::channel(1);
let (async_input_transmitter_osc, async_input_receiver_osc) = mpsc::channel(1);
let (async_output_transmitter_osc, async_output_receiver_osc) = mpsc::channel(1);
tauri::Builder
::default()
.manage(midibridge::AsyncInputTransmit {
@@ -32,31 +29,17 @@ fn main() {
.manage(oscbridge::AsyncInputTransmit {
inner: Mutex::new(async_input_transmitter_osc),
})
.invoke_handler(tauri::generate_handler![midibridge::sendmidi, oscbridge::sendosc, ablelinkbridge::sendabelinkmsg])
.invoke_handler(tauri::generate_handler![midibridge::sendmidi, oscbridge::sendosc])
.setup(|app| {
let window = Arc::new(app.get_window("main").unwrap());
let logger = Logger { window: window.clone() };
let logger = Logger { window };
midibridge::init(
logger.clone(),
async_input_receiver_midi,
async_output_receiver_midi,
async_output_transmitter_midi
);
oscbridge::init(
logger.clone(),
async_input_receiver_osc,
async_output_receiver_osc,
async_output_transmitter_osc
);
// This state must be declared in the setup so it can be shared between invoked commands and the initialized function
let abelink = Arc::new(Mutex::new(AbeLinkState::new(window)));
app.manage(ablelinkbridge::AbeLinkStateContainer {
abelink: abelink.clone(),
});
ablelinkbridge::init(logger.clone(), abelink);
oscbridge::init(logger, async_input_receiver_osc, async_output_receiver_osc, async_output_transmitter_osc);
Ok(())
})
.run(tauri::generate_context!())
+8 -108
View File
@@ -2959,15 +2959,6 @@ exports[`runs examples > example "never" example index 0 1`] = `
]
`;
exports[`runs examples > example "noise" example index 0 1`] = `
[
"[ (0/1 → 1/1) ⇝ 2/1 | s:white ]",
"[ 0/1 ⇜ (1/1 → 2/1) | s:white ]",
"[ (2/1 → 3/1) ⇝ 4/1 | s:pink ]",
"[ 2/1 ⇜ (3/1 → 4/1) | s:pink ]",
]
`;
exports[`runs examples > example "note" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c ]",
@@ -3663,107 +3654,16 @@ exports[`runs examples > example "room" example index 1 1`] = `
]
`;
exports[`runs examples > example "roomdim" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd room:0.5 roomlp:10000 roomdim:8000 ]",
"[ 1/2 → 1/1 | s:sd room:0.5 roomlp:10000 roomdim:8000 ]",
"[ 1/1 → 3/2 | s:bd room:0.5 roomlp:10000 roomdim:8000 ]",
"[ 3/2 → 2/1 | s:sd room:0.5 roomlp:10000 roomdim:8000 ]",
"[ 2/1 → 5/2 | s:bd room:0.5 roomlp:10000 roomdim:8000 ]",
"[ 5/2 → 3/1 | s:sd room:0.5 roomlp:10000 roomdim:8000 ]",
"[ 3/1 → 7/2 | s:bd room:0.5 roomlp:10000 roomdim:8000 ]",
"[ 7/2 → 4/1 | s:sd room:0.5 roomlp:10000 roomdim:8000 ]",
]
`;
exports[`runs examples > example "roomdim" example index 1 1`] = `
[
"[ 0/1 → 1/2 | s:bd room:0.5 roomlp:5000 roomdim:400 ]",
"[ 1/2 → 1/1 | s:sd room:0.5 roomlp:5000 roomdim:400 ]",
"[ 1/1 → 3/2 | s:bd room:0.5 roomlp:5000 roomdim:400 ]",
"[ 3/2 → 2/1 | s:sd room:0.5 roomlp:5000 roomdim:400 ]",
"[ 2/1 → 5/2 | s:bd room:0.5 roomlp:5000 roomdim:400 ]",
"[ 5/2 → 3/1 | s:sd room:0.5 roomlp:5000 roomdim:400 ]",
"[ 3/1 → 7/2 | s:bd room:0.5 roomlp:5000 roomdim:400 ]",
"[ 7/2 → 4/1 | s:sd room:0.5 roomlp:5000 roomdim:400 ]",
]
`;
exports[`runs examples > example "roomfade" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd room:0.5 roomlp:10000 roomfade:0.5 ]",
"[ 1/2 → 1/1 | s:sd room:0.5 roomlp:10000 roomfade:0.5 ]",
"[ 1/1 → 3/2 | s:bd room:0.5 roomlp:10000 roomfade:0.5 ]",
"[ 3/2 → 2/1 | s:sd room:0.5 roomlp:10000 roomfade:0.5 ]",
"[ 2/1 → 5/2 | s:bd room:0.5 roomlp:10000 roomfade:0.5 ]",
"[ 5/2 → 3/1 | s:sd room:0.5 roomlp:10000 roomfade:0.5 ]",
"[ 3/1 → 7/2 | s:bd room:0.5 roomlp:10000 roomfade:0.5 ]",
"[ 7/2 → 4/1 | s:sd room:0.5 roomlp:10000 roomfade:0.5 ]",
]
`;
exports[`runs examples > example "roomfade" example index 1 1`] = `
[
"[ 0/1 → 1/2 | s:bd room:0.5 roomlp:5000 roomfade:4 ]",
"[ 1/2 → 1/1 | s:sd room:0.5 roomlp:5000 roomfade:4 ]",
"[ 1/1 → 3/2 | s:bd room:0.5 roomlp:5000 roomfade:4 ]",
"[ 3/2 → 2/1 | s:sd room:0.5 roomlp:5000 roomfade:4 ]",
"[ 2/1 → 5/2 | s:bd room:0.5 roomlp:5000 roomfade:4 ]",
"[ 5/2 → 3/1 | s:sd room:0.5 roomlp:5000 roomfade:4 ]",
"[ 3/1 → 7/2 | s:bd room:0.5 roomlp:5000 roomfade:4 ]",
"[ 7/2 → 4/1 | s:sd room:0.5 roomlp:5000 roomfade:4 ]",
]
`;
exports[`runs examples > example "roomlp" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd room:0.5 roomlp:10000 ]",
"[ 1/2 → 1/1 | s:sd room:0.5 roomlp:10000 ]",
"[ 1/1 → 3/2 | s:bd room:0.5 roomlp:10000 ]",
"[ 3/2 → 2/1 | s:sd room:0.5 roomlp:10000 ]",
"[ 2/1 → 5/2 | s:bd room:0.5 roomlp:10000 ]",
"[ 5/2 → 3/1 | s:sd room:0.5 roomlp:10000 ]",
"[ 3/1 → 7/2 | s:bd room:0.5 roomlp:10000 ]",
"[ 7/2 → 4/1 | s:sd room:0.5 roomlp:10000 ]",
]
`;
exports[`runs examples > example "roomlp" example index 1 1`] = `
[
"[ 0/1 → 1/2 | s:bd room:0.5 roomlp:5000 ]",
"[ 1/2 → 1/1 | s:sd room:0.5 roomlp:5000 ]",
"[ 1/1 → 3/2 | s:bd room:0.5 roomlp:5000 ]",
"[ 3/2 → 2/1 | s:sd room:0.5 roomlp:5000 ]",
"[ 2/1 → 5/2 | s:bd room:0.5 roomlp:5000 ]",
"[ 5/2 → 3/1 | s:sd room:0.5 roomlp:5000 ]",
"[ 3/1 → 7/2 | s:bd room:0.5 roomlp:5000 ]",
"[ 7/2 → 4/1 | s:sd room:0.5 roomlp:5000 ]",
]
`;
exports[`runs examples > example "roomsize" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd room:0.8 roomsize:1 ]",
"[ 1/2 → 1/1 | s:sd room:0.8 roomsize:1 ]",
"[ 1/1 → 3/2 | s:bd room:0.8 roomsize:1 ]",
"[ 3/2 → 2/1 | s:sd room:0.8 roomsize:1 ]",
"[ 2/1 → 5/2 | s:bd room:0.8 roomsize:1 ]",
"[ 5/2 → 3/1 | s:sd room:0.8 roomsize:1 ]",
"[ 3/1 → 7/2 | s:bd room:0.8 roomsize:1 ]",
"[ 7/2 → 4/1 | s:sd room:0.8 roomsize:1 ]",
]
`;
exports[`runs examples > example "roomsize" example index 1 1`] = `
[
"[ 0/1 → 1/2 | s:bd room:0.8 roomsize:4 ]",
"[ 1/2 → 1/1 | s:sd room:0.8 roomsize:4 ]",
"[ 1/1 → 3/2 | s:bd room:0.8 roomsize:4 ]",
"[ 3/2 → 2/1 | s:sd room:0.8 roomsize:4 ]",
"[ 2/1 → 5/2 | s:bd room:0.8 roomsize:4 ]",
"[ 5/2 → 3/1 | s:sd room:0.8 roomsize:4 ]",
"[ 3/1 → 7/2 | s:bd room:0.8 roomsize:4 ]",
"[ 7/2 → 4/1 | s:sd room:0.8 roomsize:4 ]",
"[ 0/1 → 1/2 | s:bd room:0.8 size:0 ]",
"[ 1/2 → 1/1 | s:sd room:0.8 size:0 ]",
"[ 1/1 → 3/2 | s:bd room:0.8 size:1 ]",
"[ 3/2 → 2/1 | s:sd room:0.8 size:1 ]",
"[ 2/1 → 5/2 | s:bd room:0.8 size:2 ]",
"[ 5/2 → 3/1 | s:sd room:0.8 size:2 ]",
"[ 3/1 → 7/2 | s:bd room:0.8 size:4 ]",
"[ 7/2 → 4/1 | s:sd room:0.8 size:4 ]",
]
`;
-1
View File
@@ -28,7 +28,6 @@
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/osc": "workspace:*",
"@strudel.cycles/react": "workspace:*",
"@strudel/codemirror": "workspace:*",
"@strudel.cycles/serial": "workspace:*",
"@strudel.cycles/soundfonts": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
+5 -21
View File
@@ -183,40 +183,24 @@ global effects use the same chain for all events of the same orbit:
<JsDoc client:idle name="orbit" h={0} />
## Delay
### delay
## delay
<JsDoc client:idle name="delay" h={0} />
### delaytime
## delaytime
<JsDoc client:idle name="delaytime" h={0} />
### delayfeedback
## delayfeedback
<JsDoc client:idle name="delayfeedback" h={0} />
## Reverb
### room
## room
<JsDoc client:idle name="room" h={0} />
### roomsize
## roomsize
<JsDoc client:idle name="roomsize" h={0} />
### roomfade
<JsDoc client:idle name="roomfade" h={0} />
### roomlp
<JsDoc client:idle name="roomlp" h={0} />
### roomdim
<JsDoc client:idle name="roomdim" h={0} />
Next, we'll look at strudel's support for [Csound](/learn/csound).
-19
View File
@@ -23,25 +23,6 @@ The basic waveforms are `sine`, `sawtooth`, `square` and `triangle`, which can b
If you don't set a `sound` but a `note` the default value for `sound` is `triangle`!
## Noise
You can also use noise as a source by setting the waveform to: `white`, `pink` or `brown`. These are different
flavours of noise, here written from hard to soft.
<MiniRepl client:idle tune={`sound("<white pink brown>/2").scope()`} />
Here's a more musical example of how to use noise for hihats:
<MiniRepl
client:idle
tune={`sound("bd*2,<white pink brown>*8")
.decay(.04).sustain(0).scope()`}
/>
Some amount of pink noise can also be added to any oscillator by using the `noise` paremeter:
<MiniRepl client:idle tune={`note("c3").noise("<0.1 0.25 0.5>").scope()`} />
### Additive Synthesis
To tame the harsh sound of the basic waveforms, we can set the `n` control to limit the overtones of the waveform:
+7 -20
View File
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { cleanupDraw, cleanupUi, controls, evalScope, getDrawContext, logger, Cyclist } from '@strudel.cycles/core';
import { cleanupDraw, cleanupUi, controls, evalScope, getDrawContext, logger } from '@strudel.cycles/core';
import { CodeMirror, cx, flash, useHighlighting, useStrudel, useKeydown } from '@strudel.cycles/react';
import { getAudioContext, initAudioOnFirstClick, resetLoadedSounds, webaudioOutput } from '@strudel.cycles/webaudio';
import { createClient } from '@supabase/supabase-js';
@@ -22,8 +22,6 @@ import Loader from './Loader';
import { settingPatterns } from '../settings.mjs';
import { code2hash, hash2code } from './helpers.mjs';
import { isTauri } from '../tauri.mjs';
import { useWidgets } from '@strudel.cycles/react/src/hooks/useWidgets.mjs';
import { CyclistBridge } from '@strudel/desktopbridge/cyclistbridge.mjs';
const { latestCode } = settingsMap.get();
@@ -35,27 +33,25 @@ const supabase = createClient(
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBpZHhkc3hwaGxoempuem1pZnRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTYyMzA1NTYsImV4cCI6MTk3MTgwNjU1Nn0.bqlw7802fsWRnqU5BLYtmXk_k-D1VFmbkHMywWc15NM',
);
let modules = [
const modules = [
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/xen'),
import('@strudel.cycles/webaudio'),
import('@strudel/codemirror'),
import('@strudel.cycles/serial'),
import('@strudel.cycles/soundfonts'),
import('@strudel.cycles/csound'),
];
if (isTauri()) {
modules = modules.concat([
modules.concat([
import('@strudel/desktopbridge/loggerbridge.mjs'),
import('@strudel/desktopbridge/midibridge.mjs'),
import('@strudel/desktopbridge/oscbridge.mjs'),
import('@strudel/desktopbridge/cyclistbridge.mjs'),
]);
} else {
modules = modules.concat([import('@strudel.cycles/midi'), import('@strudel.cycles/osc')]);
modules.concat([import('@strudel.cycles/midi'), import('@strudel.cycles/osc')]);
}
const modulesLoading = evalScope(
@@ -132,7 +128,7 @@ export function Repl({ embedded = false }) {
} = useSettings();
const paintOptions = useMemo(() => ({ fontFamily }), [fontFamily]);
const { setWidgets } = useWidgets(view);
const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop, error } =
useStrudel({
initialCode: '// LOADING...',
@@ -146,7 +142,6 @@ export function Repl({ embedded = false }) {
},
afterEval: ({ code, meta }) => {
setMiniLocations(meta.miniLocations);
setWidgets(meta.widgets);
setPending(false);
setLatestCode(code);
window.location.hash = '#' + code2hash(code);
@@ -154,18 +149,10 @@ export function Repl({ embedded = false }) {
onEvalError: (err) => {
setPending(false);
},
onToggle: (play) => {
if (!play) {
cleanupDraw(false);
window.postMessage('strudel-stop');
} else {
window.postMessage('strudel-start');
}
},
onToggle: (play) => !play && cleanupDraw(false),
drawContext,
// drawTime: [0, 6],
paintOptions,
createCyclist: (p) => (isTauri() ? new CyclistBridge(p) : new Cyclist(p)),
});
// init code
@@ -225,7 +212,7 @@ export function Repl({ embedded = false }) {
const handleChangeCode = useCallback(
(c) => {
setCode(c);
//started && logger('[edit] code changed. hit ctrl+enter to update');
started && logger('[edit] code changed. hit ctrl+enter to update');
},
[started],
);