support object arithmetic

This commit is contained in:
Felix Roos 2022-11-01 21:31:39 +01:00
parent 24328ee8ae
commit e128b02da1
5 changed files with 159 additions and 42 deletions

View file

@ -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', () => {

View file

@ -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);
});
});