Merge pull request #105 from tidalcycles/in-source-doc

In source doc
This commit is contained in:
Alex McLean 2022-05-06 15:18:41 +02:00 committed by GitHub
commit 14487ed5cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 2603 additions and 187 deletions

1
.gitignore vendored
View File

@ -27,4 +27,5 @@ node_modules/
repl-parcel repl-parcel
mytunes.ts mytunes.ts
doc doc
out
.parcel-cache .parcel-cache

1037
doc.json Normal file

File diff suppressed because it is too large Load Diff

11
jsdoc.config.json Normal file
View File

@ -0,0 +1,11 @@
{
"source": {
"includePattern": ".+\\.(js(doc|x)?|mjs)$",
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser"
},
"plugins": ["plugins/markdown"],
"opts": {
"destination": "./out/",
"recurse": true
}
}

1175
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,9 @@
"setup": "npm i && npm run bootstrap && cd repl && npm i", "setup": "npm i && npm run bootstrap && cd repl && npm i",
"repl": "cd repl && npm run start", "repl": "cd repl && npm run start",
"osc": "cd packages/osc && npm run server", "osc": "cd packages/osc && npm run server",
"build": "cd repl && npm run build" "build": "cd repl && npm run build",
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json"
}, },
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
@ -33,6 +35,9 @@
"homepage": "https://strudel.tidalcycles.org", "homepage": "https://strudel.tidalcycles.org",
"devDependencies": { "devDependencies": {
"events": "^3.3.0", "events": "^3.3.0",
"jsdoc": "^3.6.10",
"jsdoc-json": "^2.0.2",
"jsdoc-to-markdown": "^7.1.1",
"lerna": "^4.0.0", "lerna": "^4.0.0",
"mocha": "^9.1.4" "mocha": "^9.1.4"
} }

View File

@ -14,7 +14,10 @@ export class Hap {
then the whole will be returned as None, in which case the given then the whole will be returned as None, in which case the given
value will have been sampled from the point halfway between the value will have been sampled from the point halfway between the
start and end of the 'part' timespan. start and end of the 'part' timespan.
The context is to store a list of source code locations causing the event The context is to store a list of source code locations causing the event.
The word 'Event' is more or less a reserved word in javascript, hence this
class is named called 'Hap'.
*/ */
constructor(whole, part, value, context = {}, stateful = false) { constructor(whole, part, value, context = {}, stateful = false) {
@ -37,18 +40,18 @@ export class Hap {
} }
withSpan(func) { withSpan(func) {
// Returns a new event with the function f applies to the event timespan. // Returns a new hap with the function f applies to the hap timespan.
const whole = this.whole ? func(this.whole) : undefined; const whole = this.whole ? func(this.whole) : undefined;
return new Hap(whole, func(this.part), this.value, this.context); return new Hap(whole, func(this.part), this.value, this.context);
} }
withValue(func) { withValue(func) {
// Returns a new event with the function f applies to the event value. // Returns a new hap with the function f applies to the hap value.
return new Hap(this.whole, this.part, func(this.value), this.context); return new Hap(this.whole, this.part, func(this.value), this.context);
} }
hasOnset() { hasOnset() {
// Test whether the event contains the onset, i.e that // Test whether the hap contains the onset, i.e that
// the beginning of the part is the same as that of the whole timespan.""" // the beginning of the part is the same as that of the whole timespan."""
return this.whole != undefined && this.whole.begin.equals(this.part.begin); return this.whole != undefined && this.whole.begin.equals(this.part.begin);
} }

View File

@ -13,20 +13,37 @@ import { unionWithObj } from './value.mjs';
import { isNote, toMidi, compose, removeUndefineds, flatten, id, listRange, curry, mod } from './util.mjs'; import { isNote, toMidi, compose, removeUndefineds, flatten, id, listRange, curry, mod } from './util.mjs';
import drawLine from './drawLine.mjs'; import drawLine from './drawLine.mjs';
/** @class Class representing a pattern. */
export class Pattern { export class Pattern {
// the following functions will get patternFactories as nested functions: /**
* Create a pattern.
* @param {function} query - The function that maps a State to Haps .
*/
constructor(query) { constructor(query) {
this.query = query; this.query = query;
} }
/**
* query haps insude the tiven time span
*
* @param {Fraction | number} begin from time
* @param {Fraction | number} end to time
* @returns Hap[]
* @example
* const pattern = sequence('a', ['b', 'c']);
* const haps = pattern.queryArc(0, 1);
*/
queryArc(begin, end) { queryArc(begin, end) {
return this.query(new State(new TimeSpan(begin, end))); return this.query(new State(new TimeSpan(begin, end)));
} }
/**
* Returns a new pattern, with queries split at cycle boundaries. This makes
* some calculations easier to express, as all haps are then constrained to
* happen within a cycle.
* @returns Pattern
*/
_splitQueries() { _splitQueries() {
// Splits queries at cycle boundaries. This makes some calculations
// easier to express, as all events are then constrained to happen within
// a cycle.
const pat = this; const pat = this;
const q = (state) => { const q = (state) => {
return flatten(state.span.spanCycles.map((subspan) => pat.query(state.setSpan(subspan)))); return flatten(state.span.spanCycles.map((subspan) => pat.query(state.setSpan(subspan))));
@ -34,48 +51,98 @@ export class Pattern {
return new Pattern(q); return new Pattern(q);
} }
/**
* Returns a new pattern, where the given function is applied to the query
* timespan before passing it to the original pattern.
* @param {Function} func the function to apply
* @returns Pattern
*/
withQuerySpan(func) { withQuerySpan(func) {
return new Pattern((state) => this.query(state.withSpan(func))); return new Pattern((state) => this.query(state.withSpan(func)));
} }
withQueryTime(func) { /**
// Returns a new pattern, with the function applied to both the begin * As with {@link Pattern#withQuerySpan|withQuerySpan}, but the function is applied to both the
// and end of the the query timespan * begin and end time of the query timespan.
* @param {Function} func the function to apply
* @returns Pattern
*/
withQueryTime(func) {
return new Pattern((state) => this.query(state.withSpan((span) => span.withTime(func)))); return new Pattern((state) => this.query(state.withSpan((span) => span.withTime(func))));
} }
withEventSpan(func) { /**
// Returns a new pattern, with the function applied to each event * Similar to {@link Pattern#withQuerySpan|withQuerySpan}, but the function is applied to the timespans
// timespan. * of all haps returned by pattern queries (both `part` timespans, and where
* present, `whole` timespans).
* @param {Function} func
* @returns Pattern
*/
withHapSpan(func) {
return new Pattern((state) => this.query(state).map((hap) => hap.withSpan(func))); return new Pattern((state) => this.query(state).map((hap) => hap.withSpan(func)));
} }
withEventTime(func) { /**
// Returns a new pattern, with the function applied to both the begin * As with {@link Pattern#withHapSpan|withHapSpan}, but the function is applied to both the
// and end of each event timespan. * begin and end time of the hap timespans.
return this.withEventSpan((span) => span.withTime(func)); * @param {Function} func the function to apply
* @returns Pattern
*/
withHapTime(func) {
return this.withHapSpan((span) => span.withTime(func));
} }
_withEvents(func) { /**
* Returns a new pattern with the given function applied to the list of haps returned by every query.
* @param {Function} func
* @returns Pattern
*/
_withHaps(func) {
return new Pattern((state) => func(this.query(state))); return new Pattern((state) => func(this.query(state)));
} }
_withEvent(func) { /**
return this._withEvents((events) => events.map(func)); * As with {@link Pattern#_withHaps}, but applies the function to every hap, rather than every list of haps.
* @param {Function} func
* @returns Pattern
*/
_withHap(func) {
return this._withHaps((haps) => haps.map(func));
} }
/**
* Returns a new pattern with the context field set to every hap set to the given value.
* @param {*} context
* @returns Pattern
*/
_setContext(context) { _setContext(context) {
return this._withEvent((event) => event.setContext(context)); return this._withHap((hap) => hap.setContext(context));
} }
/**
* Returns a new pattern with the given function applied to the context field of every hap.
* @param {Function} func
* @returns Pattern
*/
_withContext(func) { _withContext(func) {
return this._withEvent((event) => event.setContext(func(event.context))); return this._withHap((hap) => hap.setContext(func(hap.context)));
} }
/**
* Returns a new pattern with the context field of every hap set to an empty object.
* @returns Pattern
*/
_stripContext() { _stripContext() {
return this._withEvent((event) => event.setContext({})); return this._withHap((hap) => hap.setContext({}));
} }
/**
* Returns a new pattern with the given location information added to the
* context of every hap.
* @param {Number} start
* @param {Number} end
* @returns Pattern
*/
withLocation(start, end) { withLocation(start, end) {
const location = { const location = {
start: { line: start[0], column: start[1], offset: start[2] }, start: { line: start[0], column: start[1], offset: start[2] },
@ -113,39 +180,72 @@ export class Pattern {
}); });
} }
/**
* Returns a new pattern, with the function applied to the value of
* each hap. It has the alias {@link Pattern#fmap|fmap}.
* @param {Function} func
* @returns Pattern
*/
withValue(func) { withValue(func) {
// Returns a new pattern, with the function applied to the value of
// each event. It has the alias 'fmap'.
return new Pattern((state) => this.query(state).map((hap) => hap.withValue(func))); return new Pattern((state) => this.query(state).map((hap) => hap.withValue(func)));
} }
// alias /**
fmap(func) { * see {@link Pattern#withValue|withValue}
*/
fmap(func) {
return this.withValue(func); return this.withValue(func);
} }
_filterEvents(event_test) { /**
return new Pattern((state) => this.query(state).filter(event_test)); * Returns a new Pattern, which only returns haps that meet the given test.
* @param {Function} hap_test - a function which returns false for haps to be removed from the pattern
* @returns Pattern
*/
_filterHaps(hap_test) {
return new Pattern((state) => this.query(state).filter(hap_test));
} }
/**
* As with {@link Pattern#_filterHaps}, but the function is applied to values
* inside haps.
* @param {Function} value_test
* @returns Pattern
*/
_filterValues(value_test) { _filterValues(value_test) {
return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value))); return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value)));
} }
/**
* Returns a new pattern, with haps containing undefined values removed from
* query results.
* @returns Pattern
*/
_removeUndefineds() { _removeUndefineds() {
return this._filterValues((val) => val != undefined); return this._filterValues((val) => val != undefined);
} }
onsetsOnly() { /**
// Returns a new pattern that will only return events where the start * Returns a new pattern, with all haps without onsets filtered out. A hap
* with an onset is one with a `whole` timespan that begins at the same time
* as its `part` timespan.
* @returns Pattern
*/
onsetsOnly() {
// Returns a new pattern that will only return haps where the start
// of the 'whole' timespan matches the start of the 'part' // of the 'whole' timespan matches the start of the 'part'
// timespan, i.e. the events that include their 'onset'. // timespan, i.e. the haps that include their 'onset'.
return this._filterEvents((hap) => hap.hasOnset()); return this._filterHaps((hap) => hap.hasOnset());
} }
/**
* Returns a new pattern, with 'continuous' haps (those without 'whole'
* timespans) removed from query results.
* @returns Pattern
*/
discreteOnly() { discreteOnly() {
// removes continuous events that don't have a 'whole' timespan // removes continuous haps that don't have a 'whole' timespan
return this._filterEvents((hap) => hap.whole); return this._filterHaps((hap) => hap.whole);
} }
_appWhole(whole_func, pat_val) { _appWhole(whole_func, pat_val) {
@ -154,27 +254,38 @@ export class Pattern {
// pattern of functions. // pattern of functions.
const pat_func = this; const pat_func = this;
const query = function (state) { const query = function (state) {
const event_funcs = pat_func.query(state); const hap_funcs = pat_func.query(state);
const event_vals = pat_val.query(state); const hap_vals = pat_val.query(state);
const apply = function (event_func, event_val) { const apply = function (hap_func, hap_val) {
const s = event_func.part.intersection(event_val.part); const s = hap_func.part.intersection(hap_val.part);
if (s == undefined) { if (s == undefined) {
return undefined; return undefined;
} }
return new Hap( return new Hap(
whole_func(event_func.whole, event_val.whole), whole_func(hap_func.whole, hap_val.whole),
s, s,
event_func.value(event_val.value), hap_func.value(hap_val.value),
event_val.combineContext(event_func), hap_val.combineContext(hap_func),
); );
}; };
return flatten( return flatten(
event_funcs.map((event_func) => removeUndefineds(event_vals.map((event_val) => apply(event_func, event_val)))), hap_funcs.map((hap_func) => removeUndefineds(hap_vals.map((hap_val) => apply(hap_func, hap_val)))),
); );
}; };
return new Pattern(query); return new Pattern(query);
} }
/**
* When this method is called on a pattern of functions, it matches its haps
* with those in the given pattern of values. A new pattern is returned, with
* each matching value applied to the corresponding function.
*
* In this `appBoth` variant, where timespans of the function and value haps
* are not the same but do intersect, the resulting hap has a timespan of the
* intersection. This applies to both the part and the whole timespan.
* @param {Pattern} pat_val
* @returns Pattern
*/
appBoth(pat_val) { appBoth(pat_val) {
// Tidal's <*> // Tidal's <*>
const whole_func = function (span_a, span_b) { const whole_func = function (span_a, span_b) {
@ -186,14 +297,23 @@ export class Pattern {
return this._appWhole(whole_func, pat_val); return this._appWhole(whole_func, pat_val);
} }
/**
* As with {@link Pattern#appBoth|appBoth}, but the `whole` timespan is not the intersection,
* but the timespan from the function of patterns that this method is called
* on. In practice, this means that the pattern structure, including onsets,
* are preserved from the pattern of functions (often referred to as the left
* hand or inner pattern).
* @param {Pattern} pat_val
* @returns Pattern
*/
appLeft(pat_val) { appLeft(pat_val) {
const pat_func = this; const pat_func = this;
const query = function (state) { const query = function (state) {
const haps = []; const haps = [];
for (const hap_func of pat_func.query(state)) { for (const hap_func of pat_func.query(state)) {
const event_vals = pat_val.query(state.setSpan(hap_func.wholeOrPart())); const hap_vals = pat_val.query(state.setSpan(hap_func.wholeOrPart()));
for (const hap_val of event_vals) { for (const hap_val of hap_vals) {
const new_whole = hap_func.whole; const new_whole = hap_func.whole;
const new_part = hap_func.part.intersection(hap_val.part); const new_part = hap_func.part.intersection(hap_val.part);
if (new_part) { if (new_part) {
@ -209,6 +329,13 @@ export class Pattern {
return new Pattern(query); return new Pattern(query);
} }
/**
* As with {@link Pattern#appLeft|appLeft}, but `whole` timespans are instead taken from the
* pattern of values, i.e. structure is preserved from the right hand/outer
* pattern.
* @param {Pattern} pat_val
* @returns Pattern
*/
appRight(pat_val) { appRight(pat_val) {
const pat_func = this; const pat_func = this;
@ -232,6 +359,13 @@ export class Pattern {
return new Pattern(query); return new Pattern(query);
} }
/**
* Queries the pattern for the first cycle, returning Haps. Mainly of use when
* debugging a pattern.
* @param {Boolean} with_context - set to true, otherwise the context field
* will be stripped from the resulting haps.
* @returns [Hap]
*/
firstCycle(with_context = false) { firstCycle(with_context = false) {
var self = this; var self = this;
if (!with_context) { if (!with_context) {
@ -240,18 +374,30 @@ export class Pattern {
return self.query(new State(new TimeSpan(Fraction(0), Fraction(1)))); return self.query(new State(new TimeSpan(Fraction(0), Fraction(1))));
} }
/**
* Accessor for a list of values returned by querying the first cycle.
*/
get _firstCycleValues() { get _firstCycleValues() {
return this.firstCycle().map((hap) => hap.value); return this.firstCycle().map((hap) => hap.value);
} }
/**
* More human-readable version of the {@link Pattern#_firstCycleValues} accessor.
*/
get _showFirstCycle() { get _showFirstCycle() {
return this.firstCycle().map( return this.firstCycle().map(
(hap) => `${hap.value}: ${hap.whole.begin.toFraction()} - ${hap.whole.end.toFraction()}`, (hap) => `${hap.value}: ${hap.whole.begin.toFraction()} - ${hap.whole.end.toFraction()}`,
); );
} }
_sortEventsByPart() { /**
return this._withEvents((events) => * Returns a new pattern, which returns haps sorted in temporal order. Mainly
events.sort((a, b) => * of use when comparing two patterns for equality, in tests.
* @returns Pattern
*/
_sortHapsByPart() {
return this._withHaps((haps) =>
haps.sort((a, b) =>
a.part.begin a.part.begin
.sub(b.part.begin) .sub(b.part.begin)
.or(a.part.end.sub(b.part.end)) .or(a.part.end.sub(b.part.end))
@ -288,21 +434,21 @@ export class Pattern {
} }
_asNumber(dropfails = false, softfail = false) { _asNumber(dropfails = false, softfail = false) {
return this._withEvent((event) => { return this._withHap((hap) => {
const asNumber = Number(event.value); const asNumber = Number(hap.value);
if (!isNaN(asNumber)) { if (!isNaN(asNumber)) {
return event.withValue(() => asNumber); return hap.withValue(() => asNumber);
} }
const specialValue = { const specialValue = {
e: Math.E, e: Math.E,
pi: Math.PI, pi: Math.PI,
}[event.value]; }[hap.value];
if (typeof specialValue !== 'undefined') { if (typeof specialValue !== 'undefined') {
return event.withValue(() => specialValue); return hap.withValue(() => specialValue);
} }
if (isNote(event.value)) { if (isNote(hap.value)) {
// set context type to midi to let the player know its meant as midi number and not as frequency // set context type to midi to let the player know its meant as midi number and not as frequency
return new Hap(event.whole, event.part, toMidi(event.value), { ...event.context, type: 'midi' }); return new Hap(hap.whole, hap.part, toMidi(hap.value), { ...hap.context, type: 'midi' });
} }
if (dropfail) { if (dropfail) {
// return 'nothing' // return 'nothing'
@ -310,10 +456,10 @@ export class Pattern {
} }
if (softfail) { if (softfail) {
// return original hap // return original hap
return event; return hap;
} }
throw new Error('cannot parse as number: "' + event.value + '"'); throw new Error('cannot parse as number: "' + hap.value + '"');
return event; return hap;
}); });
if (dropfail) { if (dropfail) {
return result._removeUndefineds(); return result._removeUndefineds();
@ -389,7 +535,7 @@ export class Pattern {
join() { join() {
// Flattens a pattern of patterns into a pattern, where wholes are // Flattens a pattern of patterns into a pattern, where wholes are
// the intersection of matched inner and outer events. // the intersection of matched inner and outer haps.
return this.bind(id); return this.bind(id);
} }
@ -399,7 +545,7 @@ export class Pattern {
outerJoin() { outerJoin() {
// Flattens a pattern of patterns into a pattern, where wholes are // Flattens a pattern of patterns into a pattern, where wholes are
// taken from inner events. // taken from inner haps.
return this.outerBind(id); return this.outerBind(id);
} }
@ -409,35 +555,35 @@ export class Pattern {
innerJoin() { innerJoin() {
// Flattens a pattern of patterns into a pattern, where wholes are // Flattens a pattern of patterns into a pattern, where wholes are
// taken from inner events. // taken from inner haps.
return this.innerBind(id); return this.innerBind(id);
} }
// Flatterns patterns of patterns, by retriggering/resetting inner patterns at onsets of outer pattern events // Flatterns patterns of patterns, by retriggering/resetting inner patterns at onsets of outer pattern haps
_trigJoin(cycleZero = false) { _trigJoin(cycleZero = false) {
const pat_of_pats = this; const pat_of_pats = this;
return new Pattern((state) => { return new Pattern((state) => {
return ( return (
pat_of_pats pat_of_pats
// drop continuous events from the outer pattern. // drop continuous haps from the outer pattern.
.discreteOnly() .discreteOnly()
.query(state) .query(state)
.map((outer_hap) => { .map((outer_hap) => {
return ( return (
outer_hap.value outer_hap.value
// trig = align the inner pattern cycle start to outer pattern events // trig = align the inner pattern cycle start to outer pattern haps
// Trigzero = align the inner pattern cycle zero to outer pattern events // Trigzero = align the inner pattern cycle zero to outer pattern haps
.late(cycleZero ? outer_hap.whole.begin : outer_hap.whole.begin.cyclePos()) .late(cycleZero ? outer_hap.whole.begin : outer_hap.whole.begin.cyclePos())
.query(state) .query(state)
.map((inner_hap) => .map((inner_hap) =>
new Hap( new Hap(
// Supports continuous events in the inner pattern // Supports continuous haps in the inner pattern
inner_hap.whole ? inner_hap.whole.intersection(outer_hap.whole) : undefined, inner_hap.whole ? inner_hap.whole.intersection(outer_hap.whole) : undefined,
inner_hap.part.intersection(outer_hap.part), inner_hap.part.intersection(outer_hap.part),
inner_hap.value, inner_hap.value,
).setContext(outer_hap.combineContext(inner_hap)), ).setContext(outer_hap.combineContext(inner_hap)),
) )
// Drop events that didn't intersect // Drop haps that didn't intersect
.filter((hap) => hap.part) .filter((hap) => hap.part)
); );
}) })
@ -527,7 +673,7 @@ export class Pattern {
const end = cycle.add(span.end.sub(cycle).div(factor).min(1)); const end = cycle.add(span.end.sub(cycle).div(factor).min(1));
return new TimeSpan(begin, end); return new TimeSpan(begin, end);
}; };
return this.withQuerySpan(qf).withEventSpan(ef)._splitQueries(); return this.withQuerySpan(qf).withHapSpan(ef)._splitQueries();
} }
_compress(b, e) { _compress(b, e) {
@ -543,7 +689,7 @@ export class Pattern {
_fast(factor) { _fast(factor) {
const fastQuery = this.withQueryTime((t) => t.mul(factor)); const fastQuery = this.withQueryTime((t) => t.mul(factor));
return fastQuery.withEventTime((t) => t.div(factor)); return fastQuery.withHapTime((t) => t.div(factor));
} }
_slow(factor) { _slow(factor) {
@ -578,7 +724,7 @@ export class Pattern {
_early(offset) { _early(offset) {
// Equivalent of Tidal's <~ operator // Equivalent of Tidal's <~ operator
offset = Fraction(offset); offset = Fraction(offset);
return this.withQueryTime((t) => t.add(offset)).withEventTime((t) => t.sub(offset)); return this.withQueryTime((t) => t.add(offset)).withHapTime((t) => t.sub(offset));
} }
_late(offset) { _late(offset) {
@ -592,7 +738,7 @@ export class Pattern {
s = Fraction(s); s = Fraction(s);
const d = e.sub(s); const d = e.sub(s);
return this.withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s))) return this.withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s)))
.withEventSpan((span) => span.withCycle((t) => t.sub(s).div(d))) .withHapSpan((span) => span.withCycle((t) => t.sub(s).div(d)))
._splitQueries(); ._splitQueries();
} }
@ -632,7 +778,7 @@ export class Pattern {
} }
log() { log() {
return this._withEvent((e) => { return this._withHap((e) => {
return e.setContext({ ...e.context, logs: (e.context?.logs || []).concat([e.show()]) }); return e.setContext({ ...e.context, logs: (e.context?.logs || []).concat([e.show()]) });
}); });
} }
@ -800,14 +946,14 @@ export class Pattern {
return silence; return silence;
} }
// sets absolute duration of events // sets absolute duration of haps
_duration(value) { _duration(value) {
return this.withEventSpan((span) => new TimeSpan(span.begin, span.begin.add(value))); return this.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(value)));
} }
// sets event relative duration of events // sets hap relative duration of haps
_legato(value) { _legato(value) {
return this.withEventSpan((span) => new TimeSpan(span.begin, span.begin.add(span.end.sub(span.begin).mul(value)))); return this.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(span.end.sub(span.begin).mul(value))));
} }
_velocity(velocity) { _velocity(velocity) {
@ -957,8 +1103,14 @@ Pattern.prototype.factories = {
// Nothing // Nothing
export const silence = new Pattern((_) => []); export const silence = new Pattern((_) => []);
/** A discrete value that repeats once per cycle:
*
* @param {any} value - The value to repeat
* @returns {Pattern}
* @example
* pure('e4')
*/
export function pure(value) { export function pure(value) {
// A discrete value that repeats once per cycle
function query(state) { function query(state) {
return state.span.spanCycles.map((subspan) => new Hap(Fraction(subspan.begin).wholeCycle(), subspan, value)); return state.span.spanCycles.map((subspan) => new Hap(Fraction(subspan.begin).wholeCycle(), subspan, value));
} }
@ -978,8 +1130,13 @@ export function reify(thing) {
return pure(thing); return pure(thing);
} }
// Basic functions for combining patterns /** The given items are played at the same time at the same length:
*
* @param {...any} items - The items to stack
* @return {Pattern}
* @example
* stack(g3, b3, [e4, d4])
*/
export function stack(...pats) { export function stack(...pats) {
// Array test here is to avoid infinite recursions.. // Array test here is to avoid infinite recursions..
pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat))); pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat)));
@ -987,10 +1144,17 @@ export function stack(...pats) {
return new Pattern(query); return new Pattern(query);
} }
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
*
* synonyms: {@link cat}
*
* @param {...any} items - The items to concatenate
* @return {Pattern}
* @example
* slowcat(e5, b4, [d5, c5])
*
*/
export function slowcat(...pats) { export function slowcat(...pats) {
// Concatenation: combines a list of patterns, switching between them
// successively, one per cycle.
// Array test here is to avoid infinite recursions.. // Array test here is to avoid infinite recursions..
pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat))); pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat)));
@ -1006,14 +1170,16 @@ export function slowcat(...pats) {
// For example if three patterns are slowcat-ed, the fourth cycle of the result should // For example if three patterns are slowcat-ed, the fourth cycle of the result should
// be the second (rather than fourth) cycle from the first pattern. // be the second (rather than fourth) cycle from the first pattern.
const offset = span.begin.floor().sub(span.begin.div(pats.length).floor()); const offset = span.begin.floor().sub(span.begin.div(pats.length).floor());
return pat.withEventTime((t) => t.add(offset)).query(state.setSpan(span.withTime((t) => t.sub(offset)))); return pat.withHapTime((t) => t.add(offset)).query(state.setSpan(span.withTime((t) => t.sub(offset))));
}; };
return new Pattern(query)._splitQueries(); return new Pattern(query)._splitQueries();
} }
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle. Unlike slowcat, this version will skip cycles.
* @param {...any} items - The items to concatenate
* @return {Pattern}
*/
export function slowcatPrime(...pats) { export function slowcatPrime(...pats) {
// Concatenation: combines a list of patterns, switching between them
// successively, one per cycle. Unlike slowcat, this version will skip cycles.
pats = pats.map(reify); pats = pats.map(reify);
const query = function (state) { const query = function (state) {
const pat_n = Math.floor(state.span.begin) % pats.length; const pat_n = Math.floor(state.span.begin) % pats.length;
@ -1023,18 +1189,33 @@ export function slowcatPrime(...pats) {
return new Pattern(query)._splitQueries(); return new Pattern(query)._splitQueries();
} }
/** Concatenation: as with {@link slowcat}, but squashes a cycle from each pattern into one cycle
*
* Synonyms: {@link seq}, {@link sequence}
*
* @param {...any} items - The items to concatenate
* @return {Pattern}
* @example
* fastcat(e5, b4, [d5, c5])
* sequence(e5, b4, [d5, c5])
* seq(e5, b4, [d5, c5])
*/
export function fastcat(...pats) { export function fastcat(...pats) {
// Concatenation: as with slowcat, but squashes a cycle from each
// pattern into one cycle
return slowcat(...pats)._fast(pats.length); return slowcat(...pats)._fast(pats.length);
} }
/** See {@link slowcat} */
export function cat(...pats) { export function cat(...pats) {
return slowcat(...pats); return slowcat(...pats);
} }
/** Like {@link fastcat}, but where each step has a temporal weight:
* @param {...Array} items - The items to concatenate
* @return {Pattern}
* @example
* timeCat([3,e3],[1, g3])
*/
export function timeCat(...timepats) { export function timeCat(...timepats) {
// Like cat, but where each step has a temporal 'weight'
const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0)); const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0));
let begin = Fraction(0); let begin = Fraction(0);
const pats = []; const pats = [];
@ -1046,11 +1227,12 @@ export function timeCat(...timepats) {
return stack(...pats); return stack(...pats);
} }
/** See {@link fastcat} */
export function sequence(...pats) { export function sequence(...pats) {
return fastcat(...pats); return fastcat(...pats);
} }
// shorthand for sequence /** See {@link fastcat} */
export function seq(...pats) { export function seq(...pats) {
return fastcat(...pats); return fastcat(...pats);
} }

View File

@ -26,11 +26,11 @@ function speak(words, lang, voice) {
} }
Pattern.prototype._speak = function (lang, voice) { Pattern.prototype._speak = function (lang, voice) {
return this._withEvent((event) => { return this._withHap((hap) => {
const onTrigger = (time, event) => { const onTrigger = (time, hap) => {
speak(event.value, lang, voice); speak(hap.value, lang, voice);
}; };
return event.setContext({ ...event.context, onTrigger }); return hap.setContext({ ...hap.context, onTrigger });
}); });
}; };

View File

@ -57,7 +57,7 @@ const third = Fraction(1, 3);
const twothirds = Fraction(2, 3); const twothirds = Fraction(2, 3);
const sameFirst = (a, b) => { const sameFirst = (a, b) => {
return assert.deepStrictEqual(a._sortEventsByPart().firstCycle(), b._sortEventsByPart().firstCycle()); return assert.deepStrictEqual(a._sortHapsByPart().firstCycle(), b._sortHapsByPart().firstCycle());
}; };
describe('TimeSpan', function () { describe('TimeSpan', function () {
@ -320,7 +320,7 @@ describe('Pattern', function () {
}); });
}); });
describe('setSqueeze()', () => { describe('setSqueeze()', () => {
it('Can squeeze one pattern inside the events of another', () => { it('Can squeeze one pattern inside the haps of another', () => {
sameFirst( sameFirst(
sequence(1, [2, 3]).setSqueeze(sequence('a', 'b', 'c')), sequence(1, [2, 3]).setSqueeze(sequence('a', 'b', 'c')),
sequence( sequence(
@ -394,7 +394,7 @@ describe('Pattern', function () {
}); });
it('Makes things faster, with a pattern of factors', function () { it('Makes things faster, with a pattern of factors', function () {
assert.equal(pure('a').fast(sequence(1, 4)).firstCycle().length, 3); assert.equal(pure('a').fast(sequence(1, 4)).firstCycle().length, 3);
// .fast(sequence(1,silence) is a quick hack to cut an event in two.. // .fast(sequence(1,silence) is a quick hack to cut a hap in two..
assert.deepStrictEqual( assert.deepStrictEqual(
pure('a').fast(sequence(1, 4)).firstCycle(), pure('a').fast(sequence(1, 4)).firstCycle(),
stack(pure('a').fast(sequence(1, silence)), sequence(silence, ['a', 'a'])).firstCycle(), stack(pure('a').fast(sequence(1, silence)), sequence(silence, ['a', 'a'])).firstCycle(),
@ -468,7 +468,7 @@ describe('Pattern', function () {
}); });
it('Can alternate', function () { it('Can alternate', function () {
assert.deepStrictEqual( assert.deepStrictEqual(
pure(10).when(slowcat(true, false), add(3)).fast(4)._sortEventsByPart().firstCycle(), pure(10).when(slowcat(true, false), add(3)).fast(4)._sortHapsByPart().firstCycle(),
fastcat(13, 10, 13, 10).firstCycle(), fastcat(13, 10, 13, 10).firstCycle(),
); );
}); });
@ -679,7 +679,7 @@ describe('Pattern', function () {
}); });
}); });
describe('_setContext()', () => { describe('_setContext()', () => {
it('Can set the event context', () => { it('Can set the hap context', () => {
assert.deepStrictEqual( assert.deepStrictEqual(
pure('a') pure('a')
._setContext([ ._setContext([
@ -701,7 +701,7 @@ describe('Pattern', function () {
}); });
}); });
describe('_withContext()', () => { describe('_withContext()', () => {
it('Can update the event context', () => { it('Can update the hap context', () => {
assert.deepStrictEqual( assert.deepStrictEqual(
pure('a') pure('a')
._setContext([ ._setContext([
@ -756,13 +756,13 @@ describe('Pattern', function () {
}); });
}); });
describe('early', () => { describe('early', () => {
it('Can shift an event earlier', () => { it('Can shift a hap earlier', () => {
assert.deepStrictEqual(pure(30)._late(0.25).query(st(1, 2)), [ assert.deepStrictEqual(pure(30)._late(0.25).query(st(1, 2)), [
hap(ts(1 / 4, 5 / 4), ts(1, 5 / 4), 30), hap(ts(1 / 4, 5 / 4), ts(1, 5 / 4), 30),
hap(ts(5 / 4, 9 / 4), ts(5 / 4, 2), 30), hap(ts(5 / 4, 9 / 4), ts(5 / 4, 2), 30),
]); ]);
}); });
it('Can shift an event earlier, into negative time', () => { it('Can shift a hap earlier, into negative time', () => {
assert.deepStrictEqual(pure(30)._late(0.25).query(st(0, 1)), [ assert.deepStrictEqual(pure(30)._late(0.25).query(st(0, 1)), [
hap(ts(-3 / 4, 1 / 4), ts(0, 1 / 4), 30), hap(ts(-3 / 4, 1 / 4), ts(0, 1 / 4), 30),
hap(ts(1 / 4, 5 / 4), ts(1 / 4, 1), 30), hap(ts(1 / 4, 5 / 4), ts(1 / 4, 1), 30),
@ -780,9 +780,9 @@ describe('Pattern', function () {
describe('jux', () => { describe('jux', () => {
it('Can juxtapose', () => { it('Can juxtapose', () => {
assert.deepStrictEqual( assert.deepStrictEqual(
pure({ a: 1 }).jux(fast(2))._sortEventsByPart().firstCycle(), pure({ a: 1 }).jux(fast(2))._sortHapsByPart().firstCycle(),
stack(pure({ a: 1, pan: 0 }), pure({ a: 1, pan: 1 }).fast(2)) stack(pure({ a: 1, pan: 0 }), pure({ a: 1, pan: 1 }).fast(2))
._sortEventsByPart() ._sortHapsByPart()
.firstCycle(), .firstCycle(),
); );
}); });
@ -790,9 +790,9 @@ describe('Pattern', function () {
describe('juxBy', () => { describe('juxBy', () => {
it('Can juxtapose by half', () => { it('Can juxtapose by half', () => {
assert.deepStrictEqual( assert.deepStrictEqual(
pure({ a: 1 }).juxBy(0.5, fast(2))._sortEventsByPart().firstCycle(), pure({ a: 1 }).juxBy(0.5, fast(2))._sortHapsByPart().firstCycle(),
stack(pure({ a: 1, pan: 0.25 }), pure({ a: 1, pan: 0.75 }).fast(2)) stack(pure({ a: 1, pan: 0.25 }), pure({ a: 1, pan: 0.75 }).fast(2))
._sortEventsByPart() ._sortHapsByPart()
.firstCycle(), .firstCycle(),
); );
}); });
@ -821,7 +821,7 @@ describe('Pattern', function () {
sequence(pure('a').fast(3), [pure('b').fast(3), pure('c').fast(3)]).firstCycle(), sequence(pure('a').fast(3), [pure('b').fast(3), pure('c').fast(3)]).firstCycle(),
); );
}); });
it('Doesnt drop events in the 9th cycle', () => { it('Doesnt drop haps in the 9th cycle', () => {
// fixed with https://github.com/tidalcycles/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a // fixed with https://github.com/tidalcycles/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a
assert.equal(sequence(1, 2, 3).ply(2).early(8).firstCycle().length, 6); assert.equal(sequence(1, 2, 3).ply(2).early(8).firstCycle().length, 6);
}); });
@ -848,7 +848,7 @@ describe('Pattern', function () {
}); });
it('Can chop(2,3)', () => { it('Can chop(2,3)', () => {
assert.deepStrictEqual( assert.deepStrictEqual(
pure({ sound: 'a' }).fast(2).chop(2, 3)._sortEventsByPart().firstCycle(), pure({ sound: 'a' }).fast(2).chop(2, 3)._sortHapsByPart().firstCycle(),
sequence( sequence(
[ [
{ sound: 'a', begin: 0, end: 0.5 }, { sound: 'a', begin: 0, end: 0.5 },
@ -860,7 +860,7 @@ describe('Pattern', function () {
{ sound: 'a', begin: 2 / 3, end: 1 }, { sound: 'a', begin: 2 / 3, end: 1 },
], ],
) )
._sortEventsByPart() ._sortHapsByPart()
.firstCycle(), .firstCycle(),
); );
}); });

View File

@ -35,27 +35,27 @@ export const fromMidi = (n) => {
// const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m); // const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m);
export const mod = (n, m) => ((n % m) + m) % m; export const mod = (n, m) => ((n % m) + m) % m;
export const getPlayableNoteValue = (event) => { export const getPlayableNoteValue = (hap) => {
let { value: note, context } = event; let { value: note, context } = hap;
// if value is number => interpret as midi number as long as its not marked as frequency // if value is number => interpret as midi number as long as its not marked as frequency
if (typeof note === 'number' && context.type !== 'frequency') { if (typeof note === 'number' && context.type !== 'frequency') {
note = fromMidi(event.value); note = fromMidi(hap.value);
} else if (typeof note === 'string' && !isNote(note)) { } else if (typeof note === 'string' && !isNote(note)) {
throw new Error('not a note: ' + note); throw new Error('not a note: ' + note);
} }
return note; return note;
}; };
export const getFrequency = (event) => { export const getFrequency = (hap) => {
let { value, context } = event; let { value, context } = hap;
// if value is number => interpret as midi number as long as its not marked as frequency // if value is number => interpret as midi number as long as its not marked as frequency
if (typeof value === 'object' && value.freq) { if (typeof value === 'object' && value.freq) {
return value.freq; return value.freq;
} }
if (typeof value === 'number' && context.type !== 'frequency') { if (typeof value === 'number' && context.type !== 'frequency') {
value = fromMidi(event.value); value = fromMidi(hap.value);
} else if (typeof value === 'string' && isNote(value)) { } else if (typeof value === 'string' && isNote(value)) {
value = fromMidi(toMidi(event.value)); value = fromMidi(toMidi(hap.value));
} else if (typeof value !== 'number') { } else if (typeof value !== 'number') {
throw new Error('not a note or frequency:' + value); throw new Error('not a note or frequency:' + value);
} }

View File

@ -39,11 +39,11 @@ Pattern.prototype.midi = function (output, channel = 1) {
}')`, }')`,
); );
} }
return this._withEvent((event) => { return this._withHap((hap) => {
// const onTrigger = (time: number, event: any) => { // const onTrigger = (time: number, hap: any) => {
const onTrigger = (time, event) => { const onTrigger = (time, hap) => {
let note = event.value; let note = hap.value;
const velocity = event.context?.velocity ?? 0.9; const velocity = hap.context?.velocity ?? 0.9;
if (!isNote(note)) { if (!isNote(note)) {
throw new Error('not a note: ' + note); throw new Error('not a note: ' + note);
} }
@ -75,10 +75,10 @@ Pattern.prototype.midi = function (output, channel = 1) {
// await enableWebMidi() // await enableWebMidi()
device.playNote(note, channel, { device.playNote(note, channel, {
time, time,
duration: event.duration.valueOf() * 1000 - 5, duration: hap.duration.valueOf() * 1000 - 5,
velocity, velocity,
}); });
}; };
return event.setContext({ ...event.context, onTrigger }); return hap.setContext({ ...hap.context, onTrigger });
}); });
}; };

View File

@ -12,11 +12,11 @@ comm.open();
const latency = 0.1; const latency = 0.1;
Pattern.prototype.osc = function () { Pattern.prototype.osc = function () {
return this._withEvent((event) => { return this._withHap((hap) => {
const onTrigger = (time, event, currentTime) => { const onTrigger = (time, hap, currentTime) => {
// time should be audio time of onset // time should be audio time of onset
// currentTime should be current time of audio context (slightly before time) // currentTime should be current time of audio context (slightly before time)
const keyvals = Object.entries(event.value).flat(); const keyvals = Object.entries(hap.value).flat();
const offset = (time - currentTime + latency) * 1000; const offset = (time - currentTime + latency) * 1000;
const ts = Math.floor(Date.now() + offset); const ts = Math.floor(Date.now() + offset);
const message = new OSC.Message('/dirt/play', ...keyvals); const message = new OSC.Message('/dirt/play', ...keyvals);
@ -24,6 +24,6 @@ Pattern.prototype.osc = function () {
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60 bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
comm.send(bundle); comm.send(bundle);
}; };
return event.setContext({ ...event.context, onTrigger }); return hap.setContext({ ...hap.context, onTrigger });
}); });
}; };

View File

@ -36,23 +36,23 @@ const latency = 0.1;
// Pattern.prototype.midi = function (output: string | number, channel = 1) { // Pattern.prototype.midi = function (output: string | number, channel = 1) {
Pattern.prototype.serial = async function (...args) { Pattern.prototype.serial = async function (...args) {
return this._withEvent((event) => { return this._withHap((hap) => {
if (!serialWriter) { if (!serialWriter) {
getWriter(...args); getWriter(...args);
} }
const onTrigger = (time, event, currentTime) => { const onTrigger = (time, hap, currentTime) => {
var message = ""; var message = "";
if (typeof event.value === 'object') { if (typeof hap.value === 'object') {
for (const [key, val] of Object.entries(event.value).flat()) { for (const [key, val] of Object.entries(hap.value).flat()) {
message += `${key}:${val};` message += `${key}:${val};`
} }
} }
else { else {
message = event.value; message = hap.value;
} }
const offset = (time - currentTime + latency) * 1000; const offset = (time - currentTime + latency) * 1000;
window.setTimeout(serialWriter, offset, message); window.setTimeout(serialWriter, offset, message);
}; };
return event.setContext({ ...event.context, onTrigger }); return hap.setContext({ ...hap.context, onTrigger });
}); });
}; };

View File

@ -43,17 +43,17 @@ export function scaleTranspose(scale, offset, note) {
// Pattern.prototype._transpose = function (intervalOrSemitones: string | number) { // Pattern.prototype._transpose = function (intervalOrSemitones: string | number) {
Pattern.prototype._transpose = function (intervalOrSemitones) { Pattern.prototype._transpose = function (intervalOrSemitones) {
return this._withEvent((event) => { return this._withHap((hap) => {
const interval = !isNaN(Number(intervalOrSemitones)) const interval = !isNaN(Number(intervalOrSemitones))
? Interval.fromSemitones(intervalOrSemitones /* as number */) ? Interval.fromSemitones(intervalOrSemitones /* as number */)
: String(intervalOrSemitones); : String(intervalOrSemitones);
if (typeof event.value === 'number') { if (typeof hap.value === 'number') {
const semitones = typeof interval === 'string' ? Interval.semitones(interval) || 0 : interval; const semitones = typeof interval === 'string' ? Interval.semitones(interval) || 0 : interval;
return event.withValue(() => event.value + semitones); return hap.withValue(() => hap.value + semitones);
} }
// TODO: move simplify to player to preserve enharmonics // TODO: move simplify to player to preserve enharmonics
// tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3 // tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3
return event.withValue(() => Note.simplify(Note.transpose(event.value, interval))); return hap.withValue(() => Note.simplify(Note.transpose(hap.value, interval)));
}); });
}; };
@ -64,26 +64,26 @@ Pattern.prototype._transpose = function (intervalOrSemitones) {
// or even `stack(c3).superimpose(transpose.slowcat(7, 5))` or // or even `stack(c3).superimpose(transpose.slowcat(7, 5))` or
Pattern.prototype._scaleTranspose = function (offset /* : number | string */) { Pattern.prototype._scaleTranspose = function (offset /* : number | string */) {
return this._withEvent((event) => { return this._withHap((hap) => {
if (!event.context.scale) { if (!hap.context.scale) {
throw new Error('can only use scaleTranspose after .scale'); throw new Error('can only use scaleTranspose after .scale');
} }
if (typeof event.value !== 'string') { if (typeof hap.value !== 'string') {
throw new Error('can only use scaleTranspose with notes'); throw new Error('can only use scaleTranspose with notes');
} }
return event.withValue(() => scaleTranspose(event.context.scale, Number(offset), event.value)); return hap.withValue(() => scaleTranspose(hap.context.scale, Number(offset), hap.value));
}); });
}; };
Pattern.prototype._scale = function (scale /* : string */) { Pattern.prototype._scale = function (scale /* : string */) {
return this._withEvent((event) => { return this._withHap((hap) => {
let note = event.value; let note = hap.value;
const asNumber = Number(note); const asNumber = Number(note);
if (!isNaN(asNumber)) { if (!isNaN(asNumber)) {
let [tonic, scaleName] = Scale.tokenize(scale); let [tonic, scaleName] = Scale.tokenize(scale);
const { pc, oct = 3 } = Note.get(tonic); const { pc, oct = 3 } = Note.get(tonic);
note = scaleTranspose(pc + ' ' + scaleName, asNumber, pc + oct); note = scaleTranspose(pc + ' ' + scaleName, asNumber, pc + oct);
} }
return event.withValue(() => note).setContext({ ...event.context, scale }); return hap.withValue(() => note).setContext({ ...hap.context, scale });
}); });
}; };

View File

@ -52,36 +52,36 @@ export const getDefaultSynth = () => {
// with this function, you can play the pattern with any tone synth // with this function, you can play the pattern with any tone synth
Pattern.prototype.tone = function (instrument) { Pattern.prototype.tone = function (instrument) {
return this._withEvent((event) => { return this._withHap((hap) => {
const onTrigger = (time, event) => { const onTrigger = (time, hap) => {
let note; let note;
let velocity = event.context?.velocity ?? 0.75; let velocity = hap.context?.velocity ?? 0.75;
if (instrument instanceof PluckSynth) { if (instrument instanceof PluckSynth) {
note = getPlayableNoteValue(event); note = getPlayableNoteValue(hap);
instrument.triggerAttack(note, time); instrument.triggerAttack(note, time);
} else if (instrument instanceof NoiseSynth) { } else if (instrument instanceof NoiseSynth) {
instrument.triggerAttackRelease(event.duration.valueOf(), time); // noise has no value instrument.triggerAttackRelease(hap.duration.valueOf(), time); // noise has no value
} else if (instrument instanceof Piano) { } else if (instrument instanceof Piano) {
note = getPlayableNoteValue(event); note = getPlayableNoteValue(hap);
instrument.keyDown({ note, time, velocity }); instrument.keyDown({ note, time, velocity });
instrument.keyUp({ note, time: time + event.duration.valueOf(), velocity }); instrument.keyUp({ note, time: time + hap.duration.valueOf(), velocity });
} else if (instrument instanceof Sampler) { } else if (instrument instanceof Sampler) {
note = getPlayableNoteValue(event); note = getPlayableNoteValue(hap);
instrument.triggerAttackRelease(note, event.duration.valueOf(), time, velocity); instrument.triggerAttackRelease(note, hap.duration.valueOf(), time, velocity);
} else if (instrument instanceof Players) { } else if (instrument instanceof Players) {
if (!instrument.has(event.value)) { if (!instrument.has(hap.value)) {
throw new Error(`name "${event.value}" not defined for players`); throw new Error(`name "${hap.value}" not defined for players`);
} }
const player = instrument.player(event.value); const player = instrument.player(hap.value);
// velocity ? // velocity ?
player.start(time); player.start(time);
player.stop(time + event.duration.valueOf()); player.stop(time + hap.duration.valueOf());
} else { } else {
note = getPlayableNoteValue(event); note = getPlayableNoteValue(hap);
instrument.triggerAttackRelease(note, event.duration.valueOf(), time, velocity); instrument.triggerAttackRelease(note, hap.duration.valueOf(), time, velocity);
} }
}; };
return event.setContext({ ...event.context, instrument, onTrigger }); return hap.setContext({ ...hap.context, instrument, onTrigger });
}); });
}; };

View File

@ -30,10 +30,10 @@ const adsr = (attack, decay, sustain, release, velocity, begin, end) => {
}; };
Pattern.prototype.withAudioNode = function (createAudioNode) { Pattern.prototype.withAudioNode = function (createAudioNode) {
return this._withEvent((event) => { return this._withHap((hap) => {
return event.setContext({ return hap.setContext({
...event.context, ...hap.context,
createAudioNode: (t, e) => createAudioNode(t, e, event.context.createAudioNode?.(t, event)), createAudioNode: (t, e) => createAudioNode(t, e, hap.context.createAudioNode?.(t, hap)),
}); });
}); });
}; };
@ -84,9 +84,9 @@ Pattern.prototype.out = function () {
console.warn('out: no source! call .osc() first'); console.warn('out: no source! call .osc() first');
} }
node?.connect(master); node?.connect(master);
})._withEvent((event) => { })._withHap((hap) => {
const onTrigger = (time, e) => e.context?.createAudioNode?.(time, e); const onTrigger = (time, e) => e.context?.createAudioNode?.(time, e);
return event.setContext({ ...event.context, onTrigger }); return hap.setContext({ ...hap.context, onTrigger });
}); });
}; };

View File

@ -14,8 +14,8 @@ Pattern.prototype._tune = function (scale, tonic = 220) {
} }
tune.loadScale(scale); tune.loadScale(scale);
tune.tonicize(tonic); tune.tonicize(tonic);
return this._asNumber()._withEvent((event) => { return this._asNumber()._withHap((hap) => {
return event.withValue(() => tune.note(event.value)).setContext({ ...event.context, type: 'frequency' }); return hap.withValue(() => tune.note(hap.value)).setContext({ ...hap.context, type: 'frequency' });
}); });
}; };

View File

@ -49,18 +49,18 @@ function xenOffset(xenScale, offset, index = 0) {
// scaleNameOrRatios: string || number[], steps?: number // scaleNameOrRatios: string || number[], steps?: number
Pattern.prototype._xen = function (scaleNameOrRatios, steps) { Pattern.prototype._xen = function (scaleNameOrRatios, steps) {
return this._asNumber()._withEvent((event) => { return this._asNumber()._withHap((hap) => {
const scale = getXenScale(scaleNameOrRatios); const scale = getXenScale(scaleNameOrRatios);
steps = steps || scale.length; steps = steps || scale.length;
const frequency = xenOffset(scale, event.value); const frequency = xenOffset(scale, hap.value);
return event.withValue(() => frequency).setContext({ ...event.context, type: 'frequency' }); return hap.withValue(() => frequency).setContext({ ...hap.context, type: 'frequency' });
}); });
}; };
Pattern.prototype.tuning = function (steps) { Pattern.prototype.tuning = function (steps) {
return this._asNumber()._withEvent((event) => { return this._asNumber()._withHap((hap) => {
const frequency = xenOffset(steps, event.value); const frequency = xenOffset(steps, hap.value);
return event.withValue(() => frequency).setContext({ ...event.context, type: 'frequency' }); return hap.withValue(() => frequency).setContext({ ...hap.context, type: 'frequency' });
}); });
}; };
Pattern.prototype.define('xen', (scale, pat) => pat.xen(scale), { composable: true, patternified: true }); Pattern.prototype.define('xen', (scale, pat) => pat.xen(scale), { composable: true, patternified: true });

23
repl/etc/agpl-header.txt Normal file
View File

@ -0,0 +1,23 @@
/*
Strudel - javascript-based environment for live coding algorithmic (musical) patterns
https://strudel.tidalcycles.org / https://github.com/tidalcycles/strudel/
Copyright (C) Strudel contributors
https://github.com/tidalcycles/strudel/graphs/contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

View File

@ -22,12 +22,13 @@
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"build": "BUILD_PATH='../docs' react-scripts build && npm run build-tutorial", "build": "BUILD_PATH='../docs' react-scripts build && npm run build-tutorial && npm add-license",
"test": "mocha ./src/test --colors", "test": "mocha ./src/test --colors",
"snapshot": "cd ./src/ && rm -f ./tunes.snapshot.mjs && node ./shoot.mjs > ./tunes.snapshot.mjs", "snapshot": "cd ./src/ && rm -f ./tunes.snapshot.mjs && node ./shoot.mjs > ./tunes.snapshot.mjs",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"tutorial": "parcel src/tutorial/index.html --no-cache", "tutorial": "parcel src/tutorial/index.html --no-cache",
"build-tutorial": "rm -rf ../docs/tutorial && parcel build src/tutorial/index.html --dist-dir ../docs/tutorial --public-url /tutorial --no-scope-hoist --no-cache", "build-tutorial": "rm -rf ../docs/tutorial && parcel build src/tutorial/index.html --dist-dir ../docs/tutorial --public-url /tutorial --no-scope-hoist --no-cache",
"add-license": "cat etc/agpl-header.txt ../docs/static/js/*LICENSE.txt > /tmp/strudel-license.txt && cp /tmp/strudel-license.txt ../docs/static/js/*LICENSE.txt",
"predeploy": "npm run build", "predeploy": "npm run build",
"deploy": "gh-pages -d ../docs", "deploy": "gh-pages -d ../docs",
"static": "npx serve ../docs" "static": "npx serve ../docs"