mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 13:48:34 +00:00
Merge branch 'main' into serial-twiddles
This commit is contained in:
commit
6698fdb6e6
@ -7,6 +7,7 @@
|
||||
"test": "npm run test --workspaces --if-present && cd repl && npm run test",
|
||||
"bootstrap": "lerna bootstrap",
|
||||
"setup": "npm i && npm run bootstrap && cd repl && npm i && cd ../tutorial && npm i",
|
||||
"snapshot": "cd repl && npm run snapshot",
|
||||
"repl": "cd repl && npm run dev",
|
||||
"osc": "cd packages/osc && npm run server",
|
||||
"build": "rm -rf out && cd repl && npm run build && cd ../tutorial && npm run build",
|
||||
|
||||
@ -755,19 +755,23 @@ const generic_params = [
|
||||
|
||||
const _name = (name, ...pats) => sequence(...pats).withValue((x) => ({ [name]: x }));
|
||||
|
||||
const _setter = (func) =>
|
||||
const _setter = (func, name) =>
|
||||
function (...pats) {
|
||||
if (!pats.length) {
|
||||
return this.fmap((value) => ({ [name]: value }));
|
||||
}
|
||||
return this.set(func(...pats));
|
||||
};
|
||||
|
||||
generic_params.forEach(([type, name, description]) => {
|
||||
controls[name] = (...pats) => _name(name, ...pats);
|
||||
Pattern.prototype[name] = _setter(controls[name]);
|
||||
Pattern.prototype[name] = _setter(controls[name], name);
|
||||
});
|
||||
|
||||
// create custom param
|
||||
controls.createParam = (name) => {
|
||||
Pattern.prototype[name] = _setter(controls[name]);
|
||||
const func = (...pats) => _name(name, ...pats);
|
||||
Pattern.prototype[name] = _setter(func, name);
|
||||
return (...pats) => _name(name, ...pats);
|
||||
};
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
// this is a shortcut to eval code from a gist
|
||||
// why? to be able to shorten strudel code + e.g. be able to change instruments after links have been generated
|
||||
export default (route) =>
|
||||
fetch(`https://gist.githubusercontent.com/${route}?cachebust=${Date.now()}`)
|
||||
export default (route, cache = true) =>
|
||||
fetch(`https://gist.githubusercontent.com/${route}?cachebust=${cache ? '' : Date.now()}`)
|
||||
.then((res) => res.text())
|
||||
.then((code) => eval(code));
|
||||
|
||||
@ -86,11 +86,9 @@ export class Hap {
|
||||
}
|
||||
|
||||
showWhole() {
|
||||
return `${this.whole == undefined ? '~' : this.whole.show()}: ${this.value}`;
|
||||
}
|
||||
|
||||
showWhole() {
|
||||
return `${this.whole == undefined ? '~' : this.whole.show()}: ${this.value}`;
|
||||
return `${this.whole == undefined ? '~' : this.whole.show()}: ${
|
||||
typeof this.value === 'object' ? JSON.stringify(this.value) : this.value
|
||||
}`;
|
||||
}
|
||||
|
||||
combineContext(b) {
|
||||
|
||||
@ -15,5 +15,5 @@ export * from './state.mjs';
|
||||
export * from './timespan.mjs';
|
||||
export * from './util.mjs';
|
||||
export * from './speak.mjs';
|
||||
export * as gist from './gist.js';
|
||||
export { default as gist } from './gist.js';
|
||||
// export * from './value.mjs';
|
||||
|
||||
2
packages/core/package-lock.json
generated
2
packages/core/package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/core",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/core",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.2",
|
||||
"description": "Port of Tidal Cycles to JavaScript",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@ -1049,6 +1049,9 @@ export class Pattern {
|
||||
.unit('c')
|
||||
.slow(factor);
|
||||
}
|
||||
onTrigger(onTrigger) {
|
||||
return this._withHap((hap) => hap.setContext({ ...hap.context, onTrigger }));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - adopt value.mjs fully..
|
||||
@ -1386,6 +1389,7 @@ export function pr(args) {
|
||||
}
|
||||
|
||||
export const add = curry((a, pat) => pat.add(a));
|
||||
export const chop = curry((a, pat) => pat.chop(a))
|
||||
export const chunk = curry((a, pat) => pat.chunk(a));
|
||||
export const chunkBack = curry((a, pat) => pat.chunkBack(a));
|
||||
export const div = curry((a, pat) => pat.div(a));
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/embed",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
|
||||
"main": "embed.js",
|
||||
"type": "module",
|
||||
|
||||
@ -15,9 +15,11 @@ npm i @strudel.cycles/eval --save
|
||||
|
||||
```js
|
||||
import { evaluate, extend } from '@strudel.cycles/eval';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
|
||||
extend(strudel); // add strudel to eval scope
|
||||
evalScope(
|
||||
import('@strudel.cycles/core'),
|
||||
// import other strudel packages here
|
||||
); // add strudel to eval scope
|
||||
|
||||
async function run(code) {
|
||||
const { pattern } = await evaluate(code);
|
||||
|
||||
2
packages/eval/package-lock.json
generated
2
packages/eval/package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/eval",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/eval",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"description": "Code evaluator for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@ -28,7 +28,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.1.0",
|
||||
"@strudel.cycles/core": "^0.1.2",
|
||||
"estraverse": "^5.3.0",
|
||||
"shift-ast": "^6.1.0",
|
||||
"shift-codegen": "^7.0.3",
|
||||
|
||||
2
packages/midi/package-lock.json
generated
2
packages/midi/package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"description": "Midi API for strudel",
|
||||
"main": "index.mjs",
|
||||
"repository": {
|
||||
@ -21,7 +21,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/tone": "^0.1.1",
|
||||
"@strudel.cycles/tone": "^0.1.3",
|
||||
"tone": "^14.7.77",
|
||||
"webmidi": "^2.5.2"
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/mini",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"description": "Mini notation for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@ -25,8 +25,8 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.1.0",
|
||||
"@strudel.cycles/eval": "^0.1.1",
|
||||
"@strudel.cycles/tone": "^0.1.1"
|
||||
"@strudel.cycles/core": "^0.1.2",
|
||||
"@strudel.cycles/eval": "^0.1.3",
|
||||
"@strudel.cycles/tone": "^0.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,13 +22,15 @@ let startedAt = -1;
|
||||
*/
|
||||
Pattern.prototype.osc = function () {
|
||||
return this._withHap((hap) => {
|
||||
const onTrigger = (time, hap, currentTime, cps, cycle, delta) => {
|
||||
const onTrigger = (time, hap, currentTime, cps) => {
|
||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||
const delta = hap.duration.valueOf();
|
||||
// time should be audio time of onset
|
||||
// currentTime should be current time of audio context (slightly before time)
|
||||
if (startedAt < 0) {
|
||||
startedAt = Date.now() - currentTime * 1000;
|
||||
}
|
||||
const controls = Object.assign({}, { cps: cps, cycle: cycle, delta: delta }, hap.value);
|
||||
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
||||
const keyvals = Object.entries(controls).flat();
|
||||
const ts = Math.floor(startedAt + (time + latency) * 1000);
|
||||
const message = new OSC.Message('/dirt/play', ...keyvals);
|
||||
|
||||
2
packages/osc/package-lock.json
generated
2
packages/osc/package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/osc",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/osc",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "OSC messaging for strudel",
|
||||
"main": "osc.mjs",
|
||||
"scripts": {
|
||||
|
||||
2
packages/react/package-lock.json
generated
2
packages/react/package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/react",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.4",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/react",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.4",
|
||||
"description": "React components for strudel",
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.es.js",
|
||||
@ -38,9 +38,9 @@
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-javascript": "^0.19.0",
|
||||
"@strudel.cycles/core": "*",
|
||||
"@strudel.cycles/eval": "^0.1.1",
|
||||
"@strudel.cycles/tone": "^0.1.1",
|
||||
"@strudel.cycles/core": "^0.1.2",
|
||||
"@strudel.cycles/eval": "^0.1.3",
|
||||
"@strudel.cycles/tone": "^0.1.3",
|
||||
"react-codemirror6": "^1.1.0",
|
||||
"react-hook-inview": "^4.5.0"
|
||||
},
|
||||
|
||||
@ -57,14 +57,7 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
|
||||
/* console.warn('no instrument chosen', event);
|
||||
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
|
||||
} else {
|
||||
onTrigger(
|
||||
time,
|
||||
event,
|
||||
currentTime,
|
||||
1 /* cps */,
|
||||
event.wholeOrPart().begin.valueOf(),
|
||||
event.duration.valueOf(),
|
||||
);
|
||||
onTrigger(time, event, currentTime, 1 /* cps */);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/serial",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "Webserial API for strudel",
|
||||
"main": "serial.mjs",
|
||||
"repository": {
|
||||
|
||||
2
packages/tonal/package-lock.json
generated
2
packages/tonal/package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tonal",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tonal",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"description": "Tonal functions for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@ -25,7 +25,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.1.0",
|
||||
"@strudel.cycles/core": "^0.1.2",
|
||||
"@tonaljs/tonal": "^4.6.5",
|
||||
"webmidi": "^3.0.15"
|
||||
}
|
||||
|
||||
2
packages/tone/package-lock.json
generated
2
packages/tone/package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tone",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tone",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"description": "Tone.js API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@ -22,7 +22,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.1.0",
|
||||
"@strudel.cycles/core": "^0.1.2",
|
||||
"@tonejs/piano": "^0.2.1",
|
||||
"chord-voicings": "^0.0.1",
|
||||
"tone": "^14.7.77"
|
||||
|
||||
@ -7,3 +7,4 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
export * from './clockworker.mjs';
|
||||
export * from './scheduler.mjs';
|
||||
export * from './webaudio.mjs';
|
||||
export * from './sampler.mjs';
|
||||
|
||||
2
packages/webaudio/package-lock.json
generated
2
packages/webaudio/package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/webaudio",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/webaudio",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"description": "Web Audio helpers for Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@ -28,6 +28,6 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.1.0"
|
||||
"@strudel.cycles/core": "^0.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||
const loadCache = {}; // string: Promise<ArrayBuffer>
|
||||
|
||||
export const getCachedBuffer = (url) => bufferCache[url];
|
||||
|
||||
export const loadBuffer = (url, ac) => {
|
||||
if (!loadCache[url]) {
|
||||
loadCache[url] = fetch(url)
|
||||
@ -91,7 +93,7 @@ export const loadGithubSamples = async (path, nameFn) => {
|
||||
*
|
||||
*/
|
||||
|
||||
export const samples = (sampleMap, baseUrl = '') => {
|
||||
export const samples = (sampleMap, baseUrl = sampleMap._base) => {
|
||||
sampleCache.current = {
|
||||
...sampleCache.current,
|
||||
...Object.fromEntries(
|
||||
@ -10,18 +10,20 @@ import { State, TimeSpan } from '@strudel.cycles/core';
|
||||
export class Scheduler {
|
||||
worker;
|
||||
pattern;
|
||||
constructor({ audioContext, interval = 0.2, onEvent }) {
|
||||
constructor({ audioContext, interval = 0.2, onEvent, latency = 0.2 }) {
|
||||
this.worker = new ClockWorker(
|
||||
audioContext,
|
||||
(begin, end) => {
|
||||
this.pattern.query(new State(new TimeSpan(begin, end))).forEach((e) => {
|
||||
this.pattern.query(new State(new TimeSpan(begin + latency, end + latency))).forEach((e) => {
|
||||
if (!e.part.begin.equals(e.whole.begin)) {
|
||||
return;
|
||||
}
|
||||
if (e.context.onTrigger) {
|
||||
// TODO: kill first param, as it's contained in e
|
||||
e.context.onTrigger(e.whole.begin, e, audioContext.currentTime, 1 /* cps */);
|
||||
}
|
||||
if (onEvent) {
|
||||
onEvent?.(e);
|
||||
} else {
|
||||
console.warn('unplayable event: no audio node nor onEvent callback', e);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@ -6,8 +6,11 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
// import { Pattern, getFrequency, patternify2 } from '@strudel.cycles/core';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
import { Tone } from '@strudel.cycles/tone';
|
||||
const { Pattern, getFrequency, patternify2 } = strudel;
|
||||
import { fromMidi } from '@strudel.cycles/core';
|
||||
import { loadBuffer } from './sampler.mjs';
|
||||
const { Pattern } = strudel;
|
||||
|
||||
// export const getAudioContext = () => Tone.getContext().rawContext;
|
||||
|
||||
let audioContext;
|
||||
export const getAudioContext = () => {
|
||||
@ -17,9 +20,15 @@ export const getAudioContext = () => {
|
||||
return audioContext;
|
||||
};
|
||||
|
||||
const lookahead = 0.2;
|
||||
const getFilter = (type, frequency, Q) => {
|
||||
const filter = getAudioContext().createBiquadFilter();
|
||||
filter.type = type;
|
||||
filter.frequency.value = frequency;
|
||||
filter.Q.value = Q;
|
||||
return filter;
|
||||
};
|
||||
|
||||
const adsr = (attack, decay, sustain, release, velocity, begin, end) => {
|
||||
const getADSR = (attack, decay, sustain, release, velocity, begin, end) => {
|
||||
const gainNode = getAudioContext().createGain();
|
||||
gainNode.gain.setValueAtTime(0, begin);
|
||||
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
|
||||
@ -30,65 +39,115 @@ const adsr = (attack, decay, sustain, release, velocity, begin, end) => {
|
||||
return gainNode;
|
||||
};
|
||||
|
||||
Pattern.prototype.withAudioNode = function (createAudioNode) {
|
||||
return this._withHap((hap) => {
|
||||
return hap.setContext({
|
||||
...hap.context,
|
||||
createAudioNode: (t, e) => createAudioNode(t, e, hap.context.createAudioNode?.(t, hap)),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Pattern.prototype._wave = function (type) {
|
||||
return this.withAudioNode((t, e) => {
|
||||
const osc = getAudioContext().createOscillator();
|
||||
osc.type = type;
|
||||
const f = getFrequency(e);
|
||||
osc.frequency.value = f; // expects frequency..
|
||||
const begin = t ?? e.whole.begin.valueOf() + lookahead;
|
||||
const end = begin + e.duration.valueOf();
|
||||
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((t, e, node) => {
|
||||
const velocity = e.context?.velocity || 1;
|
||||
const begin = t ?? e.whole.begin.valueOf() + lookahead;
|
||||
const end = begin + e.duration.valueOf() + lookahead;
|
||||
const envelope = adsr(a, d, s, r, velocity, begin, end);
|
||||
node?.connect(envelope);
|
||||
return envelope;
|
||||
});
|
||||
};
|
||||
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;
|
||||
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((t, e, node) => {
|
||||
if (!node) {
|
||||
console.warn('out: no source! call .osc() first');
|
||||
return this.onTrigger(async (t, hap, ct) => {
|
||||
const ac = getAudioContext();
|
||||
// calculate correct time (tone.js workaround)
|
||||
t = ac.currentTime + t - ct;
|
||||
// destructure value
|
||||
let {
|
||||
freq,
|
||||
s,
|
||||
n = 0,
|
||||
gain = 1,
|
||||
cutoff,
|
||||
resonance = 1,
|
||||
hcutoff,
|
||||
hresonance = 1,
|
||||
bandf,
|
||||
bandq = 1,
|
||||
pan,
|
||||
attack = 0.001,
|
||||
decay = 0,
|
||||
sustain = 1,
|
||||
release = 0.001,
|
||||
speed = 1, // sample playback speed
|
||||
begin = 0,
|
||||
end = 1,
|
||||
} = hap.value;
|
||||
// the chain will hold all audio nodes that connect to each other
|
||||
const chain = [];
|
||||
if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) {
|
||||
// get frequency
|
||||
if (!freq && typeof n === 'number') {
|
||||
freq = fromMidi(n); // + 48);
|
||||
}
|
||||
if (!freq && typeof n === 'string') {
|
||||
freq = fromMidi(toMidi(n));
|
||||
}
|
||||
// make oscillator
|
||||
const o = ac.createOscillator();
|
||||
o.type = s || 'triangle';
|
||||
o.frequency.value = Number(freq);
|
||||
o.start(t);
|
||||
o.stop(t + hap.duration + release);
|
||||
chain.push(o);
|
||||
// level down oscillators as they are really loud compared to samples i've tested
|
||||
const g = ac.createGain();
|
||||
g.gain.value = 0.5;
|
||||
chain.push(g);
|
||||
// TODO: make adsr work with samples without pops
|
||||
// envelope
|
||||
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + hap.duration);
|
||||
chain.push(adsr);
|
||||
} else {
|
||||
// load sample
|
||||
const samples = getLoadedSamples();
|
||||
if (!samples) {
|
||||
console.warn('no samples loaded');
|
||||
return;
|
||||
}
|
||||
const bank = samples?.[s];
|
||||
if (!bank) {
|
||||
console.warn('sample not found:', s, 'try one of ' + Object.keys(samples));
|
||||
return;
|
||||
} else {
|
||||
if (speed === 0) {
|
||||
// no playback
|
||||
return;
|
||||
}
|
||||
if (!s) {
|
||||
console.warn('no sample specified');
|
||||
return;
|
||||
}
|
||||
const bank = samples[s];
|
||||
const sampleUrl = bank[n % bank.length];
|
||||
let buffer = await loadBuffer(sampleUrl, ac);
|
||||
if (ac.currentTime > t) {
|
||||
console.warn('sample still loading:', s, n);
|
||||
return;
|
||||
}
|
||||
const src = ac.createBufferSource();
|
||||
src.buffer = buffer;
|
||||
src.playbackRate.value = Math.abs(speed);
|
||||
// TODO: nudge, unit, cut, loop
|
||||
|
||||
let duration = src.buffer.duration;
|
||||
const offset = begin * duration;
|
||||
duration = ((end - begin) * duration) / Math.abs(speed);
|
||||
src.start(t, offset, duration);
|
||||
src.stop(t + duration);
|
||||
chain.push(src);
|
||||
}
|
||||
}
|
||||
node?.connect(master);
|
||||
})._withHap((hap) => {
|
||||
const onTrigger = (time, e) => e.context?.createAudioNode?.(time, e);
|
||||
return hap.setContext({ ...hap.context, onTrigger });
|
||||
// filters
|
||||
cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance));
|
||||
hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance));
|
||||
bandf !== undefined && chain.push(getFilter('bandpass', bandf, bandq));
|
||||
// TODO vowel
|
||||
// TODO delay / delaytime / delayfeedback
|
||||
// panning
|
||||
if (pan !== undefined) {
|
||||
const panner = ac.createStereoPanner();
|
||||
panner.pan.value = 2 * pan - 1;
|
||||
chain.push(panner);
|
||||
}
|
||||
// master out
|
||||
const master = ac.createGain();
|
||||
master.gain.value = 0.8 * gain;
|
||||
chain.push(master);
|
||||
chain.push(ac.destination);
|
||||
// connect chain elements together
|
||||
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
|
||||
});
|
||||
};
|
||||
|
||||
Pattern.prototype.define('wave', (type, pat) => pat.wave(type), { patternified: true });
|
||||
|
||||
@ -1,2 +1 @@
|
||||
export * from './webdirt.mjs';
|
||||
export * from './sampler.mjs';
|
||||
|
||||
2
packages/webdirt/package-lock.json
generated
2
packages/webdirt/package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/webdirt",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/webdirt",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.2",
|
||||
"description": "WebDirt integration for Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@ -22,7 +22,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.1.0",
|
||||
"@strudel.cycles/core": "^0.1.2",
|
||||
"WebDirt": "github:dktr0/WebDirt"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
const { Pattern } = strudel;
|
||||
import * as WebDirt from 'WebDirt';
|
||||
import { getLoadedSamples, loadBuffer, getLoadedBuffer } from './sampler.mjs';
|
||||
import { getLoadedSamples, loadBuffer, getLoadedBuffer } from '@strudel.cycles/webaudio';
|
||||
|
||||
let webDirt;
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/xen",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.3",
|
||||
"description": "Xenharmonic API for strudel",
|
||||
"main": "index.mjs",
|
||||
"scripts": {
|
||||
@ -24,6 +24,6 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.1.0"
|
||||
"@strudel.cycles/core": "^0.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
2
repl/.gitignore
vendored
2
repl/.gitignore
vendored
@ -22,3 +22,5 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
oldtunes.mjs
|
||||
@ -3,7 +3,7 @@
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --host",
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
|
||||
@ -13,7 +13,8 @@ import './App.css';
|
||||
import logo from './logo.svg';
|
||||
import * as tunes from './tunes.mjs';
|
||||
import * as WebDirt from 'WebDirt';
|
||||
import { loadWebDirt, resetLoadedSamples } from '@strudel.cycles/webdirt';
|
||||
import { loadWebDirt } from '@strudel.cycles/webdirt';
|
||||
import { resetLoadedSamples, getAudioContext } from '@strudel.cycles/webaudio';
|
||||
|
||||
evalScope(
|
||||
Tone,
|
||||
@ -139,7 +140,13 @@ function App() {
|
||||
<h1 className={isEmbedded ? 'text-l' : 'text-xl'}>Strudel {isEmbedded ? 'Mini ' : ''}REPL</h1>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button onClick={() => togglePlay()} className={cx('hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}>
|
||||
<button
|
||||
onClick={() => {
|
||||
getAudioContext().resume(); // fixes no sound in ios webkit
|
||||
togglePlay();
|
||||
}}
|
||||
className={cx('hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}
|
||||
>
|
||||
{!pending ? (
|
||||
<span className={cx('flex items-center', isEmbedded ? 'w-16' : 'w-16')}>
|
||||
{cycle.started ? (
|
||||
|
||||
@ -6,6 +6,8 @@
|
||||
import { evaluate } from '@strudel.cycles/eval';
|
||||
import { extend } from '@strudel.cycles/eval';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
import * as webaudio from '@strudel.cycles/webaudio';
|
||||
import controls from '@strudel.cycles/core/controls.mjs';
|
||||
// import gist from '@strudel.cycles/core/gist.js';
|
||||
import { mini } from '@strudel.cycles/mini/mini.mjs';
|
||||
// import { Tone } from '@strudel.cycles/tone';
|
||||
@ -83,6 +85,9 @@ const toneHelpersMocked = {
|
||||
strudel.Pattern.prototype.tone = function () {
|
||||
return this;
|
||||
};
|
||||
strudel.Pattern.prototype.webdirt = function () {
|
||||
return this;
|
||||
};
|
||||
|
||||
// draw mock
|
||||
strudel.Pattern.prototype.pianoroll = function () {
|
||||
@ -116,6 +121,7 @@ const uiHelpersMocked = {
|
||||
backgroundImage: id,
|
||||
};
|
||||
|
||||
|
||||
// TODO: refactor to evalScope
|
||||
extend(
|
||||
// Tone,
|
||||
@ -123,6 +129,8 @@ extend(
|
||||
strudel.Pattern.prototype.bootstrap(),
|
||||
toneHelpersMocked,
|
||||
uiHelpersMocked,
|
||||
controls,
|
||||
webaudio,
|
||||
/* controls,
|
||||
toneHelpers,
|
||||
voicingHelpers,
|
||||
@ -181,6 +189,6 @@ export const testCycles = {
|
||||
randomBells: 24,
|
||||
waa: 16,
|
||||
waar: 16,
|
||||
hyperpop: 60,
|
||||
hyperpop: 10,
|
||||
festivalOfFingers3: 16,
|
||||
};
|
||||
|
||||
@ -728,7 +728,7 @@ bell = bell.chain(vol(0.6).connect(delay),out());
|
||||
.slow(6)
|
||||
.pianoroll({minMidi:20,maxMidi:120,background:'transparent'})`;
|
||||
|
||||
export const waa = `"a4 [a3 c3] a3 c3"
|
||||
/* export const waa = `n("a4 [a3 c3] a3 c3")
|
||||
.sub("<7 12>/2")
|
||||
.off(1/8, add("12"))
|
||||
.off(1/4, add("7"))
|
||||
@ -736,23 +736,37 @@ export const waa = `"a4 [a3 c3] a3 c3"
|
||||
.slow(2)
|
||||
.wave("sawtooth square")
|
||||
.filter('lowpass', "<2000 1000 500>")
|
||||
.out()`;
|
||||
.out()`; */
|
||||
|
||||
export const waar = `"a4 [a3 c3] a3 c3".color('#F9D649')
|
||||
.sub("<7 12 5 12>".slow(2))
|
||||
.off(1/4,x=>x.add(7).color("#FFFFFF #0C3AA1 #C63928"))
|
||||
.off(1/8,x=>x.add(12).color('#215CB6'))
|
||||
.slow(2)
|
||||
.legato(sine.range(0.3, 2).slow(28))
|
||||
.wave("sawtooth square".fast(2))
|
||||
.filter('lowpass', cosine.range(500,4000).slow(16))
|
||||
.out()
|
||||
.pianoroll({minMidi:20,maxMidi:120,background:'#202124'})`;
|
||||
export const waa = `n(
|
||||
"a4 [a3 c3] a3 c3"
|
||||
.sub("<7 12>/2")
|
||||
.off(1/8, add("12"))
|
||||
.off(1/4, add("7"))
|
||||
)
|
||||
.legato(.5)
|
||||
.slow(2)
|
||||
.s("sawtooth square")
|
||||
.cutoff("<2000 1000 500>")
|
||||
.out()
|
||||
`;
|
||||
|
||||
export const waa2 = `n(
|
||||
"a4 [a3 c3] a3 c3"
|
||||
.sub("<7 12 5 12>".slow(2))
|
||||
.off(1/4,x=>x.add(7))
|
||||
.off(1/8,x=>x.add(12))
|
||||
)
|
||||
.slow(2)
|
||||
.legato(sine.range(0.3, 2).slow(28))
|
||||
.s("sawtooth square".fast(2))
|
||||
.cutoff(cosine.range(500,4000).slow(16))
|
||||
.out()`;
|
||||
|
||||
export const hyperpop = `const lfo = cosine.slow(15);
|
||||
const lfo2 = sine.slow(16);
|
||||
const filter1 = x=>x.filter('lowpass', lfo2.range(300,3000));
|
||||
const filter2 = x=>x.filter('highpass', lfo.range(1000,6000)).filter('lowpass',4000)
|
||||
const filter1 = x=>x.cutoff(lfo2.range(300,3000));
|
||||
const filter2 = x=>x.hcutoff(lfo.range(1000,6000)).cutoff(4000)
|
||||
const scales = cat('D3 major', 'G3 major').slow(8)
|
||||
|
||||
const drums = await players({
|
||||
@ -765,24 +779,30 @@ const drums = await players({
|
||||
|
||||
stack(
|
||||
"-7 0 -7 7".struct("x(5,8,2)").fast(2).sub(7)
|
||||
.scale(scales).wave("sawtooth,square").velocity(.3).adsr(0.01,0.1,.5,0)
|
||||
.scale(scales)
|
||||
.n()
|
||||
.s("sawtooth,square")
|
||||
.gain(.3).attack(0.01).decay(0.1).sustain(.5)
|
||||
.apply(filter1),
|
||||
"~@3 [<2 3>,<4 5>]"
|
||||
.echo(8,1/16,.7)
|
||||
.echo(4,1/16,.7)
|
||||
.scale(scales)
|
||||
.wave('square').velocity(.7).adsr(0.01,0.1,0).apply(filter1),
|
||||
"6 5 4".add(14)
|
||||
.n()
|
||||
.s('square').gain(.7)
|
||||
.attack(0.01).decay(0.1).sustain(0)
|
||||
.apply(filter1),
|
||||
"6 4 2".add(14)
|
||||
.superimpose(sub("5"))
|
||||
.fast(1).euclidLegato(3,8)
|
||||
.mask("<1 0@7>")
|
||||
.fast(2)
|
||||
.echo(32, 1/8, .9)
|
||||
.echo(32, 1/8, .8)
|
||||
.scale(scales)
|
||||
.wave("sawtooth")
|
||||
.velocity(.2)
|
||||
.adsr(.01,.5,0)
|
||||
.n()
|
||||
.s("sawtooth")
|
||||
.gain(sine.range(.1,.4).slow(8))
|
||||
.attack(.001).decay(.2).sustain(0)
|
||||
.apply(filter2)
|
||||
//.echo(4,1/16,.5)
|
||||
).out().stack(
|
||||
stack(
|
||||
"bd <~@7 [~ bd]>".fast(2),
|
||||
@ -790,7 +810,7 @@ stack(
|
||||
"[~ hh3]*2"
|
||||
).tone(drums.chain(vol(.18),out())).fast(2)
|
||||
).slow(2)
|
||||
|
||||
|
||||
//.pianoroll({minMidi:20, maxMidi:160})
|
||||
// strudel disable-highlighting`;
|
||||
|
||||
@ -816,3 +836,73 @@ stack(
|
||||
"<Cm7 [Dm7b5 G7b9] Bbm7 [Cm7b5 F7b9]>".fast(2).struct("x ~ x@3 x ~ x ~ ~ ~ x ~ x@3".late(1/8)).early(1/8).slow(2).voicings(),
|
||||
"[~ [0 ~]] 0 [~ [4 ~]] 4".sub(7).restart(scales).scale(scales).early(.25)
|
||||
).tone((await piano()).toDestination()).slow(2)`;
|
||||
|
||||
export const customTrigger = `stack(
|
||||
freq("55 [110,165] 110 [220,275]".mul("<1 <3/4 2/3>>").struct("x(3,8)").layer(x=>x.mul("1.006,.995"))),
|
||||
freq("440(5,8)".legato(.18).mul("<1 3/4 2 2/3>")).gain(perlin.range(.2,.8))
|
||||
).s("<sawtooth square>/2")
|
||||
.onTrigger((t,hap,ct)=>{
|
||||
const ac = Tone.getContext().rawContext;
|
||||
t = ac.currentTime + t - ct;
|
||||
const { freq, s, gain = 1 } = hap.value;
|
||||
const master = ac.createGain();
|
||||
master.gain.value = 0.1 * gain;
|
||||
master.connect(ac.destination);
|
||||
const o = ac.createOscillator();
|
||||
o.type = s || 'triangle';
|
||||
o.frequency.value = Number(freq);
|
||||
o.connect(master);
|
||||
o.start(t);
|
||||
o.stop(t + hap.duration);
|
||||
}).stack(s("bd(3,8),hh*4,~ sd").webdirt())`;
|
||||
|
||||
export const bornagain = `stack(
|
||||
freq("55 [110,165] 110 [220,275]".mul("<1 <3/4 2/3>>").struct("x(3,8)")
|
||||
.layer(x=>x.mul("1.006,.995"))), // detune
|
||||
freq("440(5,8)".legato(.18).mul("<1 3/4 2 2/3>")).gain(perlin.range(.2,.8))
|
||||
).s("<sawtooth square>/2")
|
||||
.cutoff(perlin.range(100,4000).slow(4))
|
||||
.jux(rev)
|
||||
.out()
|
||||
.stack(s("bd(3,8),hh*4,~ sd").webdirt())`;
|
||||
|
||||
export const meltingsubmarine = `samples({
|
||||
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav','bd/BT0A0DA.wav','bd/BT0A0D3.wav','bd/BT0A0D0.wav','bd/BT0A0A7.wav'],
|
||||
sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'],
|
||||
hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'],
|
||||
}, 'github:tidalcycles/Dirt-Samples/master/');
|
||||
stack(
|
||||
s("bd,[~ <sd!3 sd(3,4,2)>],hh(3,4)") // drums
|
||||
.speed(perlin.range(.7,.9)) // random sample speed variation
|
||||
//.hush()
|
||||
,"<a1 b1*2 a1(3,8) e2>" // bassline
|
||||
.off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps
|
||||
.add(perlin.range(0,.5)) // random pitch variation
|
||||
.superimpose(add(.05)) // add second, slightly detuned voice
|
||||
.n() // wrap in "n"
|
||||
.decay(.15).sustain(0) // make each note of equal length
|
||||
.s('sawtooth') // waveform
|
||||
.gain(.4) // turn down
|
||||
.cutoff(sine.slow(7).range(300,5000)) // automate cutoff
|
||||
//.hush()
|
||||
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings() // chords
|
||||
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
|
||||
.add(perlin.range(0,.5)) // random pitch variation
|
||||
.n() // wrap in "n"
|
||||
.s('sawtooth') // waveform
|
||||
.gain(.16) // turn down
|
||||
.cutoff(500) // fixed cutoff
|
||||
.attack(1) // slowly fade in
|
||||
//.hush()
|
||||
,"a4 c5 <e6 a6>".struct("x(5,8)")
|
||||
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
|
||||
.add(perlin.range(0,.5)) // random pitch variation
|
||||
.n() // wrap in "n"
|
||||
.decay(.1).sustain(0) // make notes short
|
||||
.s('triangle') // waveform
|
||||
.degradeBy(perlin.range(0,.5)) // randomly controlled random removal :)
|
||||
.echoWith(4,.125,(x,n)=>x.gain(.15*1/(n+1))) // echo notes
|
||||
//.hush()
|
||||
)
|
||||
.out()
|
||||
.slow(3/2)`;
|
||||
|
||||
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user