Merge branch 'main' into more-functions

This commit is contained in:
alex 2022-04-23 09:21:47 +01:00
commit f4dd549a41
32 changed files with 518 additions and 3952 deletions

View file

@ -275,14 +275,14 @@ const generic_params = [
const _name = (name, ...pats) => sequence(...pats).withValue((x) => ({ [name]: x }));
const _unionise = (func) =>
const _setter = (func) =>
function (...pats) {
return this.union(func(...pats));
return this.set(func(...pats));
};
generic_params.forEach(([type, name, description]) => {
controls[name] = (...pats) => _name(name, ...pats);
Pattern.prototype[name] = _unionise(controls[name]);
Pattern.prototype[name] = _setter(controls[name]);
});
export default controls;

View file

@ -3,7 +3,7 @@ import Fraction from './fraction.mjs';
import Hap from './hap.mjs';
import State from './state.mjs';
import { isNote, toMidi, compose, removeUndefineds, flatten, id, listRange, curry } from './util.mjs';
import { isNote, toMidi, compose, removeUndefineds, flatten, id, listRange, curry, mod } from './util.mjs';
export class Pattern {
// the following functions will get patternFactories as nested functions:
@ -182,11 +182,13 @@ export class Pattern {
const event_vals = pat_val.query(state.setSpan(hap_func.wholeOrPart()));
for (const hap_val of event_vals) {
const new_whole = hap_func.whole;
const new_part = hap_func.part.intersection_e(hap_val.part);
const new_value = hap_func.value(hap_val.value);
const new_context = hap_val.combineContext(hap_func);
const hap = new Hap(new_whole, new_part, new_value, new_context);
haps.push(hap);
const new_part = hap_func.part.intersection(hap_val.part);
if (new_part) {
const new_value = hap_func.value(hap_val.value);
const new_context = hap_val.combineContext(hap_func);
const hap = new Hap(new_whole, new_part, new_value, new_context);
haps.push(hap);
}
}
}
return haps;
@ -203,11 +205,13 @@ export class Pattern {
const hap_funcs = pat_func.query(state.setSpan(hap_val.wholeOrPart()));
for (const hap_func of hap_funcs) {
const new_whole = hap_val.whole;
const new_part = hap_func.part.intersection_e(hap_val.part);
const new_value = hap_func.value(hap_val.value);
const new_context = hap_val.combineContext(hap_func);
const hap = new Hap(new_whole, new_part, new_value, new_context);
haps.push(hap);
const new_part = hap_func.part.intersection(hap_val.part);
if (new_part) {
const new_value = hap_func.value(hap_val.value);
const new_context = hap_val.combineContext(hap_func);
const hap = new Hap(new_whole, new_part, new_value, new_context);
haps.push(hap);
}
}
}
return haps;
@ -243,9 +247,24 @@ export class Pattern {
);
}
_opleft(other, func) {
_opLeft(other, func) {
return this.fmap(func).appLeft(reify(other));
}
_opRight(other, func) {
return this.fmap(func).appRight(reify(other));
}
_opBoth(other, func) {
return this.fmap(func).appBoth(reify(other));
}
_opSqueeze(other, func) {
const otherPat = reify(other);
return this.fmap((a) => otherPat.fmap((b) => func(a)(b)))._squeezeJoin();
}
_opSqueezeFlip(other, func) {
const thisPat = this;
const otherPat = reify(other);
return otherPat.fmap((a) => thisPat.fmap((b) => func(b)(a)))._squeezeJoin();
}
_asNumber(silent = false) {
return this._withEvent((event) => {
@ -271,22 +290,6 @@ export class Pattern {
})._removeUndefineds();
}
add(other) {
return this._asNumber()._opleft(other, (a) => (b) => a + b);
}
sub(other) {
return this._asNumber()._opleft(other, (a) => (b) => a - b);
}
mul(other) {
return this._asNumber()._opleft(other, (a) => (b) => a * b);
}
div(other) {
return this._asNumber()._opleft(other, (a) => (b) => a / b);
}
round() {
return this._asNumber().fmap((v) => Math.round(v));
}
@ -320,10 +323,6 @@ export class Pattern {
return this._fromBipolar().range(min, max);
}
union(other) {
return this._opleft(other, (a) => (b) => Object.assign({}, a, b));
}
_bindWhole(choose_whole, func) {
const pat_val = this;
const query = function (state) {
@ -464,7 +463,7 @@ export class Pattern {
}
_compress(b, e) {
if (b > e || b > 1 || e > 1 || b < 0 || e < 0) {
if (b.gt(e) || b.gt(1) || e.gt(1) || b.lt(0) || e.lt(0)) {
return silence;
}
return this._fastGap(Fraction(1).div(e.sub(b)))._late(b);
@ -500,7 +499,7 @@ export class Pattern {
const slices = Array.from({ length: n }, (x, i) => i);
const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
const slicePat = slowcat(...slice_objects);
return this.union(slicePat)._fast(n);
return this.set(slicePat)._fast(n);
}
// cpm = cycles per minute
@ -669,7 +668,7 @@ export class Pattern {
}
stutWith(times, time, func) {
return stack(...listRange(0, times - 1).map((i) => func(this.late(i * time), i)));
return stack(...listRange(0, times - 1).map((i) => func(this.late(Fraction(time).mul(i)), i)));
}
stut(times, feedback, time) {
@ -678,7 +677,7 @@ export class Pattern {
// these might change with: https://github.com/tidalcycles/Tidal/issues/902
_echoWith(times, time, func) {
return stack(...listRange(0, times - 1).map((i) => func(this.late(i * time), i)));
return stack(...listRange(0, times - 1).map((i) => func(this.late(Fraction(time).mul(i)), i)));
}
_echo(times, time, feedback) {
@ -736,6 +735,49 @@ export class Pattern {
}
}
// pattern composers
const composers = {
set: [
(a) => (b) => {
// If an object is involved, do a union, discarding matching keys from a.
// Otherwise, just return b.
if (a instanceof Object || b instanceof Object) {
if (!a instanceof Object) {
a = { value: a };
}
if (!b instanceof Object) {
b = { value: b };
}
return Object.assign({}, a, b);
}
return b;
},
id,
],
add: [(a) => (b) => a + b, (x) => x._asNumber()],
sub: [(a) => (b) => a - b, (x) => x._asNumber()],
mul: [(a) => (b) => a * b, (x) => x._asNumber()],
div: [(a) => (b) => a / b, (x) => x._asNumber()],
};
for (const [name, op] of Object.entries(composers)) {
Pattern.prototype[name] = function (...other) {
return op[1](this)._opLeft(sequence(other), op[0]);
};
Pattern.prototype[name + 'Flip'] = function (...other) {
return op[1](this)._opRight(sequence(other), op[0]);
};
Pattern.prototype[name + 'Sect'] = function (...other) {
return op[1](this)._opBoth(sequence(other), op[0]);
};
Pattern.prototype[name + 'Squeeze'] = function (...other) {
return op[1](this)._opSqueeze(sequence(other), op[0]);
};
Pattern.prototype[name + 'SqueezeFlip'] = function (...other) {
return op[1](this)._opSqueezeFlip(sequence(other), op[0]);
};
}
// methods of Pattern that get callable factories
Pattern.prototype.patternified = [
'apply',
@ -798,7 +840,7 @@ export function slowcat(...pats) {
pats = pats.map(reify);
const query = function (state) {
const span = state.span;
const pat_n = Math.floor(span.begin) % pats.length;
const pat_n = mod(span.begin.sam(), pats.length);
const pat = pats[pat_n];
if (!pat) {
// pat_n can be negative, if the span is in the past..
@ -938,7 +980,7 @@ export const slow = curry((a, pat) => pat.slow(a));
export const struct = curry((a, pat) => pat.struct(a));
export const sub = curry((a, pat) => pat.sub(a));
export const superimpose = curry((array, pat) => pat.superimpose(...array));
export const union = curry((a, pat) => pat.union(a));
export const set = curry((a, pat) => pat.set(a));
export const when = curry((binary, f, pat) => pat.when(binary, f));
// problem: curried functions with spread arguments must have pat at the beginning

View file

@ -5,7 +5,7 @@ import { id } from './util.mjs';
export function steady(value) {
// A continuous value
return new Pattern((span) => Hap(undefined, span, value));
return new Pattern((state) => [new Hap(undefined, state.span, value)]);
}
export const signal = (func) => {

View file

@ -36,6 +36,9 @@ import {
id,
ply,
} from '../index.mjs';
import { steady } from '../signal.mjs';
//import { Time } from 'tone';
import pkg from 'tone';
const { Time } = pkg;
@ -47,6 +50,10 @@ const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, co
const third = Fraction(1, 3);
const twothirds = Fraction(2, 3);
const sameFirst = (a, b) => {
return assert.deepStrictEqual(a._sortEventsByPart().firstCycle(), b._sortEventsByPart().firstCycle());
};
describe('TimeSpan', function () {
describe('equals()', function () {
it('Should be equal to the same value', function () {
@ -108,6 +115,18 @@ describe('Hap', function () {
assert.deepStrictEqual(state3, { incrementme: 12 });
});
});
describe('wholeOrPart()', () => {
const ts1 = new TimeSpan(Fraction(0), Fraction(1));
const ts0_5 = new TimeSpan(Fraction(0), Fraction(0.5));
const continuousHap = new Hap(undefined, ts1, 'hello');
const discreteHap = new Hap(ts1, ts0_5, 'hello');
it('Can pick a whole', () => {
assert.deepStrictEqual(discreteHap.wholeOrPart(), ts1);
});
it('Can pick a part', () => {
assert.deepStrictEqual(continuousHap.wholeOrPart(), ts1);
});
});
});
describe('Pattern', function () {
@ -131,6 +150,33 @@ describe('Pattern', function () {
assert.equal(pure(3).add(pure(4)).query(st(0, 1))[0].value, 7);
});
});
describe('addFlip()', () => {
it('Can add things with structure from second pattern', () => {
sameFirst(sequence(1, 2).addFlip(4), sequence(5, 6).struct(true));
});
});
describe('addSqueeze()', () => {
it('Can add while squeezing the second pattern inside the events of the first', () => {
sameFirst(
sequence(1, [2, 3]).addSqueeze(sequence(10, 20, 30)),
sequence(
[11, 21, 31],
[
[12, 22, 32],
[13, 23, 33],
],
),
);
});
});
describe('addSqueezeFlip()', () => {
it('Can add while squeezing the first pattern inside the events of the second', () => {
sameFirst(
sequence(1, [2, 3]).addSqueezeFlip(10, 20, 30),
sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
);
});
});
describe('sub()', function () {
it('Can subtract things', function () {
assert.equal(pure(3).sub(pure(4)).query(st(0, 1))[0].value, -1);
@ -146,14 +192,50 @@ describe('Pattern', function () {
assert.equal(pure(3).div(pure(2)).firstCycle()[0].value, 1.5);
});
});
describe('union()', function () {
it('Can union things', function () {
describe('set()', function () {
it('Can set things in objects', function () {
assert.deepStrictEqual(
pure({ a: 4, b: 6 })
.union(pure({ c: 7 }))
.set(pure({ c: 7 }))
.firstCycle()[0].value,
{ a: 4, b: 6, c: 7 },
);
sameFirst(
sequence({ a: 1, b: 2 }, { a: 2, b: 2 }, { a: 3, b: 2 }).set({ a: 4, c: 5 }),
sequence({ a: 4, b: 2, c: 5 }).fast(3),
);
});
it('Can set things with plain values', function () {
sameFirst(sequence(1, 2, 3).set(4), sequence(4).fast(3));
});
describe('setFlip()', () => {
it('Can set things with structure from second pattern', () => {
sameFirst(sequence(1, 2).setFlip(4), pure(4).mask(true, true));
});
});
describe('setSqueeze()', () => {
it('Can squeeze one pattern inside the events of another', () => {
sameFirst(
sequence(1, [2, 3]).setSqueeze(sequence('a', 'b', 'c')),
sequence(
['a', 'b', 'c'],
[
['a', 'b', 'c'],
['a', 'b', 'c'],
],
),
);
sameFirst(
sequence(1, [2, 3]).setSqueeze('a', 'b', 'c'),
sequence(
['a', 'b', 'c'],
[
['a', 'b', 'c'],
['a', 'b', 'c'],
],
),
);
});
});
});
describe('stack()', function () {
@ -292,6 +374,15 @@ describe('Pattern', function () {
);
});
});
describe('fastcat()', function () {
it('Can go into negative time', function () {
sameFirst(
fastcat('a','b','c')
.late(1000000),
fastcat('a','b','c'),
);
});
});
describe('slowcat()', function () {
it('Can concatenate things slowly', function () {
assert.deepStrictEqual(
@ -399,7 +490,7 @@ describe('Pattern', function () {
});
});
describe('struct()', function () {
it('Can restructure a pattern', function () {
it('Can restructure a discrete pattern', function () {
assert.deepStrictEqual(sequence('a', 'b').struct(sequence(true, true, true)).firstCycle(), [
hap(ts(0, third), ts(0, third), 'a'),
hap(ts(third, twothirds), ts(third, 0.5), 'a'),
@ -425,6 +516,12 @@ describe('Pattern', function () {
sequence('a', ['a', silence], 'a').firstCycle(),
);
});
it('Can structure a continuous pattern', () => {
assert.deepStrictEqual(
steady('a').struct(true, [true, true]).firstCycle(),
sequence('a', ['a', 'a']).firstCycle(),
);
});
});
describe('mask()', function () {
it('Can fragment a pattern', function () {
@ -616,6 +713,18 @@ describe('Pattern', function () {
sequence(pure('a').fast(3), [pure('b').fast(3), pure('c').fast(3)]).firstCycle(),
);
});
it('Doesnt drop events in the 9th cycle', () => {
// fixed with https://github.com/tidalcycles/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a
assert.equal(sequence(1, 2, 3).ply(2).early(8).firstCycle().length, 6);
});
});
describe('striate', () => {
it('Can striate(2)', () => {
sameFirst(
sequence({ sound: 'a' }).striate(2),
sequence({ sound: 'a', begin: 0, end: 0.5 }, { sound: 'a', begin: 0.5, end: 1 }),
);
});
});
describe('chop', () => {
it('Can _chop(2)', () => {

View file

@ -1,5 +1,6 @@
import { strict as assert } from 'assert';
import { isNote, tokenizeNote, toMidi, mod, compose } from '../util.mjs';
import { pure } from '../pattern.mjs';
import { isNote, tokenizeNote, toMidi, fromMidi, mod, compose, getFrequency } from '../util.mjs';
describe('isNote', () => {
it('should recognize notes without accidentals', () => {
@ -64,6 +65,21 @@ describe('toMidi', () => {
assert.equal(toMidi('C##3'), 50);
});
});
describe('fromMidi', () => {
it('should turn midi into frequency', () => {
assert.equal(fromMidi(69), 440);
assert.equal(fromMidi(57), 220);
});
});
describe('getFrequency', () => {
it('should turn midi into frequency', () => {
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
assert.equal(getFrequency(happify('a4')), 440);
assert.equal(getFrequency(happify('a3')), 220);
assert.equal(getFrequency(happify(440, { type: 'frequency' })), 440); // TODO: migrate when values are objects..
assert.equal(getFrequency(happify(432, { type: 'frequency' })), 432);
});
});
describe('mod', () => {
it('should work like regular modulo with positive numbers', () => {

View file

@ -33,7 +33,7 @@ export class TimeSpan {
// (Note that the output timespan probably does not start *at* Time 0 --
// that only happens when the input Arc starts at an integral Time.)
const b = this.begin.cyclePos();
const e = b + (this.end - this.begin);
const e = b.add(this.end.sub(this.begin));
return new TimeSpan(b, e);
}
@ -81,8 +81,7 @@ export class TimeSpan {
// Like 'sect', but raises an exception if the timespans don't intersect.
const result = this.intersection(other);
if (result == undefined) {
// TODO - raise exception
// raise ValueError(f'TimeSpan {self} and TimeSpan {other} do not intersect')
throw 'TimeSpans do not intersect';
}
return result;
}

View file

@ -27,7 +27,7 @@ export const fromMidi = (n) => {
// modulo that works with negative numbers e.g. mod(-1, 3) = 2
// const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m);
export const mod = (n, m) => (n < 0 ? mod(n + m, m) : n % m);
export const mod = (n, m) => (n % m + m) % m;
export const getPlayableNoteValue = (event) => {
let { value: note, context } = event;
@ -40,6 +40,22 @@ export const getPlayableNoteValue = (event) => {
return note;
};
export const getFrequency = (event) => {
let { value, context } = event;
// if value is number => interpret as midi number as long as its not marked as frequency
if (typeof value === 'object' && value.freq) {
return value.freq;
}
if (typeof value === 'number' && context.type !== 'frequency') {
value = fromMidi(event.value);
} else if (typeof value === 'string' && isNote(value)) {
value = fromMidi(toMidi(event.value));
} else if (typeof value !== 'number') {
throw new Error('not a note or frequency:' + value);
}
return value;
};
// rotate array by n steps (to the left)
export const rotate = (arr, n) => arr.slice(n).concat(arr.slice(0, n));

View file

@ -21,6 +21,7 @@
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"contributors": ["Alex McLean <alex@slab.org>"],
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"

View file

@ -0,0 +1,4 @@
# @strudel.cycles/serial
This package adds webserial functionality to strudel Patterns, for e.g. sending messages to arduino microcontrollers.

View file

@ -0,0 +1,24 @@
{
"name": "@strudel.cycles/serial",
"version": "0.0.6",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"titdalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Alex McLean <alex@slab.org>",
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {}
}

View file

@ -0,0 +1,52 @@
import { Pattern, isPattern } from '@strudel.cycles/core';
var serialWriter;
var choosing = false;
export async function getWriter(br=115200) {
if (choosing) {
return;
}
choosing = true;
if (serialWriter) {
return serialWriter;
}
if ('serial' in navigator) {
const port = await navigator.serial.requestPort();
await port.open({ baudRate: br });
const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
const writer = textEncoder.writable.getWriter();
serialWriter = function (message) {
writer.write(message)
}
}
else {
throw('Webserial is not available in this browser.')
}
}
const latency = 0.1;
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
Pattern.prototype.serial = async function (...args) {
return this._withEvent((event) => {
if (!serialWriter) {
getWriter(...args);
}
const onTrigger = (time, event, currentTime) => {
var message = "";
if (typeof event.value === 'object') {
for (const [key, val] of Object.entries(event.value).flat()) {
message += `${key}:${val};`
}
}
else {
message = event.value;
}
const offset = (time - currentTime + latency) * 1000;
window.setTimeout(serialWriter, offset, message);
};
return event.setContext({ ...event.context, onTrigger });
});
};

View file

@ -117,157 +117,3 @@ export const highpass = (v) => new Filter(v, 'highpass');
export const adsr = (a, d = 0.1, s = 0.4, r = 0.01) => ({ envelope: { attack: a, decay: d, sustain: s, release: r } });
export const osc = (type) => ({ oscillator: { type } });
export const out = () => getDestination();
/*
You are entering experimental zone
*/
// the following code is an attempt to minimize tonejs code.. it is still an experiment
const chainable = function (instr) {
const _chain = instr.chain.bind(instr);
let chained = [];
instr.chain = (...args) => {
chained = chained.concat(args);
instr.disconnect(); // disconnect from destination / previous chain
return _chain(...chained, getDestination());
};
// shortcuts: chaining multiple won't work forn now.. like filter(1000).gain(0.5). use chain + native Tone calls instead
// instr.filter = (freq = 1000, type: BiquadFilterType = 'lowpass') =>
instr.filter = (freq = 1000, type = 'lowpass') =>
instr.chain(
new Filter(freq, type), // .Q.setValueAtTime(q, time);
);
instr.gain = (gain = 0.9) => instr.chain(new Gain(gain));
return instr;
};
// helpers
export const poly = (type) => {
const s = new PolySynth(Synth, { oscillator: { type } }).toDestination();
return chainable(s);
};
Pattern.prototype._poly = function (type = 'triangle') {
const instrumentConfig = {
oscillator: { type },
envelope: { attack: 0.01, decay: 0.01, sustain: 0.6, release: 0.01 },
};
if (!this.instrument) {
// create only once to keep the js heap happy
// this.instrument = new PolySynth(Synth, instrumentConfig).toDestination();
this.instrument = poly(type);
}
return this._withEvent((event) => {
const onTrigger = (time, event) => {
this.instrument.set(instrumentConfig);
this.instrument.triggerAttackRelease(event.value, event.duration, time);
};
return event.setContext({ ...event.context, instrumentConfig, onTrigger });
});
};
Pattern.prototype.define('poly', (type, pat) => pat.poly(type), { composable: true, patternified: true });
/*
You are entering danger zone
*/
// everything below is nice in theory, but not healthy for the JS heap, as nodes get recreated on every call
const getTrigger = (getChain, value) => (time, event) => {
const chain = getChain(); // make sure this returns a node that is connected toDestination // time
if (!isNote(value)) {
throw new Error('not a note: ' + value);
}
chain.triggerAttackRelease(value, event.duration, time);
setTimeout(() => {
// setTimeout is a little bit better compared to Transport.scheduleOnce
chain.dispose(); // mark for garbage collection
}, event.duration * 2000);
};
Pattern.prototype._synth = function (type = 'triangle') {
return this._withEvent((event) => {
const instrumentConfig = {
oscillator: { type },
envelope: { attack: 0.01, decay: 0.01, sustain: 0.6, release: 0.01 },
};
const getInstrument = () => {
const instrument = new Synth();
instrument.set(instrumentConfig);
return instrument;
};
const onTrigger = getTrigger(() => getInstrument().toDestination(), event.value);
return event.setContext({ ...event.context, getInstrument, instrumentConfig, onTrigger });
});
};
Pattern.prototype.adsr = function (attack = 0.01, decay = 0.01, sustain = 0.6, release = 0.01) {
return this._withEvent((event) => {
if (!event.context.getInstrument) {
throw new Error('cannot chain adsr: need instrument first (like synth)');
}
const instrumentConfig = { ...event.context.instrumentConfig, envelope: { attack, decay, sustain, release } };
const getInstrument = () => {
const instrument = event.context.getInstrument();
instrument.set(instrumentConfig);
return instrument;
};
const onTrigger = getTrigger(() => getInstrument().toDestination(), event.value);
return event.setContext({ ...event.context, getInstrument, instrumentConfig, onTrigger });
});
};
Pattern.prototype.chain = function (...effectGetters) {
return this._withEvent((event) => {
if (!event.context?.getInstrument) {
throw new Error('cannot chain: need instrument first (like synth)');
}
const chain = (event.context.chain || []).concat(effectGetters);
const getChain = () => {
const effects = chain.map((getEffect) => getEffect());
return event.context.getInstrument().chain(...effects, getDestination());
};
const onTrigger = getTrigger(getChain, event.value);
return event.setContext({ ...event.context, getChain, onTrigger, chain });
});
};
export const autofilter =
(freq = 1) =>
() =>
new AutoFilter(freq).start();
export const filter =
// (freq = 1, q = 1, type: BiquadFilterType = 'lowpass') =>
(freq = 1, q = 1, type = 'lowpass') =>
() =>
new Filter(freq, type); // .Q.setValueAtTime(q, time);
export const gain =
(gain = 0.9) =>
() =>
new Gain(gain);
Pattern.prototype._gain = function (g) {
return this.chain(gain(g));
};
// Pattern.prototype._filter = function (freq: number, q: number, type: BiquadFilterType = 'lowpass') {
Pattern.prototype._filter = function (freq, q, type = 'lowpass') {
return this.chain(filter(freq, q, type));
};
Pattern.prototype._autofilter = function (g) {
return this.chain(autofilter(g));
};
Pattern.prototype.define('synth', (type, pat) => pat.synth(type), { composable: true, patternified: true });
Pattern.prototype.define('gain', (gain, pat) => pat.synth(gain), { composable: true, patternified: true });
Pattern.prototype.define('filter', (cutoff, pat) => pat.filter(cutoff), { composable: true, patternified: true });
Pattern.prototype.define('autofilter', (cutoff, pat) => pat.filter(cutoff), { composable: true, patternified: true });

View file

@ -11,9 +11,12 @@ Loading...</textarea
>
<script type="module">
document.body.style = 'margin: 0';
import * as strudel from 'https://cdn.skypack.dev/@strudel.cycles/core';
import 'https://cdn.skypack.dev/@strudel.cycles/core/euclid.mjs';
import { Scheduler, getAudioContext } from 'https://cdn.skypack.dev/@strudel.cycles/webaudio@0.0.4';
// import * as strudel from '@strudel.cycles/core';
import * as strudel from '../../core/index.mjs';
import * as util from '../../core/util.mjs';
import '@strudel.cycles/core/euclid.mjs';
// import { Scheduler, getAudioContext } from 'https://cdn.skypack.dev/@strudel.cycles/webaudio@0.0.4';
import { Scheduler, getAudioContext } from '../index.mjs';
const { cat, State, TimeSpan } = strudel;
Object.assign(window, strudel); // add strudel to eval scope
@ -31,7 +34,7 @@ Loading...</textarea
.mul(slowcat(1,3/2,4/3,5/3).slow(8))
.fast(3)
.velocity(.5)
.osc(cat('sawtooth','square').fast(2))
.wave(cat('sawtooth','square').fast(2))
.adsr(0.01,.02,.5,0.1)
.filter('lowshelf',800,25)
.out()`;

View file

@ -1,11 +1,14 @@
{
"name": "@strudel.cycles/webaudio",
"version": "0.0.5",
"version": "0.0.6",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"directories": {
"example": "examples"
},
"scripts": {
"example": "npx parcel examples/repl.html"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"

View file

@ -1,4 +1,5 @@
import ClockWorker from './clockworker.mjs';
import { State, TimeSpan } from '@strudel.cycles/core';
class Scheduler {
worker;

View file

@ -1,11 +1,13 @@
import { Pattern } from '@strudel.cycles/core';
import { Pattern, getFrequency, patternify2 } from '@strudel.cycles/core';
import { Tone } from '@strudel.cycles/tone';
let audioContext;
// let audioContext;
export const getAudioContext = () => {
if (!audioContext) {
return Tone.getContext().rawContext;
/* if (!audioContext) {
audioContext = new AudioContext();
}
return audioContext;
return audioContext; */
};
const lookahead = 0.2;
@ -25,50 +27,61 @@ Pattern.prototype.withAudioNode = function (createAudioNode) {
return this._withEvent((event) => {
return event.setContext({
...event.context,
createAudioNode: (e) => createAudioNode(e, event.context.createAudioNode?.(event)),
createAudioNode: (t, e) => createAudioNode(t, e, event.context.createAudioNode?.(t, event)),
});
});
};
Pattern.prototype._osc = function (type) {
return this.withAudioNode((e) => {
Pattern.prototype._wave = function (type) {
return this.withAudioNode((t, e) => {
const osc = getAudioContext().createOscillator();
osc.type = type;
osc.frequency.value = e.value; // expects frequency..
osc.start(e.whole.begin.valueOf() + lookahead);
osc.stop(e.whole.end.valueOf() + lookahead); // release?
const f = getFrequency(e);
osc.frequency.value = f; // expects frequency..
const begin = t ?? e.whole.begin.valueOf() + lookahead;
const end = begin + e.duration;
osc.start(begin);
osc.stop(end); // release?
return osc;
});
};
Pattern.prototype.adsr = function (a = 0.01, d = 0.05, s = 1, r = 0.01) {
return this.withAudioNode((e, node) => {
return this.withAudioNode((t, e, node) => {
const velocity = e.context?.velocity || 1;
const envelope = adsr(a, d, s, r, velocity, e.whole.begin.valueOf() + lookahead, e.whole.end.valueOf() + lookahead);
const begin = t ?? e.whole.begin.valueOf() + lookahead;
const end = begin + e.duration + lookahead;
const envelope = adsr(a, d, s, r, velocity, begin, end);
node?.connect(envelope);
return envelope;
});
};
Pattern.prototype.filter = function (type = 'lowshelf', frequency = 1000, gain = 25) {
return this.withAudioNode((e, node) => {
Pattern.prototype._filter = function (type = 'lowpass', frequency = 1000) {
return this.withAudioNode((t, e, node) => {
const filter = getAudioContext().createBiquadFilter();
filter.type = type;
filter.frequency.value = frequency;
filter.gain.value = gain;
node?.connect(filter);
return filter;
});
};
Pattern.prototype.filter = function (type, frequency) {
return patternify2(Pattern.prototype._filter)(reify(type), reify(frequency), this);
};
Pattern.prototype.out = function () {
const master = getAudioContext().createGain();
master.gain.value = 0.1;
master.connect(getAudioContext().destination);
return this.withAudioNode((e, node) => {
return this.withAudioNode((t, e, node) => {
if (!node) {
console.warn('out: no source! call .osc() first');
}
node?.connect(master);
})._withEvent((event) => {
const onTrigger = (time, e) => e.context?.createAudioNode?.(time, e);
return event.setContext({ ...event.context, onTrigger });
});
};
Pattern.prototype.define('osc', (type, pat) => pat.osc(type), { patternified: true });
Pattern.prototype.define('wave', (type, pat) => pat.wave(type), { patternified: true });