mirror of
https://github.com/eliasstepanik/strudel.git
synced 2026-01-11 21:58:37 +00:00
commit
14487ed5cf
1
.gitignore
vendored
1
.gitignore
vendored
@ -27,4 +27,5 @@ node_modules/
|
||||
repl-parcel
|
||||
mytunes.ts
|
||||
doc
|
||||
out
|
||||
.parcel-cache
|
||||
11
jsdoc.config.json
Normal file
11
jsdoc.config.json
Normal 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
1175
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,9 @@
|
||||
"setup": "npm i && npm run bootstrap && cd repl && npm i",
|
||||
"repl": "cd repl && npm run start",
|
||||
"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": [
|
||||
"packages/*"
|
||||
@ -33,6 +35,9 @@
|
||||
"homepage": "https://strudel.tidalcycles.org",
|
||||
"devDependencies": {
|
||||
"events": "^3.3.0",
|
||||
"jsdoc": "^3.6.10",
|
||||
"jsdoc-json": "^2.0.2",
|
||||
"jsdoc-to-markdown": "^7.1.1",
|
||||
"lerna": "^4.0.0",
|
||||
"mocha": "^9.1.4"
|
||||
}
|
||||
|
||||
@ -14,7 +14,10 @@ export class Hap {
|
||||
then the whole will be returned as None, in which case the given
|
||||
value will have been sampled from the point halfway between the
|
||||
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) {
|
||||
@ -37,18 +40,18 @@ export class Hap {
|
||||
}
|
||||
|
||||
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;
|
||||
return new Hap(whole, func(this.part), this.value, this.context);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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."""
|
||||
return this.whole != undefined && this.whole.begin.equals(this.part.begin);
|
||||
}
|
||||
|
||||
@ -13,20 +13,37 @@ import { unionWithObj } from './value.mjs';
|
||||
import { isNote, toMidi, compose, removeUndefineds, flatten, id, listRange, curry, mod } from './util.mjs';
|
||||
import drawLine from './drawLine.mjs';
|
||||
|
||||
/** @class Class representing a 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) {
|
||||
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) {
|
||||
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() {
|
||||
// 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 q = (state) => {
|
||||
return flatten(state.span.spanCycles.map((subspan) => pat.query(state.setSpan(subspan))));
|
||||
@ -34,48 +51,98 @@ export class Pattern {
|
||||
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) {
|
||||
return new Pattern((state) => this.query(state.withSpan(func)));
|
||||
}
|
||||
|
||||
withQueryTime(func) {
|
||||
// Returns a new pattern, with the function applied to both the begin
|
||||
// and end of the the query timespan
|
||||
/**
|
||||
* As with {@link Pattern#withQuerySpan|withQuerySpan}, but the function is applied to both the
|
||||
* 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))));
|
||||
}
|
||||
|
||||
withEventSpan(func) {
|
||||
// Returns a new pattern, with the function applied to each event
|
||||
// timespan.
|
||||
/**
|
||||
* Similar to {@link Pattern#withQuerySpan|withQuerySpan}, but the function is applied to the timespans
|
||||
* 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)));
|
||||
}
|
||||
|
||||
withEventTime(func) {
|
||||
// Returns a new pattern, with the function applied to both the begin
|
||||
// and end of each event timespan.
|
||||
return this.withEventSpan((span) => span.withTime(func));
|
||||
/**
|
||||
* As with {@link Pattern#withHapSpan|withHapSpan}, but the function is applied to both the
|
||||
* begin and end time of the hap timespans.
|
||||
* @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)));
|
||||
}
|
||||
|
||||
_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) {
|
||||
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) {
|
||||
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() {
|
||||
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) {
|
||||
const location = {
|
||||
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) {
|
||||
// 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)));
|
||||
}
|
||||
|
||||
// alias
|
||||
fmap(func) {
|
||||
/**
|
||||
* see {@link Pattern#withValue|withValue}
|
||||
*/
|
||||
fmap(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) {
|
||||
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() {
|
||||
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'
|
||||
// timespan, i.e. the events that include their 'onset'.
|
||||
return this._filterEvents((hap) => hap.hasOnset());
|
||||
// timespan, i.e. the haps that include their 'onset'.
|
||||
return this._filterHaps((hap) => hap.hasOnset());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new pattern, with 'continuous' haps (those without 'whole'
|
||||
* timespans) removed from query results.
|
||||
* @returns Pattern
|
||||
*/
|
||||
discreteOnly() {
|
||||
// removes continuous events that don't have a 'whole' timespan
|
||||
return this._filterEvents((hap) => hap.whole);
|
||||
// removes continuous haps that don't have a 'whole' timespan
|
||||
return this._filterHaps((hap) => hap.whole);
|
||||
}
|
||||
|
||||
_appWhole(whole_func, pat_val) {
|
||||
@ -154,27 +254,38 @@ export class Pattern {
|
||||
// pattern of functions.
|
||||
const pat_func = this;
|
||||
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);
|
||||
const hap_funcs = pat_func.query(state);
|
||||
const hap_vals = pat_val.query(state);
|
||||
const apply = function (hap_func, hap_val) {
|
||||
const s = hap_func.part.intersection(hap_val.part);
|
||||
if (s == undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return new Hap(
|
||||
whole_func(event_func.whole, event_val.whole),
|
||||
whole_func(hap_func.whole, hap_val.whole),
|
||||
s,
|
||||
event_func.value(event_val.value),
|
||||
event_val.combineContext(event_func),
|
||||
hap_func.value(hap_val.value),
|
||||
hap_val.combineContext(hap_func),
|
||||
);
|
||||
};
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
// Tidal's <*>
|
||||
const whole_func = function (span_a, span_b) {
|
||||
@ -186,14 +297,23 @@ export class Pattern {
|
||||
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) {
|
||||
const pat_func = this;
|
||||
|
||||
const query = function (state) {
|
||||
const haps = [];
|
||||
for (const hap_func of pat_func.query(state)) {
|
||||
const event_vals = pat_val.query(state.setSpan(hap_func.wholeOrPart()));
|
||||
for (const hap_val of event_vals) {
|
||||
const hap_vals = pat_val.query(state.setSpan(hap_func.wholeOrPart()));
|
||||
for (const hap_val of hap_vals) {
|
||||
const new_whole = hap_func.whole;
|
||||
const new_part = hap_func.part.intersection(hap_val.part);
|
||||
if (new_part) {
|
||||
@ -209,6 +329,13 @@ export class Pattern {
|
||||
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) {
|
||||
const pat_func = this;
|
||||
|
||||
@ -232,6 +359,13 @@ export class Pattern {
|
||||
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) {
|
||||
var self = this;
|
||||
if (!with_context) {
|
||||
@ -240,18 +374,30 @@ export class Pattern {
|
||||
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() {
|
||||
return this.firstCycle().map((hap) => hap.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* More human-readable version of the {@link Pattern#_firstCycleValues} accessor.
|
||||
*/
|
||||
get _showFirstCycle() {
|
||||
return this.firstCycle().map(
|
||||
(hap) => `${hap.value}: ${hap.whole.begin.toFraction()} - ${hap.whole.end.toFraction()}`,
|
||||
);
|
||||
}
|
||||
|
||||
_sortEventsByPart() {
|
||||
return this._withEvents((events) =>
|
||||
events.sort((a, b) =>
|
||||
/**
|
||||
* Returns a new pattern, which returns haps sorted in temporal order. Mainly
|
||||
* of use when comparing two patterns for equality, in tests.
|
||||
* @returns Pattern
|
||||
*/
|
||||
_sortHapsByPart() {
|
||||
return this._withHaps((haps) =>
|
||||
haps.sort((a, b) =>
|
||||
a.part.begin
|
||||
.sub(b.part.begin)
|
||||
.or(a.part.end.sub(b.part.end))
|
||||
@ -288,21 +434,21 @@ export class Pattern {
|
||||
}
|
||||
|
||||
_asNumber(dropfails = false, softfail = false) {
|
||||
return this._withEvent((event) => {
|
||||
const asNumber = Number(event.value);
|
||||
return this._withHap((hap) => {
|
||||
const asNumber = Number(hap.value);
|
||||
if (!isNaN(asNumber)) {
|
||||
return event.withValue(() => asNumber);
|
||||
return hap.withValue(() => asNumber);
|
||||
}
|
||||
const specialValue = {
|
||||
e: Math.E,
|
||||
pi: Math.PI,
|
||||
}[event.value];
|
||||
}[hap.value];
|
||||
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
|
||||
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) {
|
||||
// return 'nothing'
|
||||
@ -310,10 +456,10 @@ export class Pattern {
|
||||
}
|
||||
if (softfail) {
|
||||
// return original hap
|
||||
return event;
|
||||
return hap;
|
||||
}
|
||||
throw new Error('cannot parse as number: "' + event.value + '"');
|
||||
return event;
|
||||
throw new Error('cannot parse as number: "' + hap.value + '"');
|
||||
return hap;
|
||||
});
|
||||
if (dropfail) {
|
||||
return result._removeUndefineds();
|
||||
@ -389,7 +535,7 @@ export class Pattern {
|
||||
|
||||
join() {
|
||||
// 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);
|
||||
}
|
||||
|
||||
@ -399,7 +545,7 @@ export class Pattern {
|
||||
|
||||
outerJoin() {
|
||||
// Flattens a pattern of patterns into a pattern, where wholes are
|
||||
// taken from inner events.
|
||||
// taken from inner haps.
|
||||
return this.outerBind(id);
|
||||
}
|
||||
|
||||
@ -409,35 +555,35 @@ export class Pattern {
|
||||
|
||||
innerJoin() {
|
||||
// Flattens a pattern of patterns into a pattern, where wholes are
|
||||
// taken from inner events.
|
||||
// taken from inner haps.
|
||||
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) {
|
||||
const pat_of_pats = this;
|
||||
return new Pattern((state) => {
|
||||
return (
|
||||
pat_of_pats
|
||||
// drop continuous events from the outer pattern.
|
||||
// drop continuous haps from the outer pattern.
|
||||
.discreteOnly()
|
||||
.query(state)
|
||||
.map((outer_hap) => {
|
||||
return (
|
||||
outer_hap.value
|
||||
// trig = align the inner pattern cycle start to outer pattern events
|
||||
// Trigzero = align the inner pattern cycle zero 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 haps
|
||||
.late(cycleZero ? outer_hap.whole.begin : outer_hap.whole.begin.cyclePos())
|
||||
.query(state)
|
||||
.map((inner_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.part.intersection(outer_hap.part),
|
||||
inner_hap.value,
|
||||
).setContext(outer_hap.combineContext(inner_hap)),
|
||||
)
|
||||
// Drop events that didn't intersect
|
||||
// Drop haps that didn't intersect
|
||||
.filter((hap) => hap.part)
|
||||
);
|
||||
})
|
||||
@ -527,7 +673,7 @@ export class Pattern {
|
||||
const end = cycle.add(span.end.sub(cycle).div(factor).min(1));
|
||||
return new TimeSpan(begin, end);
|
||||
};
|
||||
return this.withQuerySpan(qf).withEventSpan(ef)._splitQueries();
|
||||
return this.withQuerySpan(qf).withHapSpan(ef)._splitQueries();
|
||||
}
|
||||
|
||||
_compress(b, e) {
|
||||
@ -543,7 +689,7 @@ export class Pattern {
|
||||
|
||||
_fast(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) {
|
||||
@ -578,7 +724,7 @@ export class Pattern {
|
||||
_early(offset) {
|
||||
// Equivalent of Tidal's <~ operator
|
||||
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) {
|
||||
@ -592,7 +738,7 @@ export class Pattern {
|
||||
s = Fraction(s);
|
||||
const d = e.sub(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();
|
||||
}
|
||||
|
||||
@ -632,7 +778,7 @@ export class Pattern {
|
||||
}
|
||||
|
||||
log() {
|
||||
return this._withEvent((e) => {
|
||||
return this._withHap((e) => {
|
||||
return e.setContext({ ...e.context, logs: (e.context?.logs || []).concat([e.show()]) });
|
||||
});
|
||||
}
|
||||
@ -800,14 +946,14 @@ export class Pattern {
|
||||
return silence;
|
||||
}
|
||||
|
||||
// sets absolute duration of events
|
||||
// sets absolute duration of haps
|
||||
_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) {
|
||||
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) {
|
||||
@ -957,8 +1103,14 @@ Pattern.prototype.factories = {
|
||||
// Nothing
|
||||
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) {
|
||||
// A discrete value that repeats once per cycle
|
||||
function query(state) {
|
||||
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);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Array test here is to avoid infinite recursions..
|
||||
pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat)));
|
||||
@ -987,10 +1144,17 @@ export function stack(...pats) {
|
||||
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) {
|
||||
// Concatenation: combines a list of patterns, switching between them
|
||||
// successively, one per cycle.
|
||||
|
||||
// Array test here is to avoid infinite recursions..
|
||||
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
|
||||
// be the second (rather than fourth) cycle from the first pattern.
|
||||
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();
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
// Concatenation: combines a list of patterns, switching between them
|
||||
// successively, one per cycle. Unlike slowcat, this version will skip cycles.
|
||||
pats = pats.map(reify);
|
||||
const query = function (state) {
|
||||
const pat_n = Math.floor(state.span.begin) % pats.length;
|
||||
@ -1023,18 +1189,33 @@ export function slowcatPrime(...pats) {
|
||||
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) {
|
||||
// Concatenation: as with slowcat, but squashes a cycle from each
|
||||
// pattern into one cycle
|
||||
return slowcat(...pats)._fast(pats.length);
|
||||
}
|
||||
|
||||
/** See {@link slowcat} */
|
||||
export function cat(...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) {
|
||||
// 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));
|
||||
let begin = Fraction(0);
|
||||
const pats = [];
|
||||
@ -1046,11 +1227,12 @@ export function timeCat(...timepats) {
|
||||
return stack(...pats);
|
||||
}
|
||||
|
||||
/** See {@link fastcat} */
|
||||
export function sequence(...pats) {
|
||||
return fastcat(...pats);
|
||||
}
|
||||
|
||||
// shorthand for sequence
|
||||
/** See {@link fastcat} */
|
||||
export function seq(...pats) {
|
||||
return fastcat(...pats);
|
||||
}
|
||||
|
||||
@ -26,11 +26,11 @@ function speak(words, lang, voice) {
|
||||
}
|
||||
|
||||
Pattern.prototype._speak = function (lang, voice) {
|
||||
return this._withEvent((event) => {
|
||||
const onTrigger = (time, event) => {
|
||||
speak(event.value, lang, voice);
|
||||
return this._withHap((hap) => {
|
||||
const onTrigger = (time, hap) => {
|
||||
speak(hap.value, lang, voice);
|
||||
};
|
||||
return event.setContext({ ...event.context, onTrigger });
|
||||
return hap.setContext({ ...hap.context, onTrigger });
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -57,7 +57,7 @@ const third = Fraction(1, 3);
|
||||
const twothirds = Fraction(2, 3);
|
||||
|
||||
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 () {
|
||||
@ -320,7 +320,7 @@ describe('Pattern', function () {
|
||||
});
|
||||
});
|
||||
describe('setSqueeze()', () => {
|
||||
it('Can squeeze one pattern inside the events of another', () => {
|
||||
it('Can squeeze one pattern inside the haps of another', () => {
|
||||
sameFirst(
|
||||
sequence(1, [2, 3]).setSqueeze(sequence('a', 'b', 'c')),
|
||||
sequence(
|
||||
@ -394,7 +394,7 @@ describe('Pattern', function () {
|
||||
});
|
||||
it('Makes things faster, with a pattern of factors', function () {
|
||||
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(
|
||||
pure('a').fast(sequence(1, 4)).firstCycle(),
|
||||
stack(pure('a').fast(sequence(1, silence)), sequence(silence, ['a', 'a'])).firstCycle(),
|
||||
@ -468,7 +468,7 @@ describe('Pattern', function () {
|
||||
});
|
||||
it('Can alternate', function () {
|
||||
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(),
|
||||
);
|
||||
});
|
||||
@ -679,7 +679,7 @@ describe('Pattern', function () {
|
||||
});
|
||||
});
|
||||
describe('_setContext()', () => {
|
||||
it('Can set the event context', () => {
|
||||
it('Can set the hap context', () => {
|
||||
assert.deepStrictEqual(
|
||||
pure('a')
|
||||
._setContext([
|
||||
@ -701,7 +701,7 @@ describe('Pattern', function () {
|
||||
});
|
||||
});
|
||||
describe('_withContext()', () => {
|
||||
it('Can update the event context', () => {
|
||||
it('Can update the hap context', () => {
|
||||
assert.deepStrictEqual(
|
||||
pure('a')
|
||||
._setContext([
|
||||
@ -756,13 +756,13 @@ describe('Pattern', function () {
|
||||
});
|
||||
});
|
||||
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)), [
|
||||
hap(ts(1 / 4, 5 / 4), ts(1, 5 / 4), 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)), [
|
||||
hap(ts(-3 / 4, 1 / 4), ts(0, 1 / 4), 30),
|
||||
hap(ts(1 / 4, 5 / 4), ts(1 / 4, 1), 30),
|
||||
@ -780,9 +780,9 @@ describe('Pattern', function () {
|
||||
describe('jux', () => {
|
||||
it('Can juxtapose', () => {
|
||||
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))
|
||||
._sortEventsByPart()
|
||||
._sortHapsByPart()
|
||||
.firstCycle(),
|
||||
);
|
||||
});
|
||||
@ -790,9 +790,9 @@ describe('Pattern', function () {
|
||||
describe('juxBy', () => {
|
||||
it('Can juxtapose by half', () => {
|
||||
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))
|
||||
._sortEventsByPart()
|
||||
._sortHapsByPart()
|
||||
.firstCycle(),
|
||||
);
|
||||
});
|
||||
@ -821,7 +821,7 @@ describe('Pattern', function () {
|
||||
sequence(pure('a').fast(3), [pure('b').fast(3), pure('c').fast(3)]).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('Doesnt drop events in the 9th cycle', () => {
|
||||
it('Doesnt drop haps in the 9th cycle', () => {
|
||||
// fixed with https://github.com/tidalcycles/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a
|
||||
assert.equal(sequence(1, 2, 3).ply(2).early(8).firstCycle().length, 6);
|
||||
});
|
||||
@ -848,7 +848,7 @@ describe('Pattern', function () {
|
||||
});
|
||||
it('Can chop(2,3)', () => {
|
||||
assert.deepStrictEqual(
|
||||
pure({ sound: 'a' }).fast(2).chop(2, 3)._sortEventsByPart().firstCycle(),
|
||||
pure({ sound: 'a' }).fast(2).chop(2, 3)._sortHapsByPart().firstCycle(),
|
||||
sequence(
|
||||
[
|
||||
{ sound: 'a', begin: 0, end: 0.5 },
|
||||
@ -860,7 +860,7 @@ describe('Pattern', function () {
|
||||
{ sound: 'a', begin: 2 / 3, end: 1 },
|
||||
],
|
||||
)
|
||||
._sortEventsByPart()
|
||||
._sortHapsByPart()
|
||||
.firstCycle(),
|
||||
);
|
||||
});
|
||||
|
||||
@ -35,27 +35,27 @@ export const fromMidi = (n) => {
|
||||
// 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 getPlayableNoteValue = (event) => {
|
||||
let { value: note, context } = event;
|
||||
export const getPlayableNoteValue = (hap) => {
|
||||
let { value: note, context } = hap;
|
||||
// if value is number => interpret as midi number as long as its not marked as frequency
|
||||
if (typeof note === 'number' && context.type !== 'frequency') {
|
||||
note = fromMidi(event.value);
|
||||
note = fromMidi(hap.value);
|
||||
} else if (typeof note === 'string' && !isNote(note)) {
|
||||
throw new Error('not a note: ' + note);
|
||||
}
|
||||
return note;
|
||||
};
|
||||
|
||||
export const getFrequency = (event) => {
|
||||
let { value, context } = event;
|
||||
export const getFrequency = (hap) => {
|
||||
let { value, context } = hap;
|
||||
// if value is number => interpret as midi number as long as its not marked as frequency
|
||||
if (typeof value === 'object' && value.freq) {
|
||||
return value.freq;
|
||||
}
|
||||
if (typeof value === 'number' && context.type !== 'frequency') {
|
||||
value = fromMidi(event.value);
|
||||
value = fromMidi(hap.value);
|
||||
} else if (typeof value === 'string' && isNote(value)) {
|
||||
value = fromMidi(toMidi(event.value));
|
||||
value = fromMidi(toMidi(hap.value));
|
||||
} else if (typeof value !== 'number') {
|
||||
throw new Error('not a note or frequency:' + value);
|
||||
}
|
||||
|
||||
@ -39,11 +39,11 @@ Pattern.prototype.midi = function (output, channel = 1) {
|
||||
}')`,
|
||||
);
|
||||
}
|
||||
return this._withEvent((event) => {
|
||||
// const onTrigger = (time: number, event: any) => {
|
||||
const onTrigger = (time, event) => {
|
||||
let note = event.value;
|
||||
const velocity = event.context?.velocity ?? 0.9;
|
||||
return this._withHap((hap) => {
|
||||
// const onTrigger = (time: number, hap: any) => {
|
||||
const onTrigger = (time, hap) => {
|
||||
let note = hap.value;
|
||||
const velocity = hap.context?.velocity ?? 0.9;
|
||||
if (!isNote(note)) {
|
||||
throw new Error('not a note: ' + note);
|
||||
}
|
||||
@ -75,10 +75,10 @@ Pattern.prototype.midi = function (output, channel = 1) {
|
||||
// await enableWebMidi()
|
||||
device.playNote(note, channel, {
|
||||
time,
|
||||
duration: event.duration.valueOf() * 1000 - 5,
|
||||
duration: hap.duration.valueOf() * 1000 - 5,
|
||||
velocity,
|
||||
});
|
||||
};
|
||||
return event.setContext({ ...event.context, onTrigger });
|
||||
return hap.setContext({ ...hap.context, onTrigger });
|
||||
});
|
||||
};
|
||||
|
||||
@ -12,11 +12,11 @@ comm.open();
|
||||
const latency = 0.1;
|
||||
|
||||
Pattern.prototype.osc = function () {
|
||||
return this._withEvent((event) => {
|
||||
const onTrigger = (time, event, currentTime) => {
|
||||
return this._withHap((hap) => {
|
||||
const onTrigger = (time, hap, currentTime) => {
|
||||
// time should be audio time of onset
|
||||
// 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 ts = Math.floor(Date.now() + offset);
|
||||
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
|
||||
comm.send(bundle);
|
||||
};
|
||||
return event.setContext({ ...event.context, onTrigger });
|
||||
return hap.setContext({ ...hap.context, onTrigger });
|
||||
});
|
||||
};
|
||||
|
||||
@ -36,23 +36,23 @@ const latency = 0.1;
|
||||
|
||||
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
|
||||
Pattern.prototype.serial = async function (...args) {
|
||||
return this._withEvent((event) => {
|
||||
return this._withHap((hap) => {
|
||||
if (!serialWriter) {
|
||||
getWriter(...args);
|
||||
}
|
||||
const onTrigger = (time, event, currentTime) => {
|
||||
const onTrigger = (time, hap, currentTime) => {
|
||||
var message = "";
|
||||
if (typeof event.value === 'object') {
|
||||
for (const [key, val] of Object.entries(event.value).flat()) {
|
||||
if (typeof hap.value === 'object') {
|
||||
for (const [key, val] of Object.entries(hap.value).flat()) {
|
||||
message += `${key}:${val};`
|
||||
}
|
||||
}
|
||||
else {
|
||||
message = event.value;
|
||||
message = hap.value;
|
||||
}
|
||||
const offset = (time - currentTime + latency) * 1000;
|
||||
window.setTimeout(serialWriter, offset, message);
|
||||
};
|
||||
return event.setContext({ ...event.context, onTrigger });
|
||||
return hap.setContext({ ...hap.context, onTrigger });
|
||||
});
|
||||
};
|
||||
|
||||
@ -43,17 +43,17 @@ export function scaleTranspose(scale, offset, note) {
|
||||
|
||||
// Pattern.prototype._transpose = function (intervalOrSemitones: string | number) {
|
||||
Pattern.prototype._transpose = function (intervalOrSemitones) {
|
||||
return this._withEvent((event) => {
|
||||
return this._withHap((hap) => {
|
||||
const interval = !isNaN(Number(intervalOrSemitones))
|
||||
? Interval.fromSemitones(intervalOrSemitones /* as number */)
|
||||
: String(intervalOrSemitones);
|
||||
if (typeof event.value === 'number') {
|
||||
if (typeof hap.value === 'number') {
|
||||
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
|
||||
// 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
|
||||
|
||||
Pattern.prototype._scaleTranspose = function (offset /* : number | string */) {
|
||||
return this._withEvent((event) => {
|
||||
if (!event.context.scale) {
|
||||
return this._withHap((hap) => {
|
||||
if (!hap.context.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');
|
||||
}
|
||||
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 */) {
|
||||
return this._withEvent((event) => {
|
||||
let note = event.value;
|
||||
return this._withHap((hap) => {
|
||||
let note = hap.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 event.withValue(() => note).setContext({ ...event.context, scale });
|
||||
return hap.withValue(() => note).setContext({ ...hap.context, scale });
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -52,36 +52,36 @@ export const getDefaultSynth = () => {
|
||||
|
||||
// with this function, you can play the pattern with any tone synth
|
||||
Pattern.prototype.tone = function (instrument) {
|
||||
return this._withEvent((event) => {
|
||||
const onTrigger = (time, event) => {
|
||||
return this._withHap((hap) => {
|
||||
const onTrigger = (time, hap) => {
|
||||
let note;
|
||||
let velocity = event.context?.velocity ?? 0.75;
|
||||
let velocity = hap.context?.velocity ?? 0.75;
|
||||
if (instrument instanceof PluckSynth) {
|
||||
note = getPlayableNoteValue(event);
|
||||
note = getPlayableNoteValue(hap);
|
||||
instrument.triggerAttack(note, time);
|
||||
} 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) {
|
||||
note = getPlayableNoteValue(event);
|
||||
note = getPlayableNoteValue(hap);
|
||||
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) {
|
||||
note = getPlayableNoteValue(event);
|
||||
instrument.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
|
||||
note = getPlayableNoteValue(hap);
|
||||
instrument.triggerAttackRelease(note, hap.duration.valueOf(), time, velocity);
|
||||
} else if (instrument instanceof Players) {
|
||||
if (!instrument.has(event.value)) {
|
||||
throw new Error(`name "${event.value}" not defined for players`);
|
||||
if (!instrument.has(hap.value)) {
|
||||
throw new Error(`name "${hap.value}" not defined for players`);
|
||||
}
|
||||
const player = instrument.player(event.value);
|
||||
const player = instrument.player(hap.value);
|
||||
// velocity ?
|
||||
player.start(time);
|
||||
player.stop(time + event.duration.valueOf());
|
||||
player.stop(time + hap.duration.valueOf());
|
||||
} else {
|
||||
note = getPlayableNoteValue(event);
|
||||
instrument.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
|
||||
note = getPlayableNoteValue(hap);
|
||||
instrument.triggerAttackRelease(note, hap.duration.valueOf(), time, velocity);
|
||||
}
|
||||
};
|
||||
return event.setContext({ ...event.context, instrument, onTrigger });
|
||||
return hap.setContext({ ...hap.context, instrument, onTrigger });
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -30,10 +30,10 @@ const adsr = (attack, decay, sustain, release, velocity, begin, end) => {
|
||||
};
|
||||
|
||||
Pattern.prototype.withAudioNode = function (createAudioNode) {
|
||||
return this._withEvent((event) => {
|
||||
return event.setContext({
|
||||
...event.context,
|
||||
createAudioNode: (t, e) => createAudioNode(t, e, event.context.createAudioNode?.(t, event)),
|
||||
return this._withHap((hap) => {
|
||||
return hap.setContext({
|
||||
...hap.context,
|
||||
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');
|
||||
}
|
||||
node?.connect(master);
|
||||
})._withEvent((event) => {
|
||||
})._withHap((hap) => {
|
||||
const onTrigger = (time, e) => e.context?.createAudioNode?.(time, e);
|
||||
return event.setContext({ ...event.context, onTrigger });
|
||||
return hap.setContext({ ...hap.context, onTrigger });
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -14,8 +14,8 @@ Pattern.prototype._tune = function (scale, tonic = 220) {
|
||||
}
|
||||
tune.loadScale(scale);
|
||||
tune.tonicize(tonic);
|
||||
return this._asNumber()._withEvent((event) => {
|
||||
return event.withValue(() => tune.note(event.value)).setContext({ ...event.context, type: 'frequency' });
|
||||
return this._asNumber()._withHap((hap) => {
|
||||
return hap.withValue(() => tune.note(hap.value)).setContext({ ...hap.context, type: 'frequency' });
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -49,18 +49,18 @@ function xenOffset(xenScale, offset, index = 0) {
|
||||
|
||||
// scaleNameOrRatios: string || number[], steps?: number
|
||||
Pattern.prototype._xen = function (scaleNameOrRatios, steps) {
|
||||
return this._asNumber()._withEvent((event) => {
|
||||
return this._asNumber()._withHap((hap) => {
|
||||
const scale = getXenScale(scaleNameOrRatios);
|
||||
steps = steps || scale.length;
|
||||
const frequency = xenOffset(scale, event.value);
|
||||
return event.withValue(() => frequency).setContext({ ...event.context, type: 'frequency' });
|
||||
const frequency = xenOffset(scale, hap.value);
|
||||
return hap.withValue(() => frequency).setContext({ ...hap.context, type: 'frequency' });
|
||||
});
|
||||
};
|
||||
|
||||
Pattern.prototype.tuning = function (steps) {
|
||||
return this._asNumber()._withEvent((event) => {
|
||||
const frequency = xenOffset(steps, event.value);
|
||||
return event.withValue(() => frequency).setContext({ ...event.context, type: 'frequency' });
|
||||
return this._asNumber()._withHap((hap) => {
|
||||
const frequency = xenOffset(steps, hap.value);
|
||||
return hap.withValue(() => frequency).setContext({ ...hap.context, type: 'frequency' });
|
||||
});
|
||||
};
|
||||
Pattern.prototype.define('xen', (scale, pat) => pat.xen(scale), { composable: true, patternified: true });
|
||||
|
||||
23
repl/etc/agpl-header.txt
Normal file
23
repl/etc/agpl-header.txt
Normal 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/>.
|
||||
|
||||
*/
|
||||
|
||||
@ -22,12 +22,13 @@
|
||||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"snapshot": "cd ./src/ && rm -f ./tunes.snapshot.mjs && node ./shoot.mjs > ./tunes.snapshot.mjs",
|
||||
"eject": "react-scripts eject",
|
||||
"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",
|
||||
"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",
|
||||
"deploy": "gh-pages -d ../docs",
|
||||
"static": "npx serve ../docs"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user