Merge pull request 'Fix multiple cases where % was used instead of _mod()' (#2092) from tzwaan/saw-modulo-bug into main

Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2092
This commit is contained in:
Alex McLean
2026-08-13 01:13:29 +02:00
5 changed files with 138 additions and 19 deletions
+10 -9
View File
@@ -1002,7 +1002,7 @@ export const arpWith = register('arpWith', (func, pat) => {
* */
export const arp = register(
'arp',
(indices, pat) => pat.arpWith((haps) => reify(indices).fmap((i) => haps[i % haps.length])),
(indices, pat) => pat.arpWith((haps) => reify(indices).fmap((i) => haps[_mod(i, haps.length)])),
false,
);
@@ -1529,7 +1529,9 @@ export function slowcat(...pats) {
// Array test here is to avoid infinite recursions..
pats = pats.map((pat) => (Array.isArray(pat) ? fastcat(...pat) : reify(pat)));
if (pats.length == 1) {
if (!pats.length) {
return silence;
} else if (pats.length == 1) {
return pats[0];
}
@@ -1537,10 +1539,6 @@ export function slowcat(...pats) {
const span = state.span;
const pat_n = _mod(span.begin.sam(), pats.length);
const pat = pats[pat_n];
if (!pat) {
// pat_n can be negative, if the span is in the past..
return [];
}
// A bit of maths to make sure that cycles from constituent patterns aren't skipped.
// 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.
@@ -1557,11 +1555,14 @@ export function slowcat(...pats) {
* @return {Pattern}
*/
export function slowcatPrime(...pats) {
if (!pats.length) {
return silence;
}
pats = pats.map(reify);
const query = function (state) {
const pat_n = Math.floor(state.span.begin) % pats.length;
const pat = pats[pat_n]; // can be undefined for same cases e.g. /#cHVyZSg0MikKICAuZXZlcnkoMyxhZGQoNykpCiAgLmxhdGUoLjUp
return pat?.query(state) || [];
const pat_n = _mod(Math.floor(state.span.begin), pats.length);
const pat = pats[pat_n];
return pat.query(state);
};
return new Pattern(query).splitQueries();
}
+21 -4
View File
@@ -6,6 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import { Hap } from './hap.mjs';
import { Pattern, fastcat, pure, register, reify, silence, stack, sequenceP } from './pattern.mjs';
import { _mod } from './util.mjs';
import Fraction from './fraction.mjs';
import { id, keyAlias, getCurrentKeyboardState } from './util.mjs';
@@ -33,7 +34,7 @@ export const signal = (func) => {
* .scale('C major')
*
*/
export const saw = signal((t) => t % 1);
export const saw = signal((t) => _mod(t, 1));
/**
* A sawtooth signal between -1 and 1 (like `saw`, but bipolar).
@@ -56,7 +57,7 @@ export const saw2 = saw.toBipolar();
* .scale('C major')
*
*/
export const isaw = signal((t) => 1 - (t % 1));
export const isaw = signal((t) => 1 - _mod(t, 1));
/**
* A sawtooth signal between 1 and -1 (like `saw2`, but flipped).
@@ -113,7 +114,7 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
* n(square.segment(4).range(0,7)).scale("C:minor")
*
*/
export const square = signal((t) => Math.floor((t * 2) % 2));
export const square = signal((t) => Math.floor(_mod(t * 2, 2)));
/**
* A square signal between -1 and 1 (like `square`, but bipolar).
@@ -123,6 +124,22 @@ export const square = signal((t) => Math.floor((t * 2) % 2));
*/
export const square2 = square.toBipolar();
/**
* A square signal between 1 and 0 (like `square` but flipped).
*
* @return {Pattern}
* @tags generators
*/
export const isquare = signal((t) => 1 - Math.floor(_mod(t * 2, 2)));
/**
* A square signal between 1 and -1 (like `isquare`, but bipolar).
*
* @return {Pattern}
* @tags generators
*/
export const isquare2 = isquare.toBipolar();
/**
* A triangle signal between 0 and 1.
*
@@ -396,7 +413,7 @@ export const randrun = (n) => {
.map((n, i) => [n, i])
.sort((a, b) => (a[0] > b[0]) - (a[0] < b[0]))
.map((x) => x[1]);
const i = t.cyclePos().mul(n).floor() % n;
const i = _mod(t.cyclePos().mul(n).floor(), n);
return nums[i];
})._segment(n);
};
+37
View File
@@ -18,6 +18,7 @@ import {
fastcat,
firstOf,
slowcat,
slowcatPrime,
cat,
sequence,
palindrome,
@@ -53,6 +54,7 @@ import {
stepcat,
sometimes,
expand,
arp,
} from '../index.mjs';
import { log, logValues } from '../pattern.mjs';
@@ -544,6 +546,9 @@ describe('Pattern', () => {
});
});
describe('slowcat()', () => {
it('Can be empty', () => {
expect(slowcat().firstCycle()).toStrictEqual([]);
});
it('Can concatenate things slowly', () => {
expect(
slowcat('a', 'b')
@@ -576,6 +581,38 @@ describe('Pattern', () => {
sameFirst(slowcat('a', ['b', 'c']).fast(4), sequence('a', ['b', 'c']).fast(2));
});
});
describe('slowcatPrime()', () => {
it('Can be empty', () => {
expect(slowcatPrime().firstCycle()).toStrictEqual([]);
});
it('Can slowcat patterns swapping back and forth skipping the expected notes', () => {
expect(
slowcatPrime(fastcat(0, 1, 2, 3).slow(2), fastcat(4, 5, 6, 7).slow(2))
.fast(4)
.firstCycle()
.map((a) => a.value),
).toStrictEqual([0, 1, 6, 7, 0, 1, 6, 7]);
});
it('Can go into negative time', () => {
expect(
slowcatPrime(fastcat(0, 1, 2, 3).slow(2), fastcat(4, 5, 6, 7).slow(2))
.fast(4)
.late(8)
.firstCycle()
.map((a) => a.value),
).toStrictEqual([0, 1, 6, 7, 0, 1, 6, 7]);
});
});
describe('arp()', () => {
it('It wraps around with both positive and negative indices', () => {
expect(
stack('a', 'b', 'c')
.arp(fastcat(-3, -2, -1, 0, 1, 2, 3, 4))
.firstCycle()
.map((a) => a.value),
).toStrictEqual(['a', 'b', 'c', 'a', 'b', 'c', 'a', 'b']);
});
});
describe('rev()', () => {
it('Can reverse things', () => {
expect(
+66 -6
View File
@@ -8,7 +8,23 @@ import Fraction from 'fraction.js';
import { describe, it, expect, vi } from 'vitest';
import { saw, saw2, isaw, isaw2, per, perx, cyclesPer } from '../signal.mjs';
import {
saw,
saw2,
isaw,
isaw2,
tri,
tri2,
itri,
itri2,
square,
square2,
isquare,
isquare2,
per,
perx,
cyclesPer,
} from '../signal.mjs';
import { fastcat, sequence, State, TimeSpan, Hap, note } from '../index.mjs';
const st = (begin, end) => new State(ts(begin, end));
@@ -24,17 +40,61 @@ const sameFirst = (a, b) => {
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(saw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(0, 0.25, 0.5, 0.75).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());
});
it('Can make tri/tri2', () => {
expect(tri.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(0, 0.5, 1, 0.5).firstCycle());
expect(tri2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, 0, 1, 0).firstCycle());
});
it('Can make itri/itri2', () => {
expect(itri.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, 0.5).firstCycle());
expect(itri2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0, -1, 0).firstCycle());
});
it('Can make square/square2', () => {
expect(square.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(0, 0, 1, 1).firstCycle());
expect(square2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -1, 1, 1).firstCycle());
});
it('Can make isquare/isquare2', () => {
expect(isquare.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 1, 0, 0).firstCycle());
expect(isquare2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 1, -1, -1).firstCycle());
});
it('Can go into negative time', () => {
expect(saw.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
sequence(0, 0.25, 0.5, 0.75).firstCycle(),
);
expect(saw2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
sequence(-1, -0.5, 0, 0.5).firstCycle(),
);
expect(isaw.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
sequence(1, 0.75, 0.5, 0.25).firstCycle(),
);
expect(isaw2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
sequence(1, 0.5, 0, -0.5).firstCycle(),
);
expect(tri.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
sequence(0, 0.5, 1, 0.5).firstCycle(),
);
expect(tri2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, 0, 1, 0).firstCycle());
expect(itri.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
sequence(1, 0.5, 0, 0.5).firstCycle(),
);
expect(itri2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0, -1, 0).firstCycle());
expect(square.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(0, 0, 1, 1).firstCycle());
expect(square2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
sequence(-1, -1, 1, 1).firstCycle(),
);
expect(isquare.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
sequence(1, 1, 0, 0).firstCycle(),
);
expect(isquare2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
sequence(1, 1, -1, -1).firstCycle(),
);
});
});
describe('cyclesPer', () => {
+4
View File
@@ -7203,8 +7203,12 @@ exports[`renders tunes > tune: holyflute 1`] = `
exports[`renders tunes > tune: juxUndTollerei 1`] = `
[
"[ -99/200 ⇜ (0/1 → 1/200) | note:63 s:triangle pan:0 cutoff:758.852817928549 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ -99/200 ⇜ (0/1 → 1/200) | note:67 s:triangle pan:1 color:green cutoff:758.852817928549 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 0/1 → 1/4 | note:c3 s:sawtooth pan:0 cutoff:1100 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 0/1 → 1/4 | note:bb3 s:sawtooth pan:1 color:green cutoff:1100 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 1/200 → 101/200 | note:55 s:triangle pan:0 cutoff:1103.534282651425 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 1/200 → 101/200 | note:65 s:triangle pan:1 color:green cutoff:1103.534282651425 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 1/4 → 1/2 | note:eb3 s:sawtooth pan:0 cutoff:1275.581289814515 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 1/4 → 1/2 | note:g3 s:sawtooth pan:1 color:green cutoff:1275.581289814515 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 1/2 → 3/4 | note:g3 s:sawtooth pan:0 cutoff:1444.415089128581 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",