mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-13 14:48:36 +00:00
support object arithmetic
This commit is contained in:
parent
24328ee8ae
commit
e128b02da1
@ -10,7 +10,7 @@ import Hap from './hap.mjs';
|
||||
import State from './state.mjs';
|
||||
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, numeralArgs } from './util.mjs';
|
||||
import drawLine from './drawLine.mjs';
|
||||
|
||||
/** @class Class representing a pattern. */
|
||||
@ -68,7 +68,7 @@ export class Pattern {
|
||||
return new Pattern((state) => {
|
||||
const newState = state.withSpan(func);
|
||||
if (!newState.span) {
|
||||
return [];
|
||||
return [];
|
||||
}
|
||||
return pat.query(newState);
|
||||
});
|
||||
@ -446,8 +446,13 @@ export class Pattern {
|
||||
return otherPat.fmap((b) => this.fmap((a) => func(a)(b)))._TrigzeroJoin();
|
||||
}
|
||||
|
||||
// TODO: refactor to parseNumber / withNumberArgs (see util)
|
||||
_asNumber(dropfails = false, softfail = false) {
|
||||
return this._withHap((hap) => {
|
||||
// leave objects alone... this is needed to be able to do pat.add(n(2))
|
||||
if (!['number', 'string'].includes(typeof hap.value)) {
|
||||
return hap;
|
||||
}
|
||||
const asNumber = Number(hap.value);
|
||||
if (!isNaN(asNumber)) {
|
||||
return hap.withValue(() => asNumber);
|
||||
@ -761,7 +766,7 @@ export class Pattern {
|
||||
const bpos = span.begin.sub(cycle).mul(factor).min(1);
|
||||
const epos = span.end.sub(cycle).mul(factor).min(1);
|
||||
if (bpos >= 1) {
|
||||
return undefined;
|
||||
return undefined;
|
||||
}
|
||||
return new TimeSpan(cycle.add(bpos), cycle.add(epos));
|
||||
};
|
||||
@ -783,7 +788,7 @@ export class Pattern {
|
||||
};
|
||||
return this.withQuerySpanMaybe(qf)._withHap(ef)._splitQueries();
|
||||
}
|
||||
|
||||
|
||||
// Compress each cycle into the given timespan, leaving a gap
|
||||
_compress(b, e) {
|
||||
if (b.gt(e) || b.gt(1) || e.gt(1) || b.lt(0) || e.lt(0)) {
|
||||
@ -1387,7 +1392,7 @@ function _composeOp(a, b, func) {
|
||||
// Make composers
|
||||
(function () {
|
||||
const num = (pat) => pat._asNumber();
|
||||
const numOrString = (pat) => pat._asNumber(false, true);
|
||||
// const numOrString = (pat) => pat._asNumber(false, true); // was used for add
|
||||
|
||||
// pattern composers
|
||||
const composers = {
|
||||
@ -1412,7 +1417,7 @@ function _composeOp(a, b, func) {
|
||||
* // Behind the scenes, the notes are converted to midi numbers:
|
||||
* // "48 52 55".add("<0 5 7 0>").note()
|
||||
*/
|
||||
add: [(a, b) => a + b, numOrString], // support string concatenation
|
||||
add: [numeralArgs((a, b) => a + b), num], // support string concatenation
|
||||
/**
|
||||
*
|
||||
* Like add, but the given numbers are subtracted.
|
||||
@ -1422,7 +1427,7 @@ function _composeOp(a, b, func) {
|
||||
* "0 2 4".sub("<0 1 2 3>").scale('C4 minor').note()
|
||||
* // See add for more information.
|
||||
*/
|
||||
sub: [(a, b) => a - b, num],
|
||||
sub: [numeralArgs((a, b) => a - b), num],
|
||||
/**
|
||||
*
|
||||
* Multiplies each number by the given factor.
|
||||
@ -1431,14 +1436,14 @@ function _composeOp(a, b, func) {
|
||||
* @example
|
||||
* "1 1.5 [1.66, <2 2.33>]".mul(150).freq()
|
||||
*/
|
||||
mul: [(a, b) => a * b, num],
|
||||
mul: [numeralArgs((a, b) => a * b), num],
|
||||
/**
|
||||
*
|
||||
* Divides each number by the given factor.
|
||||
* @name div
|
||||
* @memberof Pattern
|
||||
*/
|
||||
div: [(a, b) => a / b, num],
|
||||
div: [numeralArgs((a, b) => a / b), num],
|
||||
mod: [mod, num],
|
||||
pow: [Math.pow, num],
|
||||
_and: [(a, b) => a & b, num],
|
||||
|
||||
@ -238,6 +238,7 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
|
||||
|
||||
export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin();
|
||||
|
||||
// this function expects pat to be a pattern of floats...
|
||||
export const perlinWith = (pat) => {
|
||||
const pata = pat.fmap(Math.floor);
|
||||
const patb = pat.fmap((t) => Math.floor(t) + 1);
|
||||
@ -255,7 +256,7 @@ export const perlinWith = (pat) => {
|
||||
* s("bd sd,hh*4").cutoff(perlin.range(500,2000))
|
||||
*
|
||||
*/
|
||||
export const perlin = perlinWith(time);
|
||||
export const perlin = perlinWith(time.fmap((v) => Number(v)));
|
||||
|
||||
Pattern.prototype._degradeByWith = function (withPat, x) {
|
||||
return this.fmap((a) => (_) => a).appLeft(withPat._filterValues((v) => v > x));
|
||||
|
||||
@ -47,6 +47,9 @@ import {
|
||||
|
||||
import { steady } from '../signal.mjs';
|
||||
|
||||
import controls from '../controls.mjs';
|
||||
|
||||
const { n } = controls;
|
||||
const st = (begin, end) => new State(ts(begin, end));
|
||||
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
|
||||
const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context);
|
||||
@ -138,7 +141,7 @@ describe('Pattern', () => {
|
||||
expect(pure('hello').query(st(0.5, 2.5)).length).toBe(3);
|
||||
});
|
||||
it('Supports zero-width queries', () => {
|
||||
expect(pure('hello').queryArc(0,0).length).toBe(1);
|
||||
expect(pure('hello').queryArc(0, 0).length).toBe(1);
|
||||
});
|
||||
});
|
||||
describe('fmap()', () => {
|
||||
@ -194,6 +197,9 @@ describe('Pattern', () => {
|
||||
sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
|
||||
);
|
||||
});
|
||||
it('can add object patterns', () => {
|
||||
sameFirst(n(sequence(1, [2, 3])).add(n(10)), n(sequence(11, [12, 13])));
|
||||
});
|
||||
});
|
||||
describe('keep()', () => {
|
||||
it('can structure In()', () => {
|
||||
@ -376,9 +382,10 @@ describe('Pattern', () => {
|
||||
);
|
||||
});
|
||||
it('copes with breaking up events across cycles', () => {
|
||||
expect(pure('a').slow(2)._fastGap(2)._setContext({}).query(st(0, 2))).toStrictEqual(
|
||||
[hap(ts(0, 1), ts(0, 0.5), 'a'), hap(ts(0.5, 1.5), ts(1, 1.5), 'a')]
|
||||
);
|
||||
expect(pure('a').slow(2)._fastGap(2)._setContext({}).query(st(0, 2))).toStrictEqual([
|
||||
hap(ts(0, 1), ts(0, 0.5), 'a'),
|
||||
hap(ts(0.5, 1.5), ts(1, 1.5), 'a'),
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('_compressSpan()', () => {
|
||||
@ -434,14 +441,14 @@ describe('Pattern', () => {
|
||||
// mini('eb3 [c3 g3]/2 ') always plays [c3 g3]
|
||||
});
|
||||
it('Supports zero-length queries', () => {
|
||||
expect(steady('a')._slow(1).queryArc(0,0)
|
||||
).toStrictEqual(steady('a').queryArc(0,0))
|
||||
expect(steady('a')._slow(1).queryArc(0, 0)).toStrictEqual(steady('a').queryArc(0, 0));
|
||||
});
|
||||
});
|
||||
describe('slow()', () => {
|
||||
it('Supports zero-length queries', () => {
|
||||
expect(steady('a').slow(1)._setContext({}).queryArc(0,0)
|
||||
).toStrictEqual(steady('a')._setContext({}).queryArc(0,0))
|
||||
expect(steady('a').slow(1)._setContext({}).queryArc(0, 0)).toStrictEqual(
|
||||
steady('a')._setContext({}).queryArc(0, 0),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('inside', () => {
|
||||
@ -812,10 +819,11 @@ describe('Pattern', () => {
|
||||
});
|
||||
it('Squeezes to the correct cycle', () => {
|
||||
expect(
|
||||
pure(time.struct(true))._squeezeJoin().queryArc(3,4).map(x => x.value)
|
||||
).toStrictEqual(
|
||||
[Fraction(3.5)]
|
||||
)
|
||||
pure(time.struct(true))
|
||||
._squeezeJoin()
|
||||
.queryArc(3, 4)
|
||||
.map((x) => x.value),
|
||||
).toStrictEqual([Fraction(3.5)]);
|
||||
});
|
||||
});
|
||||
describe('ply', () => {
|
||||
@ -868,9 +876,7 @@ describe('Pattern', () => {
|
||||
});
|
||||
describe('range', () => {
|
||||
it('Can be patterned', () => {
|
||||
expect(sequence(0, 0).range(sequence(0, 0.5), 1).firstCycle()).toStrictEqual(
|
||||
sequence(0, 0.5).firstCycle(),
|
||||
);
|
||||
expect(sequence(0, 0).range(sequence(0, 0.5), 1).firstCycle()).toStrictEqual(sequence(0, 0.5).firstCycle());
|
||||
});
|
||||
});
|
||||
describe('range2', () => {
|
||||
|
||||
@ -5,7 +5,19 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import { pure } from '../pattern.mjs';
|
||||
import { isNote, tokenizeNote, toMidi, fromMidi, mod, compose, getFrequency, getPlayableNoteValue } from '../util.mjs';
|
||||
import {
|
||||
isNote,
|
||||
tokenizeNote,
|
||||
toMidi,
|
||||
fromMidi,
|
||||
mod,
|
||||
compose,
|
||||
getFrequency,
|
||||
getPlayableNoteValue,
|
||||
parseNumeral,
|
||||
numeralArgs,
|
||||
fractionalArgs,
|
||||
} from '../util.mjs';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('isNote', () => {
|
||||
@ -92,16 +104,16 @@ describe('getFrequency', () => {
|
||||
expect(getFrequency(happify(57, { type: 'midi' }))).toEqual(220);
|
||||
});
|
||||
it('should return frequencies unchanged', () => {
|
||||
expect(getFrequency(happify(440, { type: 'frequency' }))).toEqual(440);
|
||||
expect(getFrequency(happify(440, { type: 'frequency' }))).toEqual(440);
|
||||
expect(getFrequency(happify(432, { type: 'frequency' }))).toEqual(432);
|
||||
});
|
||||
it('should turn object with a "freq" property into frequency', () => {
|
||||
expect(getFrequency(happify({freq: 220}))).toEqual(220)
|
||||
expect(getFrequency(happify({freq: 440}))).toEqual(440)
|
||||
expect(getFrequency(happify({ freq: 220 }))).toEqual(220);
|
||||
expect(getFrequency(happify({ freq: 440 }))).toEqual(440);
|
||||
});
|
||||
it('should throw an error when given a non-note', () => {
|
||||
expect(() => getFrequency(happify('Q'))).toThrowError(`not a note or frequency: Q`)
|
||||
expect(() => getFrequency(happify('Z'))).toThrowError(`not a note or frequency: Z`)
|
||||
expect(() => getFrequency(happify('Q'))).toThrowError(`not a note or frequency: Q`);
|
||||
expect(() => getFrequency(happify('Z'))).toThrowError(`not a note or frequency: Z`);
|
||||
});
|
||||
});
|
||||
|
||||
@ -140,22 +152,72 @@ describe('compose', () => {
|
||||
describe('getPlayableNoteValue', () => {
|
||||
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
|
||||
it('should return object "note" property', () => {
|
||||
expect(getPlayableNoteValue(happify({note: "a4"}))).toEqual('a4')
|
||||
expect(getPlayableNoteValue(happify({ note: 'a4' }))).toEqual('a4');
|
||||
});
|
||||
it('should return object "n" property', () => {
|
||||
expect(getPlayableNoteValue(happify({n: "a4"}))).toEqual('a4')
|
||||
expect(getPlayableNoteValue(happify({ n: 'a4' }))).toEqual('a4');
|
||||
});
|
||||
it('should return object "value" property', () => {
|
||||
expect(getPlayableNoteValue(happify({value: "a4"}))).toEqual('a4')
|
||||
expect(getPlayableNoteValue(happify({ value: 'a4' }))).toEqual('a4');
|
||||
});
|
||||
it('should turn midi into frequency', () => {
|
||||
expect(getPlayableNoteValue(happify(57, {type: 'midi'}))).toEqual(220)
|
||||
})
|
||||
expect(getPlayableNoteValue(happify(57, { type: 'midi' }))).toEqual(220);
|
||||
});
|
||||
it('should return frequency value', () => {
|
||||
expect(getPlayableNoteValue(happify(220, {type: 'frequency'}))).toEqual(220)
|
||||
})
|
||||
expect(getPlayableNoteValue(happify(220, { type: 'frequency' }))).toEqual(220);
|
||||
});
|
||||
it('should throw an error if value is not an object, number, or string', () => {
|
||||
expect(() => getPlayableNoteValue(happify(false))).toThrowError(`not a note: false`)
|
||||
expect(() => getPlayableNoteValue(happify(undefined))).toThrowError(`not a note: undefined`)
|
||||
})
|
||||
});
|
||||
expect(() => getPlayableNoteValue(happify(false))).toThrowError(`not a note: false`);
|
||||
expect(() => getPlayableNoteValue(happify(undefined))).toThrowError(`not a note: undefined`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseNumeral', () => {
|
||||
it('should parse numbers as is', () => {
|
||||
expect(parseNumeral(4)).toBe(4);
|
||||
expect(parseNumeral(0)).toBe(0);
|
||||
expect(parseNumeral(20)).toBe(20);
|
||||
expect(parseNumeral('20')).toBe(20);
|
||||
expect(parseNumeral(1.5)).toBe(1.5);
|
||||
});
|
||||
it('should parse notes', () => {
|
||||
expect(parseNumeral('c4')).toBe(60);
|
||||
expect(parseNumeral('c#4')).toBe(61);
|
||||
expect(parseNumeral('db4')).toBe(61);
|
||||
});
|
||||
it('should throw an error for unknown strings', () => {
|
||||
expect(() => parseNumeral('xyz')).toThrowError('cannot parse as numeral: "xyz"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseFractional', () => {
|
||||
it('should parse numbers as is', () => {
|
||||
expect(parseFractional(4)).toBe(4);
|
||||
expect(parseFractional(0)).toBe(0);
|
||||
expect(parseFractional(20)).toBe(20);
|
||||
expect(parseFractional('20')).toBe(20);
|
||||
expect(parseFractional(1.5)).toBe(1.5);
|
||||
});
|
||||
it('should parse fractional shorthands values', () => {
|
||||
expect(parseFractional('w')).toBe(1);
|
||||
expect(parseFractional('h')).toBe(0.5);
|
||||
expect(parseFractional('q')).toBe(0.25);
|
||||
expect(parseFractional('e')).toBe(0.125);
|
||||
});
|
||||
it('should throw an error for unknown strings', () => {
|
||||
expect(() => parseFractional('xyz')).toThrowError('cannot parse as fractional: "xyz"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('numeralArgs', () => {
|
||||
it('should convert function arguments to numbers', () => {
|
||||
const add = numeralArgs((a, b) => a + b);
|
||||
expect(add('c4', 2)).toBe(62);
|
||||
});
|
||||
});
|
||||
describe('fractionalArgs', () => {
|
||||
it('should convert function arguments to numbers', () => {
|
||||
const add = fractionalArgs((a, b) => a + b);
|
||||
expect(add('q', 2)).toBe(2.25);
|
||||
});
|
||||
});
|
||||
|
||||
@ -130,3 +130,46 @@ export function curry(func, overload) {
|
||||
}
|
||||
return fn;
|
||||
}
|
||||
|
||||
export function parseNumeral(numOrString) {
|
||||
const asNumber = Number(numOrString);
|
||||
if (!isNaN(asNumber)) {
|
||||
return asNumber;
|
||||
}
|
||||
if (isNote(numOrString)) {
|
||||
return toMidi(numOrString);
|
||||
}
|
||||
throw new Error(`cannot parse as numeral: "${numOrString}"`);
|
||||
}
|
||||
|
||||
export function mapArgs(fn, mapFn) {
|
||||
return (...args) => fn(...args.map(mapFn));
|
||||
}
|
||||
|
||||
export function numeralArgs(fn) {
|
||||
return mapArgs(fn, parseNumeral);
|
||||
}
|
||||
|
||||
export function parseFractional(numOrString) {
|
||||
const asNumber = Number(numOrString);
|
||||
if (!isNaN(asNumber)) {
|
||||
return asNumber;
|
||||
}
|
||||
const specialValue = {
|
||||
pi: Math.PI,
|
||||
w: 1,
|
||||
h: 0.5,
|
||||
q: 0.25,
|
||||
e: 0.125,
|
||||
s: 0.0625,
|
||||
t: 1 / 3,
|
||||
f: 0.2,
|
||||
x: 1 / 6,
|
||||
}[numOrString];
|
||||
if (typeof specialValue !== 'undefined') {
|
||||
return specialValue;
|
||||
}
|
||||
throw new Error(`cannot parse as fractional: "${numOrString}"`);
|
||||
}
|
||||
|
||||
export const fractionalArgs = (fn) => mapArgs(fn, parseFractional);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user