mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-12 22:28:37 +00:00
build
This commit is contained in:
parent
df0650e48d
commit
827c983175
@ -118,21 +118,33 @@ class TimeSpan {
|
||||
}
|
||||
}
|
||||
class Hap {
|
||||
constructor(whole, part, value) {
|
||||
constructor(whole, part, value, context = {}, stateful = false) {
|
||||
this.whole = whole;
|
||||
this.part = part;
|
||||
this.value = value;
|
||||
this.context = context;
|
||||
this.stateful = stateful;
|
||||
if (stateful) {
|
||||
assert(typeof this.value === "function", "Stateful values must be functions");
|
||||
}
|
||||
}
|
||||
withSpan(func) {
|
||||
const whole = this.whole ? func(this.whole) : void 0;
|
||||
return new Hap(whole, func(this.part), this.value);
|
||||
return new Hap(whole, func(this.part), this.value, this.context);
|
||||
}
|
||||
withValue(func) {
|
||||
return new Hap(this.whole, this.part, func(this.value));
|
||||
return new Hap(this.whole, this.part, func(this.value), this.context);
|
||||
}
|
||||
hasOnset() {
|
||||
return this.whole != void 0 && this.whole.begin.equals(this.part.begin);
|
||||
}
|
||||
resolveState(state) {
|
||||
if (this.stateful && this.hasOnset()) {
|
||||
const func = this.value[newState, newValue] = func(state);
|
||||
return [newState, this.withValue(() => newValue)];
|
||||
}
|
||||
return [state, this];
|
||||
}
|
||||
spanEquals(other) {
|
||||
return this.whole == void 0 && other.whole == void 0 || this.whole.equals(other.whole);
|
||||
}
|
||||
@ -140,7 +152,25 @@ class Hap {
|
||||
return this.spanEquals(other) && this.part.equals(other.part) && this.value === other.value;
|
||||
}
|
||||
show() {
|
||||
return "(" + (this.whole == void 0 ? "~" : this.whole.show()) + ", " + this.part.show() + ", " + JSON.stringify(this.value?.value ?? this.value) + ")";
|
||||
return "(" + (this.whole == void 0 ? "~" : this.whole.show()) + ", " + this.part.show() + ", " + this.value + ")";
|
||||
}
|
||||
setContext(context) {
|
||||
return new Hap(this.whole, this.part, this.value, context);
|
||||
}
|
||||
}
|
||||
export class State {
|
||||
constructor(span, controls = {}) {
|
||||
this.span = span;
|
||||
this.controls = controls;
|
||||
}
|
||||
setSpan(span) {
|
||||
return new State(span, this.controls);
|
||||
}
|
||||
withSpan(func) {
|
||||
return this.setSpan(func(this.span));
|
||||
}
|
||||
setControls(controls) {
|
||||
return new State(this.span, controls);
|
||||
}
|
||||
}
|
||||
class Pattern {
|
||||
@ -157,42 +187,55 @@ class Pattern {
|
||||
}
|
||||
_splitQueries() {
|
||||
const pat = this;
|
||||
const q = (span) => flatten(span.spanCycles.map((subspan) => pat.query(subspan)));
|
||||
const q = (state) => {
|
||||
return flatten(state.span.spanCycles.map((subspan) => pat.query(state.setSpan(subspan))));
|
||||
};
|
||||
return new Pattern(q);
|
||||
}
|
||||
withQuerySpan(func) {
|
||||
return new Pattern((span) => this.query(func(span)));
|
||||
return new Pattern((state) => this.query(state.withSpan(func)));
|
||||
}
|
||||
withQueryTime(func) {
|
||||
return new Pattern((span) => this.query(span.withTime(func)));
|
||||
return new Pattern((state) => this.query(state.withSpan((span) => span.withTime(func))));
|
||||
}
|
||||
withEventSpan(func) {
|
||||
return new Pattern((span) => this.query(span).map((hap) => hap.withSpan(func)));
|
||||
return new Pattern((state) => this.query(state).map((hap) => hap.withSpan(func)));
|
||||
}
|
||||
withEventTime(func) {
|
||||
return this.withEventSpan((span) => span.withTime(func));
|
||||
}
|
||||
_withEvents(func) {
|
||||
return new Pattern((span) => func(this.query(span)));
|
||||
return new Pattern((state) => func(this.query(state)));
|
||||
}
|
||||
_withEvent(func) {
|
||||
return this._withEvents((events) => events.map(func));
|
||||
}
|
||||
_setContext(context) {
|
||||
return this._withEvent((event) => event.setContext(context));
|
||||
}
|
||||
_withContext(func) {
|
||||
return this._withEvent((event) => event.setContext(func(event.context)));
|
||||
}
|
||||
_stripContext() {
|
||||
return this._withEvent((event) => event.setContext({}));
|
||||
}
|
||||
withLocation(location) {
|
||||
return this.fmap((value) => {
|
||||
value = typeof value === "object" && !Array.isArray(value) ? value : {value};
|
||||
const locations = (value.locations || []).concat([location]);
|
||||
return {...value, locations};
|
||||
return this._withContext((context) => {
|
||||
const locations = (context.locations || []).concat([location]);
|
||||
return {...context, locations};
|
||||
});
|
||||
}
|
||||
withValue(func) {
|
||||
return new Pattern((span) => this.query(span).map((hap) => hap.withValue(func)));
|
||||
return new Pattern((state) => this.query(state).map((hap) => hap.withValue(func)));
|
||||
}
|
||||
fmap(func) {
|
||||
return this.withValue(func);
|
||||
}
|
||||
_filterEvents(event_test) {
|
||||
return new Pattern((span) => this.query(span).filter(event_test));
|
||||
return new Pattern((state) => this.query(state).filter(event_test));
|
||||
}
|
||||
_filterValues(value_test) {
|
||||
return new Pattern((span) => this.query(span).filter((hap) => value_test(hap.value)));
|
||||
return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value)));
|
||||
}
|
||||
_removeUndefineds() {
|
||||
return this._filterValues((val) => val != void 0);
|
||||
@ -202,15 +245,15 @@ class Pattern {
|
||||
}
|
||||
_appWhole(whole_func, pat_val) {
|
||||
const pat_func = this;
|
||||
const query = function(span) {
|
||||
const event_funcs = pat_func.query(span);
|
||||
const event_vals = pat_val.query(span);
|
||||
const query = function(state) {
|
||||
const event_funcs = pat_func.query(state);
|
||||
const event_vals = pat_val.query(state);
|
||||
const apply = function(event_func, event_val) {
|
||||
const s = event_func.part.intersection(event_val.part);
|
||||
if (s == void 0) {
|
||||
return void 0;
|
||||
}
|
||||
return new Hap(whole_func(event_func.whole, event_val.whole), s, event_func.value(event_val.value));
|
||||
return new Hap(whole_func(event_func.whole, event_val.whole), s, event_func.value(event_val.value), event_val.context);
|
||||
};
|
||||
return flatten(event_funcs.map((event_func) => removeUndefineds(event_vals.map((event_val) => apply(event_func, event_val)))));
|
||||
};
|
||||
@ -227,15 +270,19 @@ class Pattern {
|
||||
}
|
||||
appLeft(pat_val) {
|
||||
const pat_func = this;
|
||||
const query = function(span) {
|
||||
const query = function(state) {
|
||||
const haps = [];
|
||||
for (const hap_func of pat_func.query(span)) {
|
||||
const event_vals = pat_val.query(hap_func.part);
|
||||
for (const hap_func of pat_func.query(state)) {
|
||||
const event_vals = pat_val.query(state.setSpan(hap_func.part));
|
||||
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 hap = new Hap(new_whole, new_part, new_value);
|
||||
const hap = new Hap(new_whole, new_part, new_value, {
|
||||
...hap_val.context,
|
||||
...hap_func.context,
|
||||
locations: (hap_val.context.locations || []).concat(hap_func.context.locations || [])
|
||||
});
|
||||
haps.push(hap);
|
||||
}
|
||||
}
|
||||
@ -245,15 +292,19 @@ class Pattern {
|
||||
}
|
||||
appRight(pat_val) {
|
||||
const pat_func = this;
|
||||
const query = function(span) {
|
||||
const query = function(state) {
|
||||
const haps = [];
|
||||
for (const hap_val of pat_val.query(span)) {
|
||||
const hap_funcs = pat_func.query(hap_val.part);
|
||||
for (const hap_val of pat_val.query(state)) {
|
||||
const hap_funcs = pat_func.query(state.setSpan(hap_val.part));
|
||||
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 hap = new Hap(new_whole, new_part, new_value);
|
||||
const hap = new Hap(new_whole, new_part, new_value, {
|
||||
...hap_func.context,
|
||||
...hap_val.context,
|
||||
locations: (hap_val.context.locations || []).concat(hap_func.context.locations || [])
|
||||
});
|
||||
haps.push(hap);
|
||||
}
|
||||
}
|
||||
@ -261,8 +312,12 @@ class Pattern {
|
||||
};
|
||||
return new Pattern(query);
|
||||
}
|
||||
get firstCycle() {
|
||||
return this.query(new TimeSpan(Fraction(0), Fraction(1)));
|
||||
firstCycle(with_context = false) {
|
||||
var self = this;
|
||||
if (!with_context) {
|
||||
self = self._stripContext();
|
||||
}
|
||||
return self.query(new State(new TimeSpan(Fraction(0), Fraction(1))));
|
||||
}
|
||||
_sortEventsByPart() {
|
||||
return this._withEvents((events) => events.sort((a, b) => a.part.begin.sub(b.part.begin).or(a.part.end.sub(b.part.end)).or(a.whole.begin.sub(b.whole.begin).or(a.whole.end.sub(b.whole.end)))));
|
||||
@ -287,14 +342,18 @@ class Pattern {
|
||||
}
|
||||
_bindWhole(choose_whole, func) {
|
||||
const pat_val = this;
|
||||
const query = function(span) {
|
||||
const query = function(state) {
|
||||
const withWhole = function(a, b) {
|
||||
return new Hap(choose_whole(a.whole, b.whole), b.part, b.value);
|
||||
return new Hap(choose_whole(a.whole, b.whole), b.part, b.value, {
|
||||
...a.context,
|
||||
...b.context,
|
||||
locations: (a.context.locations || []).concat(b.context.locations || [])
|
||||
});
|
||||
};
|
||||
const match = function(a) {
|
||||
return func(a.value).query(a.part).map((b) => withWhole(a, b));
|
||||
return func(a.value).query(state.setSpan(a.part)).map((b) => withWhole(a, b));
|
||||
};
|
||||
return flatten(pat_val.query(span).map((a) => match(a)));
|
||||
return flatten(pat_val.query(state).map((a) => match(a)));
|
||||
};
|
||||
return new Pattern(query);
|
||||
}
|
||||
@ -409,7 +468,8 @@ class Pattern {
|
||||
}
|
||||
rev() {
|
||||
const pat = this;
|
||||
const query = function(span) {
|
||||
const query = function(state) {
|
||||
const span = state.span;
|
||||
const cycle = span.begin.sam();
|
||||
const next_cycle = span.begin.nextSam();
|
||||
const reflect = function(to_reflect) {
|
||||
@ -419,7 +479,7 @@ class Pattern {
|
||||
reflected.end = tmp;
|
||||
return reflected;
|
||||
};
|
||||
const haps = pat.query(reflect(span));
|
||||
const haps = pat.query(state.setSpan(reflect(span)));
|
||||
return haps.map((hap) => hap.withSpan(reflect));
|
||||
};
|
||||
return new Pattern(query)._splitQueries();
|
||||
@ -463,8 +523,8 @@ Pattern.prototype.patternified = ["apply", "fast", "slow", "early", "late"];
|
||||
Pattern.prototype.factories = {pure, stack, slowcat, fastcat, cat, timeCat, sequence, polymeter, pm, polyrhythm, pr};
|
||||
const silence = new Pattern((_) => []);
|
||||
function pure(value) {
|
||||
function query(span) {
|
||||
return span.spanCycles.map((subspan) => new Hap(Fraction(subspan.begin).wholeCycle(), subspan, value));
|
||||
function query(state) {
|
||||
return state.span.spanCycles.map((subspan) => new Hap(Fraction(subspan.begin).wholeCycle(), subspan, value));
|
||||
}
|
||||
return new Pattern(query);
|
||||
}
|
||||
@ -472,7 +532,7 @@ function steady(value) {
|
||||
return new Pattern((span) => Hap(void 0, span, value));
|
||||
}
|
||||
export const signal = (func) => {
|
||||
const query = (span) => [new Hap(void 0, span, func(span.midpoint()))];
|
||||
const query = (state) => [new Hap(void 0, state.span, func(state.span.midpoint()))];
|
||||
return new Pattern(query);
|
||||
};
|
||||
const _toBipolar = (pat) => pat.fmap((x) => x * 2 - 1);
|
||||
@ -497,28 +557,29 @@ function reify(thing) {
|
||||
}
|
||||
function stack(...pats) {
|
||||
const reified = pats.map((pat) => reify(pat));
|
||||
const query = (span) => flatten(reified.map((pat) => pat.query(span)));
|
||||
const query = (state) => flatten(reified.map((pat) => pat.query(state)));
|
||||
return new Pattern(query);
|
||||
}
|
||||
function slowcat(...pats) {
|
||||
pats = pats.map(reify);
|
||||
const query = function(span) {
|
||||
const query = function(state) {
|
||||
const span = state.span;
|
||||
const pat_n = Math.floor(span.begin) % pats.length;
|
||||
const pat = pats[pat_n];
|
||||
if (!pat) {
|
||||
return [];
|
||||
}
|
||||
const offset = span.begin.floor().sub(span.begin.div(pats.length).floor());
|
||||
return pat.withEventTime((t) => t.add(offset)).query(span.withTime((t) => t.sub(offset)));
|
||||
return pat.withEventTime((t) => t.add(offset)).query(state.setSpan(span.withTime((t) => t.sub(offset))));
|
||||
};
|
||||
return new Pattern(query)._splitQueries();
|
||||
}
|
||||
function slowcatPrime(...pats) {
|
||||
pats = pats.map(reify);
|
||||
const query = function(span) {
|
||||
const pat_n = Math.floor(span.begin) % pats.length;
|
||||
const query = function(state) {
|
||||
const pat_n = Math.floor(state.span.begin) % pats.length;
|
||||
const pat = pats[pat_n];
|
||||
return pat.query(span);
|
||||
return pat.query(state);
|
||||
};
|
||||
return new Pattern(query)._splitQueries();
|
||||
}
|
||||
@ -638,9 +699,8 @@ Pattern.prototype.bootstrap = () => {
|
||||
return bootstrapped;
|
||||
};
|
||||
function withLocationOffset(pat, offset) {
|
||||
return pat.fmap((value) => {
|
||||
value = typeof value === "object" && !Array.isArray(value) ? value : {value};
|
||||
let locations = value.locations || [];
|
||||
return pat._withContext((context) => {
|
||||
let locations = context.locations || [];
|
||||
locations = locations.map(({start, end}) => {
|
||||
const colOffset = start.line === 1 ? offset.start.column : 0;
|
||||
return {
|
||||
@ -656,7 +716,7 @@ function withLocationOffset(pat, offset) {
|
||||
}
|
||||
};
|
||||
});
|
||||
return {...value, locations};
|
||||
return {...context, locations};
|
||||
});
|
||||
}
|
||||
export {
|
||||
|
||||
538
docs/_snowpack/pkg/@tonejs/piano.js
Normal file
538
docs/_snowpack/pkg/@tonejs/piano.js
Normal file
@ -0,0 +1,538 @@
|
||||
import { T as ToneAudioNode, V as Volume, F as Frequency, M as Midi, S as Sampler, a as ToneAudioBuffers, b as ToneBufferSource, o as optionsFromArguments, G as Gain, i as isString } from '../common/index-b6fc655f.js';
|
||||
import '../common/webmidi.min-97732fd4.js';
|
||||
import '../common/_commonjsHelpers-8c19dec8.js';
|
||||
|
||||
var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
/**
|
||||
* Base class for the other components
|
||||
*/
|
||||
class PianoComponent extends ToneAudioNode {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.name = 'PianoComponent';
|
||||
this.input = undefined;
|
||||
this.output = new Volume({ context: this.context });
|
||||
/**
|
||||
* If the component is enabled or not
|
||||
*/
|
||||
this._enabled = false;
|
||||
/**
|
||||
* The volume output of the component
|
||||
*/
|
||||
this.volume = this.output.volume;
|
||||
/**
|
||||
* Boolean indication of if the component is loaded or not
|
||||
*/
|
||||
this._loaded = false;
|
||||
this.volume.value = options.volume;
|
||||
this._enabled = options.enabled;
|
||||
this.samples = options.samples;
|
||||
}
|
||||
/**
|
||||
* If the samples are loaded or not
|
||||
*/
|
||||
get loaded() {
|
||||
return this._loaded;
|
||||
}
|
||||
/**
|
||||
* Load the samples
|
||||
*/
|
||||
load() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
if (this._enabled) {
|
||||
yield this._internalLoad();
|
||||
this._loaded = true;
|
||||
}
|
||||
else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// import * as Tone from '../node_modules/tone/Tone'
|
||||
function midiToNote(midi) {
|
||||
const frequency = Frequency(midi, 'midi');
|
||||
const ret = frequency.toNote();
|
||||
return ret;
|
||||
}
|
||||
function randomBetween(low, high) {
|
||||
return Math.random() * (high - low) + low;
|
||||
}
|
||||
|
||||
function getReleasesUrl(midi) {
|
||||
return `rel${midi - 20}.[mp3|ogg]`;
|
||||
}
|
||||
function getHarmonicsUrl(midi) {
|
||||
return `harmS${midiToNote(midi).replace('#', 's')}.[mp3|ogg]`;
|
||||
}
|
||||
function getNotesUrl(midi, vel) {
|
||||
return `${midiToNote(midi).replace('#', 's')}v${vel}.[mp3|ogg]`;
|
||||
}
|
||||
/**
|
||||
* Maps velocity depths to Salamander velocities
|
||||
*/
|
||||
const velocitiesMap = {
|
||||
1: [8],
|
||||
2: [6, 12],
|
||||
3: [1, 7, 15],
|
||||
4: [1, 5, 10, 15],
|
||||
5: [1, 4, 8, 12, 16],
|
||||
6: [1, 3, 7, 10, 13, 16],
|
||||
7: [1, 3, 6, 9, 11, 13, 16],
|
||||
8: [1, 3, 5, 7, 9, 11, 13, 16],
|
||||
9: [1, 3, 5, 7, 9, 11, 13, 15, 16],
|
||||
10: [1, 2, 3, 5, 7, 9, 11, 13, 15, 16],
|
||||
11: [1, 2, 3, 5, 7, 9, 11, 13, 14, 15, 16],
|
||||
12: [1, 2, 3, 4, 5, 7, 9, 11, 13, 14, 15, 16],
|
||||
13: [1, 2, 3, 4, 5, 7, 9, 11, 12, 13, 14, 15, 16],
|
||||
14: [1, 2, 3, 4, 5, 6, 7, 9, 11, 12, 13, 14, 15, 16],
|
||||
15: [1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
16: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
};
|
||||
/**
|
||||
* All the notes of audio samples
|
||||
*/
|
||||
const allNotes = [
|
||||
21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54,
|
||||
57, 60, 63, 66, 69, 72, 75, 78, 81, 84,
|
||||
87, 90, 93, 96, 99, 102, 105, 108
|
||||
];
|
||||
function getNotesInRange(min, max) {
|
||||
return allNotes.filter(note => min <= note && note <= max);
|
||||
}
|
||||
/**
|
||||
* All the notes of audio samples
|
||||
*/
|
||||
const harmonics = [21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87];
|
||||
function getHarmonicsInRange(min, max) {
|
||||
return harmonics.filter(note => min <= note && note <= max);
|
||||
}
|
||||
function inHarmonicsRange(note) {
|
||||
return harmonics[0] <= note && note <= harmonics[harmonics.length - 1];
|
||||
}
|
||||
|
||||
class Harmonics extends PianoComponent {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this._urls = {};
|
||||
const notes = getHarmonicsInRange(options.minNote, options.maxNote);
|
||||
for (const n of notes) {
|
||||
this._urls[n] = getHarmonicsUrl(n);
|
||||
}
|
||||
}
|
||||
triggerAttack(note, time, velocity) {
|
||||
if (this._enabled && inHarmonicsRange(note)) {
|
||||
this._sampler.triggerAttack(Midi(note).toNote(), time, velocity * randomBetween(0.5, 1));
|
||||
}
|
||||
}
|
||||
_internalLoad() {
|
||||
return new Promise(onload => {
|
||||
this._sampler = new Sampler({
|
||||
baseUrl: this.samples,
|
||||
onload,
|
||||
urls: this._urls,
|
||||
}).connect(this.output);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Keybed extends PianoComponent {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
/**
|
||||
* The urls to load
|
||||
*/
|
||||
this._urls = {};
|
||||
for (let i = options.minNote; i <= options.maxNote; i++) {
|
||||
this._urls[i] = getReleasesUrl(i);
|
||||
}
|
||||
}
|
||||
_internalLoad() {
|
||||
return new Promise(success => {
|
||||
this._buffers = new ToneAudioBuffers(this._urls, success, this.samples);
|
||||
});
|
||||
}
|
||||
start(note, time, velocity) {
|
||||
if (this._enabled && this._buffers.has(note)) {
|
||||
const source = new ToneBufferSource({
|
||||
url: this._buffers.get(note),
|
||||
context: this.context,
|
||||
}).connect(this.output);
|
||||
// randomize the velocity slightly
|
||||
source.start(time, 0, undefined, 0.015 * velocity * randomBetween(0.5, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Pedal extends PianoComponent {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this._downTime = Infinity;
|
||||
this._currentSound = null;
|
||||
this._downTime = Infinity;
|
||||
}
|
||||
_internalLoad() {
|
||||
return new Promise((success) => {
|
||||
this._buffers = new ToneAudioBuffers({
|
||||
down1: 'pedalD1.mp3',
|
||||
down2: 'pedalD2.mp3',
|
||||
up1: 'pedalU1.mp3',
|
||||
up2: 'pedalU2.mp3',
|
||||
}, success, this.samples);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Squash the current playing sound
|
||||
*/
|
||||
_squash(time) {
|
||||
if (this._currentSound && this._currentSound.state !== 'stopped') {
|
||||
this._currentSound.stop(time);
|
||||
}
|
||||
this._currentSound = null;
|
||||
}
|
||||
_playSample(time, dir) {
|
||||
if (this._enabled) {
|
||||
this._currentSound = new ToneBufferSource({
|
||||
url: this._buffers.get(`${dir}${Math.random() > 0.5 ? 1 : 2}`),
|
||||
context: this.context,
|
||||
curve: 'exponential',
|
||||
fadeIn: 0.05,
|
||||
fadeOut: 0.1,
|
||||
}).connect(this.output);
|
||||
this._currentSound.start(time, randomBetween(0, 0.01), undefined, 0.1 * randomBetween(0.5, 1));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Put the pedal down
|
||||
*/
|
||||
down(time) {
|
||||
this._squash(time);
|
||||
this._downTime = time;
|
||||
this._playSample(time, 'down');
|
||||
}
|
||||
/**
|
||||
* Put the pedal up
|
||||
*/
|
||||
up(time) {
|
||||
this._squash(time);
|
||||
this._downTime = Infinity;
|
||||
this._playSample(time, 'up');
|
||||
}
|
||||
/**
|
||||
* Indicates if the pedal is down at the given time
|
||||
*/
|
||||
isDown(time) {
|
||||
return time > this._downTime;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A single velocity of strings
|
||||
*/
|
||||
class PianoString extends ToneAudioNode {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.name = 'PianoString';
|
||||
this._urls = {};
|
||||
// create the urls
|
||||
options.notes.forEach(note => this._urls[note] = getNotesUrl(note, options.velocity));
|
||||
this.samples = options.samples;
|
||||
}
|
||||
load() {
|
||||
return new Promise(onload => {
|
||||
this._sampler = this.output = new Sampler({
|
||||
attack: 0,
|
||||
baseUrl: this.samples,
|
||||
curve: 'exponential',
|
||||
onload,
|
||||
release: 0.4,
|
||||
urls: this._urls,
|
||||
volume: 3,
|
||||
});
|
||||
});
|
||||
}
|
||||
triggerAttack(note, time, velocity) {
|
||||
this._sampler.triggerAttack(note, time, velocity);
|
||||
}
|
||||
triggerRelease(note, time) {
|
||||
this._sampler.triggerRelease(note, time);
|
||||
}
|
||||
}
|
||||
|
||||
var __awaiter$1 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
/**
|
||||
* Manages all of the hammered string sounds
|
||||
*/
|
||||
class PianoStrings extends PianoComponent {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
const notes = getNotesInRange(options.minNote, options.maxNote);
|
||||
const velocities = velocitiesMap[options.velocities].slice();
|
||||
this._strings = velocities.map(velocity => {
|
||||
const string = new PianoString(Object.assign(options, {
|
||||
notes, velocity,
|
||||
}));
|
||||
return string;
|
||||
});
|
||||
this._activeNotes = new Map();
|
||||
}
|
||||
/**
|
||||
* Scale a value between a given range
|
||||
*/
|
||||
scale(val, inMin, inMax, outMin, outMax) {
|
||||
return ((val - inMin) / (inMax - inMin)) * (outMax - outMin) + outMin;
|
||||
}
|
||||
triggerAttack(note, time, velocity) {
|
||||
const scaledVel = this.scale(velocity, 0, 1, -0.5, this._strings.length - 0.51);
|
||||
const stringIndex = Math.max(Math.round(scaledVel), 0);
|
||||
let gain = 1 + scaledVel - stringIndex;
|
||||
if (this._strings.length === 1) {
|
||||
gain = velocity;
|
||||
}
|
||||
const sampler = this._strings[stringIndex];
|
||||
if (this._activeNotes.has(note)) {
|
||||
this.triggerRelease(note, time);
|
||||
}
|
||||
this._activeNotes.set(note, sampler);
|
||||
sampler.triggerAttack(Midi(note).toNote(), time, gain);
|
||||
}
|
||||
triggerRelease(note, time) {
|
||||
// trigger the release of all of the notes at that velociy
|
||||
if (this._activeNotes.has(note)) {
|
||||
this._activeNotes.get(note).triggerRelease(Midi(note).toNote(), time);
|
||||
this._activeNotes.delete(note);
|
||||
}
|
||||
}
|
||||
_internalLoad() {
|
||||
return __awaiter$1(this, void 0, void 0, function* () {
|
||||
yield Promise.all(this._strings.map((s) => __awaiter$1(this, void 0, void 0, function* () {
|
||||
yield s.load();
|
||||
s.connect(this.output);
|
||||
})));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var __awaiter$2 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
/**
|
||||
* The Piano
|
||||
*/
|
||||
class Piano extends ToneAudioNode {
|
||||
constructor() {
|
||||
super(optionsFromArguments(Piano.getDefaults(), arguments));
|
||||
this.name = 'Piano';
|
||||
this.input = undefined;
|
||||
this.output = new Gain({ context: this.context });
|
||||
/**
|
||||
* The currently held notes
|
||||
*/
|
||||
this._heldNotes = new Map();
|
||||
/**
|
||||
* If it's loaded or not
|
||||
*/
|
||||
this._loaded = false;
|
||||
const options = optionsFromArguments(Piano.getDefaults(), arguments);
|
||||
// make sure it ends with a /
|
||||
if (!options.url.endsWith('/')) {
|
||||
options.url += '/';
|
||||
}
|
||||
this.maxPolyphony = options.maxPolyphony;
|
||||
this._heldNotes = new Map();
|
||||
this._sustainedNotes = new Map();
|
||||
this._strings = new PianoStrings(Object.assign({}, options, {
|
||||
enabled: true,
|
||||
samples: options.url,
|
||||
volume: options.volume.strings,
|
||||
})).connect(this.output);
|
||||
this.strings = this._strings.volume;
|
||||
this._pedal = new Pedal(Object.assign({}, options, {
|
||||
enabled: options.pedal,
|
||||
samples: options.url,
|
||||
volume: options.volume.pedal,
|
||||
})).connect(this.output);
|
||||
this.pedal = this._pedal.volume;
|
||||
this._keybed = new Keybed(Object.assign({}, options, {
|
||||
enabled: options.release,
|
||||
samples: options.url,
|
||||
volume: options.volume.keybed,
|
||||
})).connect(this.output);
|
||||
this.keybed = this._keybed.volume;
|
||||
this._harmonics = new Harmonics(Object.assign({}, options, {
|
||||
enabled: options.release,
|
||||
samples: options.url,
|
||||
volume: options.volume.harmonics,
|
||||
})).connect(this.output);
|
||||
this.harmonics = this._harmonics.volume;
|
||||
}
|
||||
static getDefaults() {
|
||||
return Object.assign(ToneAudioNode.getDefaults(), {
|
||||
maxNote: 108,
|
||||
minNote: 21,
|
||||
pedal: true,
|
||||
release: false,
|
||||
url: 'https://tambien.github.io/Piano/audio/',
|
||||
velocities: 1,
|
||||
maxPolyphony: 32,
|
||||
volume: {
|
||||
harmonics: 0,
|
||||
keybed: 0,
|
||||
pedal: 0,
|
||||
strings: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Load all the samples
|
||||
*/
|
||||
load() {
|
||||
return __awaiter$2(this, void 0, void 0, function* () {
|
||||
yield Promise.all([
|
||||
this._strings.load(),
|
||||
this._pedal.load(),
|
||||
this._keybed.load(),
|
||||
this._harmonics.load(),
|
||||
]);
|
||||
this._loaded = true;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* If all the samples are loaded or not
|
||||
*/
|
||||
get loaded() {
|
||||
return this._loaded;
|
||||
}
|
||||
/**
|
||||
* Put the pedal down at the given time. Causes subsequent
|
||||
* notes and currently held notes to sustain.
|
||||
*/
|
||||
pedalDown({ time = this.immediate() } = {}) {
|
||||
if (this.loaded) {
|
||||
time = this.toSeconds(time);
|
||||
if (!this._pedal.isDown(time)) {
|
||||
this._pedal.down(time);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Put the pedal up. Dampens sustained notes
|
||||
*/
|
||||
pedalUp({ time = this.immediate() } = {}) {
|
||||
if (this.loaded) {
|
||||
const seconds = this.toSeconds(time);
|
||||
if (this._pedal.isDown(seconds)) {
|
||||
this._pedal.up(seconds);
|
||||
// dampen each of the notes
|
||||
this._sustainedNotes.forEach((t, note) => {
|
||||
if (!this._heldNotes.has(note)) {
|
||||
this._strings.triggerRelease(note, seconds);
|
||||
}
|
||||
});
|
||||
this._sustainedNotes.clear();
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Play a note.
|
||||
* @param note The note to play. If it is a number, it is assumed to be MIDI
|
||||
* @param velocity The velocity to play the note
|
||||
* @param time The time of the event
|
||||
*/
|
||||
keyDown({ note, midi, time = this.immediate(), velocity = 0.8 }) {
|
||||
if (this.loaded && this.maxPolyphony > this._heldNotes.size + this._sustainedNotes.size) {
|
||||
time = this.toSeconds(time);
|
||||
if (isString(note)) {
|
||||
midi = Math.round(Midi(note).toMidi());
|
||||
}
|
||||
if (!this._heldNotes.has(midi)) {
|
||||
// record the start time and velocity
|
||||
this._heldNotes.set(midi, { time, velocity });
|
||||
this._strings.triggerAttack(midi, time, velocity);
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.warn('samples not loaded');
|
||||
}
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Release a held note.
|
||||
*/
|
||||
keyUp({ note, midi, time = this.immediate(), velocity = 0.8 }) {
|
||||
if (this.loaded) {
|
||||
time = this.toSeconds(time);
|
||||
if (isString(note)) {
|
||||
midi = Math.round(Midi(note).toMidi());
|
||||
}
|
||||
if (this._heldNotes.has(midi)) {
|
||||
const prevNote = this._heldNotes.get(midi);
|
||||
this._heldNotes.delete(midi);
|
||||
// compute the release velocity
|
||||
const holdTime = Math.pow(Math.max(time - prevNote.time, 0.1), 0.7);
|
||||
const prevVel = prevNote.velocity;
|
||||
let dampenGain = (3 / holdTime) * prevVel * velocity;
|
||||
dampenGain = Math.max(dampenGain, 0.4);
|
||||
dampenGain = Math.min(dampenGain, 4);
|
||||
if (this._pedal.isDown(time)) {
|
||||
if (!this._sustainedNotes.has(midi)) {
|
||||
this._sustainedNotes.set(midi, time);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// release the string sound
|
||||
this._strings.triggerRelease(midi, time);
|
||||
// trigger the harmonics sound
|
||||
this._harmonics.triggerAttack(midi, time, dampenGain);
|
||||
}
|
||||
// trigger the keybed release sound
|
||||
this._keybed.start(midi, time, velocity);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
stopAll() {
|
||||
this.pedalUp();
|
||||
this._heldNotes.forEach((_, midi) => {
|
||||
this.keyUp({ midi });
|
||||
});
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
var __awaiter$3 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
|
||||
export { Piano };
|
||||
19651
docs/_snowpack/pkg/common/index-b6fc655f.js
Normal file
19651
docs/_snowpack/pkg/common/index-b6fc655f.js
Normal file
File diff suppressed because it is too large
Load Diff
37
docs/_snowpack/pkg/common/webmidi.min-97732fd4.js
Normal file
37
docs/_snowpack/pkg/common/webmidi.min-97732fd4.js
Normal file
File diff suppressed because one or more lines are too long
@ -1,6 +1,7 @@
|
||||
{
|
||||
"imports": {
|
||||
"@tonaljs/tonal": "./@tonaljs/tonal.js",
|
||||
"@tonejs/piano": "./@tonejs/piano.js",
|
||||
"chord-voicings": "./chord-voicings.js",
|
||||
"codemirror/lib/codemirror.css": "./codemirror/lib/codemirror.css",
|
||||
"codemirror/mode/javascript/javascript.js": "./codemirror/mode/javascript/javascript.js",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
14
docs/dist/App.js
vendored
14
docs/dist/App.js
vendored
@ -14,7 +14,6 @@ try {
|
||||
} catch (err) {
|
||||
console.warn("failed to decode", err);
|
||||
}
|
||||
Tone.setContext(new Tone.Context({latencyHint: "playback", lookAhead: 1}));
|
||||
const defaultSynth = new Tone.PolySynth().chain(new Tone.Gain(0.5), Tone.getDestination());
|
||||
defaultSynth.set({
|
||||
oscillator: {type: "triangle"},
|
||||
@ -30,7 +29,7 @@ function getRandomTune() {
|
||||
const randomTune = getRandomTune();
|
||||
function App() {
|
||||
const [editor, setEditor] = useState();
|
||||
const {setCode, setPattern, error, code, cycle, dirty, log, togglePlay, activateCode, pattern, pushLog} = useRepl({
|
||||
const {setCode, setPattern, error, code, cycle, dirty, log, togglePlay, activateCode, pattern, pushLog, pending} = useRepl({
|
||||
tune: decoded || randomTune,
|
||||
defaultSynth,
|
||||
onDraw: useCallback(markEvent(editor), [editor])
|
||||
@ -40,12 +39,11 @@ function App() {
|
||||
logBox.current.scrollTop = logBox.current?.scrollHeight;
|
||||
}, [log]);
|
||||
useLayoutEffect(() => {
|
||||
const handleKeyPress = (e) => {
|
||||
const handleKeyPress = async (e) => {
|
||||
if (e.ctrlKey || e.altKey) {
|
||||
switch (e.code) {
|
||||
case "Enter":
|
||||
activateCode();
|
||||
!cycle.started && cycle.start();
|
||||
await activateCode();
|
||||
break;
|
||||
case "Period":
|
||||
cycle.stop();
|
||||
@ -81,11 +79,11 @@ function App() {
|
||||
}, "Strudel REPL")), /* @__PURE__ */ React.createElement("div", {
|
||||
className: "flex space-x-4"
|
||||
}, /* @__PURE__ */ React.createElement("button", {
|
||||
onClick: () => {
|
||||
onClick: async () => {
|
||||
const _code = getRandomTune();
|
||||
console.log("tune", _code);
|
||||
setCode(_code);
|
||||
const parsed = evaluate(_code);
|
||||
const parsed = await evaluate(_code);
|
||||
setPattern(parsed.pattern);
|
||||
}
|
||||
}, "🎲 random tune"), /* @__PURE__ */ React.createElement("button", null, /* @__PURE__ */ React.createElement("a", {
|
||||
@ -116,7 +114,7 @@ function App() {
|
||||
}, error?.message || "unknown error")), /* @__PURE__ */ React.createElement("button", {
|
||||
className: "flex-none w-full border border-gray-700 p-2 bg-slate-700 hover:bg-slate-500",
|
||||
onClick: () => togglePlay()
|
||||
}, cycle.started ? "pause" : "play"), /* @__PURE__ */ React.createElement("textarea", {
|
||||
}, !pending ? /* @__PURE__ */ React.createElement(React.Fragment, null, cycle.started ? "pause" : "play") : /* @__PURE__ */ React.createElement(React.Fragment, null, "loading...")), /* @__PURE__ */ React.createElement("textarea", {
|
||||
className: "grow bg-[#283237] border-0 text-xs min-h-[200px]",
|
||||
value: log,
|
||||
readOnly: true,
|
||||
|
||||
2
docs/dist/CodeMirror.js
vendored
2
docs/dist/CodeMirror.js
vendored
@ -20,7 +20,7 @@ export default function CodeMirror({value, onChange, options, editorDidMount}) {
|
||||
});
|
||||
}
|
||||
export const markEvent = (editor) => (time, event) => {
|
||||
const locs = event.value.locations;
|
||||
const locs = event.context.locations;
|
||||
if (!locs || !editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
4
docs/dist/evaluate.js
vendored
4
docs/dist/evaluate.js
vendored
@ -20,9 +20,9 @@ function hackLiteral(literal, names, func) {
|
||||
hackLiteral(String, ["mini", "m"], bootstrapped.mini);
|
||||
hackLiteral(String, ["pure", "p"], bootstrapped.pure);
|
||||
Object.assign(globalThis, bootstrapped, Tone, toneHelpers);
|
||||
export const evaluate = (code) => {
|
||||
export const evaluate = async (code) => {
|
||||
const shapeshifted = shapeshifter(code);
|
||||
let evaluated = eval(shapeshifted);
|
||||
let evaluated = await eval(shapeshifted);
|
||||
if (typeof evaluated === "function") {
|
||||
evaluated = evaluated();
|
||||
}
|
||||
|
||||
3
docs/dist/parse.js
vendored
3
docs/dist/parse.js
vendored
@ -90,7 +90,8 @@ export function patternifyAST(ast) {
|
||||
return ast.source_;
|
||||
}
|
||||
const {start, end} = ast.location_;
|
||||
return pure(ast.source_).withLocation({start, end});
|
||||
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
|
||||
return pure(value).withLocation({start, end});
|
||||
}
|
||||
return patternifyAST(ast.source_);
|
||||
case "stretch":
|
||||
|
||||
13
docs/dist/shapeshifter.js
vendored
13
docs/dist/shapeshifter.js
vendored
@ -92,15 +92,16 @@ export default (code) => {
|
||||
// add to location to pure(x) calls
|
||||
if (node.type === 'CallExpression' && node.callee.name === 'pure') {
|
||||
const literal = node.arguments[0];
|
||||
const value = literal[{ LiteralNumericExpression: 'value', LiteralStringExpression: 'name' }[literal.type]];
|
||||
return reifyWithLocation(value + '', node.arguments[0], ast.locations, artificialNodes);
|
||||
// const value = literal[{ LiteralNumericExpression: 'value', LiteralStringExpression: 'name' }[literal.type]];
|
||||
// console.log('value',value);
|
||||
return reifyWithLocation(literal, node.arguments[0], ast.locations, artificialNodes);
|
||||
}
|
||||
// replace pseudo note variables
|
||||
if (node.type === 'IdentifierExpression') {
|
||||
if (isNote(node.name)) {
|
||||
const value = node.name[1] === 's' ? node.name.replace('s', '#') : node.name;
|
||||
if (addLocations && isMarkable) {
|
||||
return reifyWithLocation(value, node, ast.locations, artificialNodes);
|
||||
return reifyWithLocation(new LiteralStringExpression({ value }), node, ast.locations, artificialNodes);
|
||||
}
|
||||
return new LiteralStringExpression({ value });
|
||||
}
|
||||
@ -110,7 +111,7 @@ export default (code) => {
|
||||
}
|
||||
if (addLocations && node.type === 'LiteralStringExpression' && isMarkable) {
|
||||
// console.log('add', node);
|
||||
return reifyWithLocation(node.value, node, ast.locations, artificialNodes);
|
||||
return reifyWithLocation(node, node, ast.locations, artificialNodes);
|
||||
}
|
||||
if (!addMiniLocations) {
|
||||
return wrapFunction('reify', node);
|
||||
@ -219,10 +220,10 @@ function wrapLocationOffset(node, stringNode, locations, artificialNodes) {
|
||||
|
||||
// turns node in reify(value).withLocation(location), where location is the node's location in the source code
|
||||
// with this, the reified pattern can pass its location to the event, to know where to highlight when it's active
|
||||
function reifyWithLocation(value, node, locations, artificialNodes) {
|
||||
function reifyWithLocation(literalNode, node, locations, artificialNodes) {
|
||||
const withLocation = new CallExpression({
|
||||
callee: new StaticMemberExpression({
|
||||
object: wrapFunction('reify', new LiteralStringExpression({ value })),
|
||||
object: wrapFunction('reify', literalNode),
|
||||
property: 'withLocation',
|
||||
}),
|
||||
arguments: [getLocationObject(node, locations)],
|
||||
|
||||
37
docs/dist/tonal.js
vendored
37
docs/dist/tonal.js
vendored
@ -1,15 +1,6 @@
|
||||
import {Note, Interval, Scale} from "../_snowpack/pkg/@tonaljs/tonal.js";
|
||||
import {Pattern as _Pattern} from "../_snowpack/link/strudel.js";
|
||||
const Pattern = _Pattern;
|
||||
function toNoteEvent(event) {
|
||||
if (typeof event === "string" || typeof event === "number") {
|
||||
return {value: event};
|
||||
}
|
||||
if (event.value) {
|
||||
return event;
|
||||
}
|
||||
throw new Error("not a valid note event: " + JSON.stringify(event));
|
||||
}
|
||||
const mod = (n, m) => n < 0 ? mod(n + m, m) : n % m;
|
||||
export function intervalDirection(from, to, direction = 1) {
|
||||
const sign = Math.sign(direction);
|
||||
@ -44,43 +35,37 @@ function scaleTranspose(scale, offset, note) {
|
||||
}
|
||||
return n + o;
|
||||
}
|
||||
Pattern.prototype._mapNotes = function(func) {
|
||||
return this.fmap((event) => {
|
||||
const noteEvent = toNoteEvent(event);
|
||||
return {...noteEvent, ...func(noteEvent)};
|
||||
});
|
||||
};
|
||||
Pattern.prototype._transpose = function(intervalOrSemitones) {
|
||||
return this._mapNotes(({value, scale}) => {
|
||||
return this._withEvent((event) => {
|
||||
const interval = !isNaN(Number(intervalOrSemitones)) ? Interval.fromSemitones(intervalOrSemitones) : String(intervalOrSemitones);
|
||||
if (typeof value === "number") {
|
||||
if (typeof event.value === "number") {
|
||||
const semitones = typeof interval === "string" ? Interval.semitones(interval) || 0 : interval;
|
||||
return {value: value + semitones};
|
||||
return event.withValue(() => event.value + semitones);
|
||||
}
|
||||
return {value: Note.transpose(value, interval), scale};
|
||||
return event.withValue(() => Note.transpose(event.value, interval));
|
||||
});
|
||||
};
|
||||
Pattern.prototype._scaleTranspose = function(offset) {
|
||||
return this._mapNotes(({value, scale}) => {
|
||||
if (!scale) {
|
||||
return this._withEvent((event) => {
|
||||
if (!event.context.scale) {
|
||||
throw new Error("can only use scaleTranspose after .scale");
|
||||
}
|
||||
if (typeof value !== "string") {
|
||||
if (typeof event.value !== "string") {
|
||||
throw new Error("can only use scaleTranspose with notes");
|
||||
}
|
||||
return {value: scaleTranspose(scale, Number(offset), value), scale};
|
||||
return event.withValue(() => scaleTranspose(event.context.scale, Number(offset), event.value));
|
||||
});
|
||||
};
|
||||
Pattern.prototype._scale = function(scale) {
|
||||
return this._mapNotes((value) => {
|
||||
let note = value.value;
|
||||
return this._withEvent((event) => {
|
||||
let note = event.value;
|
||||
const asNumber = Number(note);
|
||||
if (!isNaN(asNumber)) {
|
||||
let [tonic, scaleName] = Scale.tokenize(scale);
|
||||
const {pc, oct = 3} = Note.get(tonic);
|
||||
note = scaleTranspose(pc + " " + scaleName, asNumber, pc + oct);
|
||||
}
|
||||
return {...value, value: note, scale};
|
||||
return event.withValue(() => note).setContext({...event.context, scale});
|
||||
});
|
||||
};
|
||||
Pattern.prototype.define("transpose", (a, pat) => pat.transpose(a), {composable: true, patternified: true});
|
||||
|
||||
62
docs/dist/tone.js
vendored
62
docs/dist/tone.js
vendored
@ -17,20 +17,23 @@ import {
|
||||
Sampler,
|
||||
getDestination
|
||||
} from "../_snowpack/pkg/tone.js";
|
||||
import {Piano} from "../_snowpack/pkg/@tonejs/piano.js";
|
||||
const Pattern = _Pattern;
|
||||
Pattern.prototype.tone = function(instrument) {
|
||||
return this.fmap((value) => {
|
||||
value = typeof value !== "object" && !Array.isArray(value) ? {value} : value;
|
||||
const onTrigger = (time, event) => {
|
||||
return this._withEvent((event) => {
|
||||
const onTrigger = (time, event2) => {
|
||||
if (instrument.constructor.name === "PluckSynth") {
|
||||
instrument.triggerAttack(value.value, time);
|
||||
instrument.triggerAttack(event2.value, time);
|
||||
} else if (instrument.constructor.name === "NoiseSynth") {
|
||||
instrument.triggerAttackRelease(event.duration, time);
|
||||
instrument.triggerAttackRelease(event2.duration, time);
|
||||
} else if (instrument.constructor.name === "Piano") {
|
||||
instrument.keyDown({note: event2.value, time, velocity: 0.5});
|
||||
instrument.keyUp({note: event2.value, time: time + event2.duration});
|
||||
} else {
|
||||
instrument.triggerAttackRelease(value.value, event.duration, time);
|
||||
instrument.triggerAttackRelease(event2.value, event2.duration, time);
|
||||
}
|
||||
};
|
||||
return {...value, instrument, onTrigger};
|
||||
return event.setContext({...event.context, instrument, onTrigger});
|
||||
});
|
||||
};
|
||||
Pattern.prototype.define("tone", (type, pat) => pat.tone(type), {composable: true, patternified: false});
|
||||
@ -45,6 +48,11 @@ export const pluck = (options) => new PluckSynth(options);
|
||||
export const polysynth = (options) => new PolySynth(options);
|
||||
export const sampler = (options) => new Sampler(options);
|
||||
export const synth = (options) => new Synth(options);
|
||||
export const piano = async (options = {velocities: 1}) => {
|
||||
const p = new Piano(options);
|
||||
await p.load();
|
||||
return p;
|
||||
};
|
||||
export const vol = (v) => new Gain(v);
|
||||
export const lowpass = (v) => new Filter(v, "lowpass");
|
||||
export const highpass = (v) => new Filter(v, "highpass");
|
||||
@ -75,13 +83,12 @@ Pattern.prototype._poly = function(type = "triangle") {
|
||||
if (!this.instrument) {
|
||||
this.instrument = poly(type);
|
||||
}
|
||||
return this.fmap((value) => {
|
||||
value = typeof value !== "object" && !Array.isArray(value) ? {value} : value;
|
||||
const onTrigger = (time, event) => {
|
||||
return this._withEvent((event) => {
|
||||
const onTrigger = (time, event2) => {
|
||||
this.instrument.set(instrumentConfig);
|
||||
this.instrument.triggerAttackRelease(value.value, event.duration, time);
|
||||
this.instrument.triggerAttackRelease(event2.value, event2.duration, time);
|
||||
};
|
||||
return {...value, instrumentConfig, onTrigger};
|
||||
return event.setContext({...event.context, instrumentConfig, onTrigger});
|
||||
});
|
||||
};
|
||||
Pattern.prototype.define("poly", (type, pat) => pat.poly(type), {composable: true, patternified: true});
|
||||
@ -96,8 +103,7 @@ const getTrigger = (getChain, value) => (time, event) => {
|
||||
}, event.duration * 2e3);
|
||||
};
|
||||
Pattern.prototype._synth = function(type = "triangle") {
|
||||
return this.fmap((value) => {
|
||||
value = typeof value !== "object" && !Array.isArray(value) ? {value} : value;
|
||||
return this._withEvent((event) => {
|
||||
const instrumentConfig = {
|
||||
oscillator: {type},
|
||||
envelope: {attack: 0.01, decay: 0.01, sustain: 0.6, release: 0.01}
|
||||
@ -107,37 +113,37 @@ Pattern.prototype._synth = function(type = "triangle") {
|
||||
instrument.set(instrumentConfig);
|
||||
return instrument;
|
||||
};
|
||||
const onTrigger = getTrigger(() => getInstrument().toDestination(), value.value);
|
||||
return {...value, getInstrument, instrumentConfig, onTrigger};
|
||||
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.fmap((value) => {
|
||||
if (!value?.getInstrument) {
|
||||
return this._withEvent((event) => {
|
||||
if (!event.context.getInstrument) {
|
||||
throw new Error("cannot chain adsr: need instrument first (like synth)");
|
||||
}
|
||||
const instrumentConfig = {...value.instrumentConfig, envelope: {attack, decay, sustain, release}};
|
||||
const instrumentConfig = {...event.context.instrumentConfig, envelope: {attack, decay, sustain, release}};
|
||||
const getInstrument = () => {
|
||||
const instrument = value.getInstrument();
|
||||
const instrument = event.context.getInstrument();
|
||||
instrument.set(instrumentConfig);
|
||||
return instrument;
|
||||
};
|
||||
const onTrigger = getTrigger(() => getInstrument().toDestination(), value.value);
|
||||
return {...value, getInstrument, instrumentConfig, onTrigger};
|
||||
const onTrigger = getTrigger(() => getInstrument().toDestination(), event.value);
|
||||
return event.setContext({...event.context, getInstrument, instrumentConfig, onTrigger});
|
||||
});
|
||||
};
|
||||
Pattern.prototype.chain = function(...effectGetters) {
|
||||
return this.fmap((value) => {
|
||||
if (!value?.getInstrument) {
|
||||
return this._withEvent((event) => {
|
||||
if (!event.context?.getInstrument) {
|
||||
throw new Error("cannot chain: need instrument first (like synth)");
|
||||
}
|
||||
const chain = (value.chain || []).concat(effectGetters);
|
||||
const chain = (event.context.chain || []).concat(effectGetters);
|
||||
const getChain = () => {
|
||||
const effects = chain.map((getEffect) => getEffect());
|
||||
return value.getInstrument().chain(...effects, getDestination());
|
||||
return event.context.getInstrument().chain(...effects, getDestination());
|
||||
};
|
||||
const onTrigger = getTrigger(getChain, value.value);
|
||||
return {...value, getChain, onTrigger, chain};
|
||||
const onTrigger = getTrigger(getChain, event.value);
|
||||
return event.setContext({...event.context, getChain, onTrigger, chain});
|
||||
});
|
||||
};
|
||||
export const autofilter = (freq = 1) => () => new AutoFilter(freq).start();
|
||||
|
||||
8
docs/dist/tunes.js
vendored
8
docs/dist/tunes.js
vendored
@ -426,3 +426,11 @@ export const sowhatelse = `()=> {
|
||||
"[2,4]/4".scale('D dorian').apply(t).tone(instr('pad')).mask("<x x x ~>/8")
|
||||
).fast(6/8)
|
||||
}`;
|
||||
export const barryHarris = `piano()
|
||||
.then(p => "0,2,[7 6]"
|
||||
.add("<0 1 2 3 4 5 7 8>")
|
||||
.scale('C bebop major')
|
||||
.transpose("<0 1 2 1>/8")
|
||||
.slow(2)
|
||||
.tone(p.toDestination()))
|
||||
`;
|
||||
|
||||
7
docs/dist/useCycle.js
vendored
7
docs/dist/useCycle.js
vendored
@ -1,6 +1,6 @@
|
||||
import {useEffect, useState} from "../_snowpack/pkg/react.js";
|
||||
import * as Tone from "../_snowpack/pkg/tone.js";
|
||||
import {TimeSpan} from "../_snowpack/link/strudel.js";
|
||||
import {TimeSpan, State} from "../_snowpack/link/strudel.js";
|
||||
function useCycle(props) {
|
||||
const {onEvent, onQuery, onSchedule, ready = true, onDraw} = props;
|
||||
const [started, setStarted] = useState(false);
|
||||
@ -8,7 +8,7 @@ function useCycle(props) {
|
||||
const activeCycle = () => Math.floor(Tone.getTransport().seconds / cycleDuration);
|
||||
const query = (cycle = activeCycle()) => {
|
||||
const timespan = new TimeSpan(cycle, cycle + 1);
|
||||
const events = onQuery?.(timespan) || [];
|
||||
const events = onQuery?.(new State(timespan)) || [];
|
||||
onSchedule?.(events, cycle);
|
||||
const cancelFrom = timespan.begin.valueOf();
|
||||
Tone.getTransport().cancel(cancelFrom);
|
||||
@ -22,7 +22,8 @@ function useCycle(props) {
|
||||
const toneEvent = {
|
||||
time: event.part.begin.valueOf(),
|
||||
duration: event.whole.end.sub(event.whole.begin).valueOf(),
|
||||
value: event.value
|
||||
value: event.value,
|
||||
context: event.context
|
||||
};
|
||||
onEvent(time, toneEvent);
|
||||
Tone.Draw.schedule(() => {
|
||||
|
||||
23
docs/dist/useRepl.js
vendored
23
docs/dist/useRepl.js
vendored
@ -12,19 +12,22 @@ function useRepl({tune, defaultSynth, autolink = true, onEvent, onDraw}) {
|
||||
const [activeCode, setActiveCode] = useState();
|
||||
const [log, setLog] = useState("");
|
||||
const [error, setError] = useState();
|
||||
const [pending, setPending] = useState(false);
|
||||
const [hash, setHash] = useState("");
|
||||
const [pattern, setPattern] = useState();
|
||||
const dirty = code !== activeCode || error;
|
||||
const generateHash = () => encodeURIComponent(btoa(code));
|
||||
const activateCode = (_code = code) => {
|
||||
!cycle.started && cycle.start();
|
||||
broadcast({type: "start", from: id});
|
||||
const activateCode = async (_code = code) => {
|
||||
if (activeCode && !dirty) {
|
||||
setError(void 0);
|
||||
!cycle.started && cycle.start();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const parsed = evaluate(_code);
|
||||
setPending(true);
|
||||
const parsed = await evaluate(_code);
|
||||
!cycle.started && cycle.start();
|
||||
broadcast({type: "start", from: id});
|
||||
setPattern(() => parsed.pattern);
|
||||
if (autolink) {
|
||||
window.location.hash = "#" + encodeURIComponent(btoa(code));
|
||||
@ -32,6 +35,7 @@ function useRepl({tune, defaultSynth, autolink = true, onEvent, onDraw}) {
|
||||
setHash(generateHash());
|
||||
setError(void 0);
|
||||
setActiveCode(_code);
|
||||
setPending(false);
|
||||
} catch (err) {
|
||||
err.message = "evaluation error: " + err.message;
|
||||
console.warn(err);
|
||||
@ -48,8 +52,9 @@ function useRepl({tune, defaultSynth, autolink = true, onEvent, onDraw}) {
|
||||
onEvent: useCallback((time, event) => {
|
||||
try {
|
||||
onEvent?.(event);
|
||||
if (!event.value?.onTrigger) {
|
||||
const note = event.value?.value || event.value;
|
||||
const {onTrigger} = event.context;
|
||||
if (!onTrigger) {
|
||||
const note = event.value;
|
||||
if (!isNote(note)) {
|
||||
throw new Error("not a note: " + note);
|
||||
}
|
||||
@ -59,7 +64,6 @@ function useRepl({tune, defaultSynth, autolink = true, onEvent, onDraw}) {
|
||||
throw new Error("no defaultSynth passed to useRepl.");
|
||||
}
|
||||
} else {
|
||||
const {onTrigger} = event.value;
|
||||
onTrigger(time, event);
|
||||
}
|
||||
} catch (err) {
|
||||
@ -68,9 +72,9 @@ function useRepl({tune, defaultSynth, autolink = true, onEvent, onDraw}) {
|
||||
pushLog(err.message);
|
||||
}
|
||||
}, [onEvent]),
|
||||
onQuery: useCallback((span) => {
|
||||
onQuery: useCallback((state) => {
|
||||
try {
|
||||
return pattern?.query(span) || [];
|
||||
return pattern?.query(state) || [];
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
err.message = "query error: " + err.message;
|
||||
@ -95,6 +99,7 @@ function useRepl({tune, defaultSynth, autolink = true, onEvent, onDraw}) {
|
||||
}
|
||||
};
|
||||
return {
|
||||
pending,
|
||||
code,
|
||||
setCode,
|
||||
pattern,
|
||||
|
||||
15
docs/dist/voicings.js
vendored
15
docs/dist/voicings.js
vendored
@ -10,7 +10,7 @@ const getVoicing = (chord, lastVoicing, range = ["F3", "A4"]) => dictionaryVoici
|
||||
});
|
||||
const Pattern = _Pattern;
|
||||
Pattern.prototype.fmapNested = function(func) {
|
||||
return new Pattern((span) => this.query(span).map((event) => reify(func(event)).query(span).map((hap) => new Hap(event.whole, event.part, hap.value))).flat());
|
||||
return new Pattern((span) => this.query(span).map((event) => reify(func(event)).query(span).map((hap) => new Hap(event.whole, event.part, hap.value, hap.context))).flat());
|
||||
};
|
||||
Pattern.prototype.voicings = function(range) {
|
||||
let lastVoicing;
|
||||
@ -18,15 +18,16 @@ Pattern.prototype.voicings = function(range) {
|
||||
range = ["F3", "A4"];
|
||||
}
|
||||
return this.fmapNested((event) => {
|
||||
lastVoicing = getVoicing(event.value?.value || event.value, lastVoicing, range);
|
||||
return stack(...lastVoicing);
|
||||
lastVoicing = getVoicing(event.value, lastVoicing, range);
|
||||
return stack(...lastVoicing)._withContext(() => ({
|
||||
locations: event.context.locations || []
|
||||
}));
|
||||
});
|
||||
};
|
||||
Pattern.prototype.rootNotes = function(octave = 2) {
|
||||
return this._mapNotes((value) => {
|
||||
const [_, root] = value.value.match(/^([a-gA-G])[b#]?.*$/);
|
||||
const bassNote = root + octave;
|
||||
return {...value, value: bassNote};
|
||||
return this.fmap((value) => {
|
||||
const [_, root] = value.match(/^([a-gA-G])[b#]?.*$/);
|
||||
return root + octave;
|
||||
});
|
||||
};
|
||||
Pattern.prototype.define("voicings", (range, pat) => pat.voicings(range), {composable: true});
|
||||
|
||||
@ -843,8 +843,8 @@ Ensure the default browser behavior of the `hidden` attribute.
|
||||
.CodeMirror {
|
||||
width: 100% !important;
|
||||
height: inherit !important;
|
||||
}.justify-center {
|
||||
justify-content: center;
|
||||
}main {
|
||||
margin: 0 auto;
|
||||
}.hover\:bg-slate-500:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(100 116 139 / var(--tw-bg-opacity));
|
||||
@ -1346,4 +1346,4 @@ span.CodeMirror-selectedtext { background: none; }
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=index.fd7d9b66.css.map */
|
||||
/*# sourceMappingURL=index.0ea4d9ed.css.map */
|
||||
1
docs/tutorial/index.0ea4d9ed.css.map
Normal file
1
docs/tutorial/index.0ea4d9ed.css.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="icon" href="/tutorial/favicon.e3ab9dd9.ico">
|
||||
<link rel="stylesheet" type="text/css" href="/tutorial/index.fd7d9b66.css">
|
||||
<link rel="stylesheet" type="text/css" href="/tutorial/index.0ea4d9ed.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="Strudel REPL">
|
||||
<title>Strudel Tutorial</title>
|
||||
@ -11,6 +11,6 @@
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<script src="/tutorial/index.4ae5d228.js" defer=""></script>
|
||||
<script src="/tutorial/index.26c147ee.js" defer=""></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user