diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index c7e143ae6..c9ed21663 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -970,10 +970,48 @@ export const keyDown = register('keyDown', function (pat) { }); /** - * A pattern that gives the duration of events that are combined with it. + * A pattern measuring the duration of events, + * in cycles per event. `cyclesPer` doesn't have structure itself, but takes structure, and therefore + * event durations, from the pattern that it is combined with. + * For example `cyclesPer.struct("1 1 [1 1] 1")` would give the same as `"0.25 0.25 [0.125 0.125] 0.25"`. + * See also its reciprocal, `per`, also known as `perCycle`. * @example - * sound("bd sd [bd bd] sd*4 [- sd] [bd [bd bd]]").note(delta.withValue(x => 1/x + 20)) + * // Shorter events are lower in pitch + * sound("saw saw [saw saw] saw") + * .note(cyclesPer.range(50, 100)) + * @example + * sound("bd sd [bd bd] sd*4 [- sd] [bd [bd bd]]") + * .note(cyclesPer.add(20)) */ -export const delta = new Pattern(function (state) { +export const cyclesPer = new Pattern(function (state) { return [new Hap(undefined, state.span, state.span.duration)]; }); + +/** + * A pattern measuring the 'shortness' of events, or in other words, the duration of pattern events, + * in events per cycle. `per` doesn't have structure itself, but takes structure, and therefore + * event durations, from the pattern that it is combined with. + * For example `per.struct("1 1 [1 1] 1")` would give the same as `"4 4 [8 8] 4"`. + * See also its reciprocal, `cyclesPer`. + * @synonyms perCycle + * @example + * // Shorter events are more distorted + * n("0 0*2 0 0*2 0 [0 0 0]@2").sound("bd") + * .distort(per.div(2)) + */ +export const per = new Pattern(function (state) { + return [new Hap(undefined, state.span, Fraction(1).div(state.span.duration))]; +}); + +export const perCycle = per; + +/** + * Like `per` but measures the shortness of events according to an exponential curve. In + * particular, where the event duration halves, the + * returned value increases by one. `perx.struct("1 1 [1 [1 1]] 1")` would therefore be + * the same as `"3 3 [4 [5 5]] 3"`. + */ +export const perx = new Pattern(function (state) { + const n = Fraction(1).div(state.span.duration); + return [new Hap(undefined, state.span, Math.log(n) / Math.log(2) + 1)]; +}); diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 408ff274b..aaa14bd27 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -740,20 +740,6 @@ describe('Pattern', () => { ); }); }); - describe('signal()', () => { - it('Can make saw/saw2', () => { - expect(saw.struct(true, true, true, true).firstCycle()).toStrictEqual( - sequence(0, 1 / 4, 1 / 2, 3 / 4).firstCycle(), - ); - - expect(saw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -0.5, 0, 0.5).firstCycle()); - }); - it('Can make isaw/isaw2', () => { - expect(isaw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.75, 0.5, 0.25).firstCycle()); - - expect(isaw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, -0.5).firstCycle()); - }); - }); describe('_setContext()', () => { it('Can set the hap context', () => { expect( diff --git a/packages/core/test/signal.test.mjs b/packages/core/test/signal.test.mjs new file mode 100644 index 000000000..b02f8a08b --- /dev/null +++ b/packages/core/test/signal.test.mjs @@ -0,0 +1,61 @@ +/* +signal.test.mjs - +Copyright (C) 2022 Strudel contributors - see +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 . +*/ + +import Fraction from 'fraction.js'; + +import { describe, it, expect, vi } from 'vitest'; + +import { saw, saw2, isaw, isaw2, per, perx, cyclesPer } from '../signal.mjs'; +import { fastcat, sequence, State, TimeSpan, Hap } from '../index.mjs'; + +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); + +const third = Fraction(1, 3); +const twothirds = Fraction(2, 3); + +const sameFirst = (a, b) => { + return expect(a.sortHapsByPart().firstCycle()).toStrictEqual(b.sortHapsByPart().firstCycle()); +}; + +describe('signal()', () => { + it('Can make saw/saw2', () => { + expect(saw.struct(true, true, true, true).firstCycle()).toStrictEqual( + sequence(0, 1 / 4, 1 / 2, 3 / 4).firstCycle(), + ); + + expect(saw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -0.5, 0, 0.5).firstCycle()); + }); + it('Can make isaw/isaw2', () => { + expect(isaw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.75, 0.5, 0.25).firstCycle()); + + expect(isaw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, -0.5).firstCycle()); + }); +}); + +describe('cyclesPer', () => { + it('gives cycles per hap', () => { + sameFirst( + cyclesPer.struct(true, true, true, fastcat(true, true)), + sequence(0.25, 0.25, 0.25, fastcat(0.125, 0.125)).fmap(Fraction), + ); + }); +}); +describe('per', () => { + it('gives haps per cycle', () => { + sameFirst(per.struct(true, true, true, fastcat(true, true)), sequence(4, 4, 4, fastcat(8, 8)).fmap(Fraction)); + }); +}); + +describe('perx', () => { + it('gives exponential haps per cycle', () => { + sameFirst( + perx.struct(true, true, true, fastcat(true, fastcat(true, true))), + sequence(3, 3, 3, fastcat(4, fastcat(5, 5))), + ); + }); +}); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 0b9774205..5f8133ec5 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -2539,6 +2539,84 @@ exports[`runs examples > example "cut" example index 0 1`] = ` ] `; +exports[`runs examples > example "cyclesPer" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:saw note:62.5 ]", + "[ 1/4 → 1/2 | s:saw note:62.5 ]", + "[ 1/2 → 5/8 | s:saw note:56.25 ]", + "[ 5/8 → 3/4 | s:saw note:56.25 ]", + "[ 3/4 → 1/1 | s:saw note:62.5 ]", + "[ 1/1 → 5/4 | s:saw note:62.5 ]", + "[ 5/4 → 3/2 | s:saw note:62.5 ]", + "[ 3/2 → 13/8 | s:saw note:56.25 ]", + "[ 13/8 → 7/4 | s:saw note:56.25 ]", + "[ 7/4 → 2/1 | s:saw note:62.5 ]", + "[ 2/1 → 9/4 | s:saw note:62.5 ]", + "[ 9/4 → 5/2 | s:saw note:62.5 ]", + "[ 5/2 → 21/8 | s:saw note:56.25 ]", + "[ 21/8 → 11/4 | s:saw note:56.25 ]", + "[ 11/4 → 3/1 | s:saw note:62.5 ]", + "[ 3/1 → 13/4 | s:saw note:62.5 ]", + "[ 13/4 → 7/2 | s:saw note:62.5 ]", + "[ 7/2 → 29/8 | s:saw note:56.25 ]", + "[ 29/8 → 15/4 | s:saw note:56.25 ]", + "[ 15/4 → 4/1 | s:saw note:62.5 ]", +] +`; + +exports[`runs examples > example "cyclesPer" example index 1 1`] = ` +[ + "[ 0/1 → 1/6 | s:bd note:20.166666666666668 ]", + "[ 1/6 → 1/3 | s:sd note:20.166666666666668 ]", + "[ 1/3 → 5/12 | s:bd note:20.083333333333332 ]", + "[ 5/12 → 1/2 | s:bd note:20.083333333333332 ]", + "[ 1/2 → 13/24 | s:sd note:20.041666666666668 ]", + "[ 13/24 → 7/12 | s:sd note:20.041666666666668 ]", + "[ 7/12 → 5/8 | s:sd note:20.041666666666668 ]", + "[ 5/8 → 2/3 | s:sd note:20.041666666666668 ]", + "[ 3/4 → 5/6 | s:sd note:20.083333333333332 ]", + "[ 5/6 → 11/12 | s:bd note:20.083333333333332 ]", + "[ 11/12 → 23/24 | s:bd note:20.041666666666668 ]", + "[ 23/24 → 1/1 | s:bd note:20.041666666666668 ]", + "[ 1/1 → 7/6 | s:bd note:20.166666666666668 ]", + "[ 7/6 → 4/3 | s:sd note:20.166666666666668 ]", + "[ 4/3 → 17/12 | s:bd note:20.083333333333332 ]", + "[ 17/12 → 3/2 | s:bd note:20.083333333333332 ]", + "[ 3/2 → 37/24 | s:sd note:20.041666666666668 ]", + "[ 37/24 → 19/12 | s:sd note:20.041666666666668 ]", + "[ 19/12 → 13/8 | s:sd note:20.041666666666668 ]", + "[ 13/8 → 5/3 | s:sd note:20.041666666666668 ]", + "[ 7/4 → 11/6 | s:sd note:20.083333333333332 ]", + "[ 11/6 → 23/12 | s:bd note:20.083333333333332 ]", + "[ 23/12 → 47/24 | s:bd note:20.041666666666668 ]", + "[ 47/24 → 2/1 | s:bd note:20.041666666666668 ]", + "[ 2/1 → 13/6 | s:bd note:20.166666666666668 ]", + "[ 13/6 → 7/3 | s:sd note:20.166666666666668 ]", + "[ 7/3 → 29/12 | s:bd note:20.083333333333332 ]", + "[ 29/12 → 5/2 | s:bd note:20.083333333333332 ]", + "[ 5/2 → 61/24 | s:sd note:20.041666666666668 ]", + "[ 61/24 → 31/12 | s:sd note:20.041666666666668 ]", + "[ 31/12 → 21/8 | s:sd note:20.041666666666668 ]", + "[ 21/8 → 8/3 | s:sd note:20.041666666666668 ]", + "[ 11/4 → 17/6 | s:sd note:20.083333333333332 ]", + "[ 17/6 → 35/12 | s:bd note:20.083333333333332 ]", + "[ 35/12 → 71/24 | s:bd note:20.041666666666668 ]", + "[ 71/24 → 3/1 | s:bd note:20.041666666666668 ]", + "[ 3/1 → 19/6 | s:bd note:20.166666666666668 ]", + "[ 19/6 → 10/3 | s:sd note:20.166666666666668 ]", + "[ 10/3 → 41/12 | s:bd note:20.083333333333332 ]", + "[ 41/12 → 7/2 | s:bd note:20.083333333333332 ]", + "[ 7/2 → 85/24 | s:sd note:20.041666666666668 ]", + "[ 85/24 → 43/12 | s:sd note:20.041666666666668 ]", + "[ 43/12 → 29/8 | s:sd note:20.041666666666668 ]", + "[ 29/8 → 11/3 | s:sd note:20.041666666666668 ]", + "[ 15/4 → 23/6 | s:sd note:20.083333333333332 ]", + "[ 23/6 → 47/12 | s:bd note:20.083333333333332 ]", + "[ 47/12 → 95/24 | s:bd note:20.041666666666668 ]", + "[ 95/24 → 4/1 | s:bd note:20.041666666666668 ]", +] +`; + exports[`runs examples > example "decay" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c3 decay:0.1 sustain:0 ]", @@ -2784,59 +2862,6 @@ exports[`runs examples > example "delaysync" example index 0 1`] = ` ] `; -exports[`runs examples > example "delta" example index 0 1`] = ` -[ - "[ 0/1 → 1/6 | s:bd note:26 ]", - "[ 1/6 → 1/3 | s:sd note:26 ]", - "[ 1/3 → 5/12 | s:bd note:32 ]", - "[ 5/12 → 1/2 | s:bd note:32 ]", - "[ 1/2 → 13/24 | s:sd note:44 ]", - "[ 13/24 → 7/12 | s:sd note:44 ]", - "[ 7/12 → 5/8 | s:sd note:44 ]", - "[ 5/8 → 2/3 | s:sd note:44 ]", - "[ 3/4 → 5/6 | s:sd note:32 ]", - "[ 5/6 → 11/12 | s:bd note:32 ]", - "[ 11/12 → 23/24 | s:bd note:44 ]", - "[ 23/24 → 1/1 | s:bd note:44 ]", - "[ 1/1 → 7/6 | s:bd note:26 ]", - "[ 7/6 → 4/3 | s:sd note:26 ]", - "[ 4/3 → 17/12 | s:bd note:32 ]", - "[ 17/12 → 3/2 | s:bd note:32 ]", - "[ 3/2 → 37/24 | s:sd note:44 ]", - "[ 37/24 → 19/12 | s:sd note:44 ]", - "[ 19/12 → 13/8 | s:sd note:44 ]", - "[ 13/8 → 5/3 | s:sd note:44 ]", - "[ 7/4 → 11/6 | s:sd note:32 ]", - "[ 11/6 → 23/12 | s:bd note:32 ]", - "[ 23/12 → 47/24 | s:bd note:44 ]", - "[ 47/24 → 2/1 | s:bd note:44 ]", - "[ 2/1 → 13/6 | s:bd note:26 ]", - "[ 13/6 → 7/3 | s:sd note:26 ]", - "[ 7/3 → 29/12 | s:bd note:32 ]", - "[ 29/12 → 5/2 | s:bd note:32 ]", - "[ 5/2 → 61/24 | s:sd note:44 ]", - "[ 61/24 → 31/12 | s:sd note:44 ]", - "[ 31/12 → 21/8 | s:sd note:44 ]", - "[ 21/8 → 8/3 | s:sd note:44 ]", - "[ 11/4 → 17/6 | s:sd note:32 ]", - "[ 17/6 → 35/12 | s:bd note:32 ]", - "[ 35/12 → 71/24 | s:bd note:44 ]", - "[ 71/24 → 3/1 | s:bd note:44 ]", - "[ 3/1 → 19/6 | s:bd note:26 ]", - "[ 19/6 → 10/3 | s:sd note:26 ]", - "[ 10/3 → 41/12 | s:bd note:32 ]", - "[ 41/12 → 7/2 | s:bd note:32 ]", - "[ 7/2 → 85/24 | s:sd note:44 ]", - "[ 85/24 → 43/12 | s:sd note:44 ]", - "[ 43/12 → 29/8 | s:sd note:44 ]", - "[ 29/8 → 11/3 | s:sd note:44 ]", - "[ 15/4 → 23/6 | s:sd note:32 ]", - "[ 23/6 → 47/12 | s:bd note:32 ]", - "[ 47/12 → 95/24 | s:bd note:44 ]", - "[ 95/24 → 4/1 | s:bd note:44 ]", -] -`; - exports[`runs examples > example "density" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:crackle density:0.01 ]", @@ -7966,6 +7991,51 @@ exports[`runs examples > example "penv" example index 0 1`] = ` ] `; +exports[`runs examples > example "per" example index 0 1`] = ` +[ + "[ 0/1 → 1/7 | n:0 s:bd distort:3.5 ]", + "[ 1/7 → 3/14 | n:0 s:bd distort:7 ]", + "[ 3/14 → 2/7 | n:0 s:bd distort:7 ]", + "[ 2/7 → 3/7 | n:0 s:bd distort:3.5 ]", + "[ 3/7 → 1/2 | n:0 s:bd distort:7 ]", + "[ 1/2 → 4/7 | n:0 s:bd distort:7 ]", + "[ 4/7 → 5/7 | n:0 s:bd distort:3.5 ]", + "[ 5/7 → 17/21 | n:0 s:bd distort:5.25 ]", + "[ 17/21 → 19/21 | n:0 s:bd distort:5.25 ]", + "[ 19/21 → 1/1 | n:0 s:bd distort:5.25 ]", + "[ 1/1 → 8/7 | n:0 s:bd distort:3.5 ]", + "[ 8/7 → 17/14 | n:0 s:bd distort:7 ]", + "[ 17/14 → 9/7 | n:0 s:bd distort:7 ]", + "[ 9/7 → 10/7 | n:0 s:bd distort:3.5 ]", + "[ 10/7 → 3/2 | n:0 s:bd distort:7 ]", + "[ 3/2 → 11/7 | n:0 s:bd distort:7 ]", + "[ 11/7 → 12/7 | n:0 s:bd distort:3.5 ]", + "[ 12/7 → 38/21 | n:0 s:bd distort:5.25 ]", + "[ 38/21 → 40/21 | n:0 s:bd distort:5.25 ]", + "[ 40/21 → 2/1 | n:0 s:bd distort:5.25 ]", + "[ 2/1 → 15/7 | n:0 s:bd distort:3.5 ]", + "[ 15/7 → 31/14 | n:0 s:bd distort:7 ]", + "[ 31/14 → 16/7 | n:0 s:bd distort:7 ]", + "[ 16/7 → 17/7 | n:0 s:bd distort:3.5 ]", + "[ 17/7 → 5/2 | n:0 s:bd distort:7 ]", + "[ 5/2 → 18/7 | n:0 s:bd distort:7 ]", + "[ 18/7 → 19/7 | n:0 s:bd distort:3.5 ]", + "[ 19/7 → 59/21 | n:0 s:bd distort:5.25 ]", + "[ 59/21 → 61/21 | n:0 s:bd distort:5.25 ]", + "[ 61/21 → 3/1 | n:0 s:bd distort:5.25 ]", + "[ 3/1 → 22/7 | n:0 s:bd distort:3.5 ]", + "[ 22/7 → 45/14 | n:0 s:bd distort:7 ]", + "[ 45/14 → 23/7 | n:0 s:bd distort:7 ]", + "[ 23/7 → 24/7 | n:0 s:bd distort:3.5 ]", + "[ 24/7 → 7/2 | n:0 s:bd distort:7 ]", + "[ 7/2 → 25/7 | n:0 s:bd distort:7 ]", + "[ 25/7 → 26/7 | n:0 s:bd distort:3.5 ]", + "[ 26/7 → 80/21 | n:0 s:bd distort:5.25 ]", + "[ 80/21 → 82/21 | n:0 s:bd distort:5.25 ]", + "[ 82/21 → 4/1 | n:0 s:bd distort:5.25 ]", +] +`; + exports[`runs examples > example "perlin" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh cutoff:500 ]",