Merge branch 'main' into envelope_improvements

This commit is contained in:
Jade (Rose) Rowland 2023-12-31 11:51:49 -05:00 committed by GitHub
commit a800b32529
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
79 changed files with 4033 additions and 4306 deletions

View file

@ -33,24 +33,24 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/autocomplete": "^6.6.0",
"@codemirror/commands": "^6.2.4",
"@codemirror/lang-javascript": "^6.1.7",
"@codemirror/language": "^6.6.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.10.0",
"@lezer/highlight": "^1.1.4",
"@codemirror/autocomplete": "^6.11.1",
"@codemirror/commands": "^6.3.3",
"@codemirror/lang-javascript": "^6.2.1",
"@codemirror/language": "^6.10.0",
"@codemirror/search": "^6.5.5",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.23.0",
"@lezer/highlight": "^1.2.0",
"@replit/codemirror-emacs": "^6.0.1",
"@replit/codemirror-vim": "^6.0.14",
"@replit/codemirror-vim": "^6.1.0",
"@replit/codemirror-vscode-keymap": "^6.0.2",
"@strudel.cycles/core": "workspace:*",
"@uiw/codemirror-themes": "^4.19.16",
"@uiw/codemirror-themes-all": "^4.19.16",
"nanostores": "^0.8.1",
"@nanostores/persistent": "^0.8.0"
"@uiw/codemirror-themes": "^4.21.21",
"@uiw/codemirror-themes-all": "^4.21.21",
"nanostores": "^0.9.5",
"@nanostores/persistent": "^0.9.1"
},
"devDependencies": {
"vite": "^4.3.3"
"vite": "^5.0.10"
}
}

View file

@ -9,7 +9,7 @@ export default defineConfig({
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],

View file

@ -31,11 +31,11 @@
},
"homepage": "https://strudel.cc",
"dependencies": {
"fraction.js": "^4.2.0"
"fraction.js": "^4.3.7"
},
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
"devDependencies": {
"vite": "^4.3.3",
"vitest": "^0.33.0"
"vite": "^5.0.10",
"vitest": "^1.1.0"
}
}

View file

@ -101,6 +101,7 @@ const timeToRand = (x) => Math.abs(intSeedToRand(timeToIntSeed(x)));
const timeToRandsPrime = (seed, n) => {
const result = [];
// eslint-disable-next-line
for (let i = 0; i < n; ++n) {
result.push(intSeedToRand(seed));
seed = xorwise(seed);

View file

@ -183,13 +183,17 @@ describe('Pattern', () => {
});
it('can Trig() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).add.trig(20, 30).early(2),
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.add.trig(20, 30)
.early(2),
sequence(26, 27, 36, 37),
);
});
it('can Trigzero() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).add.trigzero(20, 30).early(2),
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.add.trigzero(20, 30)
.early(2),
sequence(21, 22, 31, 32),
);
});
@ -231,13 +235,17 @@ describe('Pattern', () => {
});
it('can Trig() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keep.trig(20, 30).early(2),
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keep.trig(20, 30)
.early(2),
sequence(6, 7, 6, 7),
);
});
it('can Trigzero() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keep.trigzero(20, 30).early(2),
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keep.trigzero(20, 30)
.early(2),
sequence(1, 2, 1, 2),
);
});
@ -273,13 +281,17 @@ describe('Pattern', () => {
});
it('can Trig() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepif.trig(false, true).early(2),
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keepif.trig(false, true)
.early(2),
sequence(silence, silence, 6, 7),
);
});
it('can Trigzero() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepif.trigzero(false, true).early(2),
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keepif.trigzero(false, true)
.early(2),
sequence(silence, silence, 1, 2),
);
});
@ -651,7 +663,11 @@ describe('Pattern', () => {
});
describe('struct()', () => {
it('Can restructure a discrete pattern', () => {
expect(sequence('a', 'b').struct(sequence(true, true, true)).firstCycle()).toStrictEqual([
expect(
sequence('a', 'b')
.struct(sequence(true, true, true))
.firstCycle(),
).toStrictEqual([
hap(ts(0, third), ts(0, third), 'a'),
hap(ts(third, twothirds), ts(third, 0.5), 'a'),
hap(ts(third, twothirds), ts(0.5, twothirds), 'b'),
@ -682,7 +698,11 @@ describe('Pattern', () => {
});
describe('mask()', () => {
it('Can fragment a pattern', () => {
expect(sequence('a', 'b').mask(sequence(true, true, true)).firstCycle()).toStrictEqual([
expect(
sequence('a', 'b')
.mask(sequence(true, true, true))
.firstCycle(),
).toStrictEqual([
hap(ts(0, 0.5), ts(0, third), 'a'),
hap(ts(0, 0.5), ts(third, 0.5), 'a'),
hap(ts(0.5, 1), ts(0.5, twothirds), 'b'),
@ -951,9 +971,11 @@ describe('Pattern', () => {
expect(stack(pure('a').mask(1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(1);
});
it('Doesnt merge two overlapping haps', () => {
expect(stack(pure('a').mask(1, 1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(
2,
);
expect(
stack(pure('a').mask(1, 1, 0), pure('a').mask(0, 1))
.defragmentHaps()
.firstCycle().length,
).toStrictEqual(2);
});
it('Doesnt merge two touching haps with different values', () => {
expect(stack(pure('a').mask(1, 0), pure('b').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(2);

View file

@ -4,6 +4,8 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
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/>.
*/
import { logger } from './logger.mjs';
// returns true if the given string is a note
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name);
@ -84,6 +86,18 @@ export const midi2note = (n) => {
// modulo that works with negative numbers e.g. _mod(-1, 3) = 2. Works on numbers (rather than patterns of numbers, as @mod@ from pattern.mjs does)
export const _mod = (n, m) => ((n % m) + m) % m;
export function nanFallback(value, fallback) {
if (isNaN(Number(value))) {
logger(`"${value}" is not a number, falling back to ${fallback}`, 'warning');
return fallback;
}
return value;
}
// round to nearest int, negative numbers will output a subtracted index
export const getSoundIndex = (n, numSounds) => {
return _mod(Math.round(nanFallback(n ?? 0, 0)), numSounds);
};
export const getPlayableNoteValue = (hap) => {
let { value, context } = hap;
let note = value;
@ -262,14 +276,14 @@ export const sol2note = (n, notation = 'letters') => {
notation === 'solfeggio'
? solfeggio /*check if its is any of the following*/
: notation === 'indian'
? indian
: notation === 'german'
? german
: notation === 'byzantine'
? byzantine
: notation === 'japanese'
? japanese
: english; /*if not use standard version*/
? indian
: notation === 'german'
? german
: notation === 'byzantine'
? byzantine
: notation === 'japanese'
? japanese
: english; /*if not use standard version*/
const note = pc[n % 12]; /*calculating the midi value to the note*/
const oct = Math.floor(n / 12) - 1;
return note + oct;

View file

@ -9,7 +9,7 @@ export default defineConfig({
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],

View file

@ -37,6 +37,6 @@
"@strudel.cycles/webaudio": "workspace:*"
},
"devDependencies": {
"vite": "^4.3.3"
"vite": "^5.0.10"
}
}

View file

@ -9,7 +9,7 @@ export default defineConfig({
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],

View file

@ -23,7 +23,7 @@
},
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@tauri-apps/api": "^1.4.0"
"@tauri-apps/api": "^1.5.3"
},
"homepage": "https://github.com/tidalcycles/strudel#readme"
}

View file

@ -25,7 +25,8 @@ export async function initHydra(options = {}) {
feedStrudel = false,
...hydraConfig
} = { detectAudio: false, ...options };
await import(src);
await import(/* @vite-ignore */ src);
const hydra = new Hydra(hydraConfig);
if (feedStrudel) {
const { canvas } = getDrawContext();

View file

@ -38,6 +38,6 @@
},
"devDependencies": {
"pkg": "^5.8.1",
"vite": "^4.3.3"
"vite": "^5.0.10"
}
}

View file

@ -9,7 +9,7 @@ export default defineConfig({
lib: {
entry: resolve(__dirname, 'hydra.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],

View file

@ -31,9 +31,9 @@
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"webmidi": "^3.1.5"
"webmidi": "^3.1.8"
},
"devDependencies": {
"vite": "^4.3.3"
"vite": "^5.0.10"
}
}

View file

@ -9,7 +9,7 @@ export default defineConfig({
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],

View file

@ -219,7 +219,7 @@ function peg$parse(input, options) {
var peg$r0 = /^[1-9]/;
var peg$r1 = /^[eE]/;
var peg$r2 = /^[0-9]/;
var peg$r3 = /^[ \n\r\t]/;
var peg$r3 = /^[ \n\r\t\xA0]/;
var peg$r4 = /^[0-9~]/;
var peg$r5 = /^[^\n]/;
var peg$r6 = /^[a-z\xB5\xDF-\xF6\xF8-\xFF\u0101\u0103\u0105\u0107\u0109\u010B\u010D\u010F\u0111\u0113\u0115\u0117\u0119\u011B\u011D\u011F\u0121\u0123\u0125\u0127\u0129\u012B\u012D\u012F\u0131\u0133\u0135\u0137-\u0138\u013A\u013C\u013E\u0140\u0142\u0144\u0146\u0148-\u0149\u014B\u014D\u014F\u0151\u0153\u0155\u0157\u0159\u015B\u015D\u015F\u0161\u0163\u0165\u0167\u0169\u016B\u016D\u016F\u0171\u0173\u0175\u0177\u017A\u017C\u017E-\u0180\u0183\u0185\u0188\u018C-\u018D\u0192\u0195\u0199-\u019B\u019E\u01A1\u01A3\u01A5\u01A8\u01AA-\u01AB\u01AD\u01B0\u01B4\u01B6\u01B9-\u01BA\u01BD-\u01BF\u01C6\u01C9\u01CC\u01CE\u01D0\u01D2\u01D4\u01D6\u01D8\u01DA\u01DC-\u01DD\u01DF\u01E1\u01E3\u01E5\u01E7\u01E9\u01EB\u01ED\u01EF-\u01F0\u01F3\u01F5\u01F9\u01FB\u01FD\u01FF\u0201\u0203\u0205\u0207\u0209\u020B\u020D\u020F\u0211\u0213\u0215\u0217\u0219\u021B\u021D\u021F\u0221\u0223\u0225\u0227\u0229\u022B\u022D\u022F\u0231\u0233-\u0239\u023C\u023F-\u0240\u0242\u0247\u0249\u024B\u024D\u024F-\u0293\u0295-\u02AF\u0371\u0373\u0377\u037B-\u037D\u0390\u03AC-\u03CE\u03D0-\u03D1\u03D5-\u03D7\u03D9\u03DB\u03DD\u03DF\u03E1\u03E3\u03E5\u03E7\u03E9\u03EB\u03ED\u03EF-\u03F3\u03F5\u03F8\u03FB-\u03FC\u0430-\u045F\u0461\u0463\u0465\u0467\u0469\u046B\u046D\u046F\u0471\u0473\u0475\u0477\u0479\u047B\u047D\u047F\u0481\u048B\u048D\u048F\u0491\u0493\u0495\u0497\u0499\u049B\u049D\u049F\u04A1\u04A3\u04A5\u04A7\u04A9\u04AB\u04AD\u04AF\u04B1\u04B3\u04B5\u04B7\u04B9\u04BB\u04BD\u04BF\u04C2\u04C4\u04C6\u04C8\u04CA\u04CC\u04CE-\u04CF\u04D1\u04D3\u04D5\u04D7\u04D9\u04DB\u04DD\u04DF\u04E1\u04E3\u04E5\u04E7\u04E9\u04EB\u04ED\u04EF\u04F1\u04F3\u04F5\u04F7\u04F9\u04FB\u04FD\u04FF\u0501\u0503\u0505\u0507\u0509\u050B\u050D\u050F\u0511\u0513\u0515\u0517\u0519\u051B\u051D\u051F\u0521\u0523\u0525\u0527\u0529\u052B\u052D\u052F\u0560-\u0588\u10D0-\u10FA\u10FD-\u10FF\u13F8-\u13FD\u1C80-\u1C88\u1D00-\u1D2B\u1D6B-\u1D77\u1D79-\u1D9A\u1E01\u1E03\u1E05\u1E07\u1E09\u1E0B\u1E0D\u1E0F\u1E11\u1E13\u1E15\u1E17\u1E19\u1E1B\u1E1D\u1E1F\u1E21\u1E23\u1E25\u1E27\u1E29\u1E2B\u1E2D\u1E2F\u1E31\u1E33\u1E35\u1E37\u1E39\u1E3B\u1E3D\u1E3F\u1E41\u1E43\u1E45\u1E47\u1E49\u1E4B\u1E4D\u1E4F\u1E51\u1E53\u1E55\u1E57\u1E59\u1E5B\u1E5D\u1E5F\u1E61\u1E63\u1E65\u1E67\u1E69\u1E6B\u1E6D\u1E6F\u1E71\u1E73\u1E75\u1E77\u1E79\u1E7B\u1E7D\u1E7F\u1E81\u1E83\u1E85\u1E87\u1E89\u1E8B\u1E8D\u1E8F\u1E91\u1E93\u1E95-\u1E9D\u1E9F\u1EA1\u1EA3\u1EA5\u1EA7\u1EA9\u1EAB\u1EAD\u1EAF\u1EB1\u1EB3\u1EB5\u1EB7\u1EB9\u1EBB\u1EBD\u1EBF\u1EC1\u1EC3\u1EC5\u1EC7\u1EC9\u1ECB\u1ECD\u1ECF\u1ED1\u1ED3\u1ED5\u1ED7\u1ED9\u1EDB\u1EDD\u1EDF\u1EE1\u1EE3\u1EE5\u1EE7\u1EE9\u1EEB\u1EED\u1EEF\u1EF1\u1EF3\u1EF5\u1EF7\u1EF9\u1EFB\u1EFD\u1EFF-\u1F07\u1F10-\u1F15\u1F20-\u1F27\u1F30-\u1F37\u1F40-\u1F45\u1F50-\u1F57\u1F60-\u1F67\u1F70-\u1F7D\u1F80-\u1F87\u1F90-\u1F97\u1FA0-\u1FA7\u1FB0-\u1FB4\u1FB6-\u1FB7\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FC7\u1FD0-\u1FD3\u1FD6-\u1FD7\u1FE0-\u1FE7\u1FF2-\u1FF4\u1FF6-\u1FF7\u210A\u210E-\u210F\u2113\u212F\u2134\u2139\u213C-\u213D\u2146-\u2149\u214E\u2184\u2C30-\u2C5E\u2C61\u2C65-\u2C66\u2C68\u2C6A\u2C6C\u2C71\u2C73-\u2C74\u2C76-\u2C7B\u2C81\u2C83\u2C85\u2C87\u2C89\u2C8B\u2C8D\u2C8F\u2C91\u2C93\u2C95\u2C97\u2C99\u2C9B\u2C9D\u2C9F\u2CA1\u2CA3\u2CA5\u2CA7\u2CA9\u2CAB\u2CAD\u2CAF\u2CB1\u2CB3\u2CB5\u2CB7\u2CB9\u2CBB\u2CBD\u2CBF\u2CC1\u2CC3\u2CC5\u2CC7\u2CC9\u2CCB\u2CCD\u2CCF\u2CD1\u2CD3\u2CD5\u2CD7\u2CD9\u2CDB\u2CDD\u2CDF\u2CE1\u2CE3-\u2CE4\u2CEC\u2CEE\u2CF3\u2D00-\u2D25\u2D27\u2D2D\uA641\uA643\uA645\uA647\uA649\uA64B\uA64D\uA64F\uA651\uA653\uA655\uA657\uA659\uA65B\uA65D\uA65F\uA661\uA663\uA665\uA667\uA669\uA66B\uA66D\uA681\uA683\uA685\uA687\uA689\uA68B\uA68D\uA68F\uA691\uA693\uA695\uA697\uA699\uA69B\uA723\uA725\uA727\uA729\uA72B\uA72D\uA72F-\uA731\uA733\uA735\uA737\uA739\uA73B\uA73D\uA73F\uA741\uA743\uA745\uA747\uA749\uA74B\uA74D\uA74F\uA751\uA753\uA755\uA757\uA759\uA75B\uA75D\uA75F\uA761\uA763\uA765\uA767\uA769\uA76B\uA76D\uA76F\uA771-\uA778\uA77A\uA77C\uA77F\uA781\uA783\uA785\uA787\uA78C\uA78E\uA791\uA793-\uA795\uA797\uA799\uA79B\uA79D\uA79F\uA7A1\uA7A3\uA7A5\uA7A7\uA7A9\uA7AF\uA7B5\uA7B7\uA7B9\uA7FA\uAB30-\uAB5A\uAB60-\uAB65\uAB70-\uABBF\uFB00-\uFB06\uFB13-\uFB17\uFF41-\uFF5A]/;
@ -238,7 +238,7 @@ function peg$parse(input, options) {
var peg$e6 = peg$literalExpectation("0", false);
var peg$e7 = peg$classExpectation([["0", "9"]], false, false);
var peg$e8 = peg$otherExpectation("whitespace");
var peg$e9 = peg$classExpectation([" ", "\n", "\r", "\t"], false, false);
var peg$e9 = peg$classExpectation([" ", "\n", "\r", "\t", "\xA0"], false, false);
var peg$e10 = peg$literalExpectation(",", false);
var peg$e11 = peg$literalExpectation("|", false);
var peg$e12 = peg$literalExpectation("\"", false);

View file

@ -95,7 +95,7 @@ DIGIT = [0-9]
// ------------------ delimiters ---------------------------
ws "whitespace" = [ \n\r\t]*
ws "whitespace" = [ \n\r\t\u00A0]*
comma = ws "," ws
pipe = ws "|" ws
quote = '"' / "'"

View file

@ -42,7 +42,7 @@ const applyOptions = (parent, enter) => (pat, i) => {
}
case 'tail': {
const friend = enter(op.arguments_.element);
pat = pat.fmap((a) => (b) => Array.isArray(a) ? [...a, b] : [a, b]).appLeft(friend);
pat = pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend);
break;
}
case 'range': {
@ -160,11 +160,19 @@ export const getLeafLocation = (code, leaf, globalOffset = 0) => {
};
// takes quoted mini string, returns ast
export const mini2ast = (code) => krill.parse(code);
export const mini2ast = (code, start, userCode) => {
try {
return krill.parse(code);
} catch (error) {
const region = [error.location.start.offset + start, error.location.end.offset + start];
const line = userCode.slice(0, region[0]).split('\n').length;
throw new Error(`[mini] parse error at line ${line}: ${error.message}`);
}
};
// takes quoted mini string, returns all nodes that are leaves
export const getLeaves = (code) => {
const ast = mini2ast(code);
export const getLeaves = (code, start, userCode) => {
const ast = mini2ast(code, start, userCode);
let leaves = [];
patternifyAST(
ast,
@ -180,8 +188,8 @@ export const getLeaves = (code) => {
};
// takes quoted mini string, returns locations [fromCol,toCol] of all leaf nodes
export const getLeafLocations = (code, offset = 0) => {
return getLeaves(code).map((l) => getLeafLocation(code, l, offset));
export const getLeafLocations = (code, start = 0, userCode) => {
return getLeaves(code, start, userCode).map((l) => getLeafLocation(code, l, start));
};
// mini notation only (wraps in "")

View file

@ -36,7 +36,7 @@
},
"devDependencies": {
"peggy": "^3.0.2",
"vite": "^4.3.3",
"vitest": "^0.33.0"
"vite": "^5.0.10",
"vitest": "^1.1.0"
}
}

View file

@ -9,7 +9,7 @@ export default defineConfig({
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],

View file

@ -41,6 +41,6 @@
},
"devDependencies": {
"pkg": "^5.8.1",
"vite": "^4.3.3"
"vite": "^5.0.10"
}
}

View file

@ -9,7 +9,7 @@ export default defineConfig({
lib: {
entry: resolve(__dirname, 'osc.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],

View file

@ -33,7 +33,6 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@rollup/plugin-replace": "^5.0.5",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/midi": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
@ -42,10 +41,11 @@
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel/codemirror": "workspace:*",
"@strudel/hydra": "workspace:*",
"rollup-plugin-visualizer": "^5.8.1"
"@strudel/hydra": "workspace:*"
},
"devDependencies": {
"vite": "^4.3.3"
"@rollup/plugin-replace": "^5.0.5",
"rollup-plugin-visualizer": "^5.12.0",
"vite": "^5.0.10"
}
}

View file

@ -12,7 +12,7 @@ export default defineConfig({
entry: resolve(__dirname, 'index.mjs'),
name: 'strudel',
formats: ['es', 'iife'],
fileName: (ext) => ({ es: 'index.mjs', iife: 'index.js' }[ext]),
fileName: (ext) => ({ es: 'index.mjs', iife: 'index.js' })[ext],
},
rollupOptions: {
// external: [...Object.keys(dependencies)],

View file

@ -32,6 +32,6 @@
"@strudel.cycles/core": "workspace:*"
},
"devDependencies": {
"vite": "^4.3.3"
"vite": "^5.0.10"
}
}

View file

@ -9,7 +9,7 @@ export default defineConfig({
lib: {
entry: resolve(__dirname, 'serial.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],

View file

@ -1,4 +1,5 @@
import { noteToMidi, freqToMidi } from '@strudel.cycles/core';
import { noteToMidi, freqToMidi, getSoundIndex } from '@strudel.cycles/core';
import { getAudioContext, registerSound, getEnvelope, getADSRValues } from '@strudel.cycles/webaudio';
import gm from './gm.mjs';
@ -130,14 +131,14 @@ export function registerSoundfonts() {
registerSound(
name,
async (time, value, onended) => {
const { n = 0 } = value;
const [attack, decay, sustain, release] = getADSRValues([
value.attack,
value.decay,
value.sustain,
value.release,
]);
const font = fonts[n % fonts.length];
const n = getSoundIndex(value.n, fonts.length);
const font = fonts[n];
const ctx = getAudioContext();
const bufferSource = await getFontBufferSource(font, value, ctx);
bufferSource.start(time);

View file

@ -36,7 +36,7 @@
"soundfont2": "^0.4.0"
},
"devDependencies": {
"node-fetch": "^3.3.1",
"vite": "^4.3.3"
"node-fetch": "^3.3.2",
"vite": "^5.0.10"
}
}

View file

@ -9,7 +9,7 @@ export default defineConfig({
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],

View file

@ -33,9 +33,9 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"devDependencies": {
"vite": "^4.3.3"
"vite": "^5.0.10"
},
"dependencies": {
"nanostores": "^0.8.1"
"nanostores": "^0.9.5"
}
}

View file

@ -1,4 +1,4 @@
import { noteToMidi, valueToMidi, nanFallback } from './util.mjs';
import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs';
import { getAudioContext, registerSound } from './index.mjs';
import { getADSRValues, getEnvelope } from './helpers.mjs';
import { logger } from './logger.mjs';
@ -31,10 +31,12 @@ export const getSampleBufferSource = async (s, n, note, speed, freq, bank, resol
transpose = midi - 36; // C3 is middle C
const ac = getAudioContext();
let sampleUrl;
let index = 0;
if (Array.isArray(bank)) {
n = nanFallback(n, 0);
sampleUrl = bank[n % bank.length];
index = getSoundIndex(n, bank.length);
sampleUrl = bank[index];
} else {
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
// object format will expect keys as notes
@ -45,12 +47,13 @@ export const getSampleBufferSource = async (s, n, note, speed, freq, bank, resol
null,
);
transpose = -midiDiff(closest); // semitones to repitch
sampleUrl = bank[closest][n % bank[closest].length];
index = getSoundIndex(n, bank[closest].length);
sampleUrl = bank[closest][index];
}
if (resolveUrl) {
sampleUrl = await resolveUrl(sampleUrl);
}
let buffer = await loadBuffer(sampleUrl, ac, s, n);
let buffer = await loadBuffer(sampleUrl, ac, s, index);
if (speed < 0) {
// should this be cached?
buffer = reverseBuffer(buffer);

View file

@ -28,11 +28,13 @@ export const resetLoadedSounds = () => soundMap.set({});
let audioContext;
export const setDefaultAudioContext = () => {
audioContext = new AudioContext();
};
export const getAudioContext = () => {
if (!audioContext) {
audioContext = new AudioContext();
const maxChannelCount = audioContext.destination.maxChannelCount;
audioContext.destination.channelCount = maxChannelCount;
setDefaultAudioContext();
}
return audioContext;
};
@ -84,15 +86,22 @@ let delays = {};
const maxfeedback = 0.98;
let channelMerger, destinationGain;
//update the output channel configuration to match user's audio device
export function initializeAudioOutput() {
const audioContext = getAudioContext();
const maxChannelCount = audioContext.destination.maxChannelCount;
audioContext.destination.channelCount = maxChannelCount;
channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount });
destinationGain = new GainNode(audioContext);
channelMerger.connect(destinationGain);
destinationGain.connect(audioContext.destination);
}
// input: AudioNode, channels: ?Array<int>
export const connectToDestination = (input, channels = [0, 1]) => {
const ctx = getAudioContext();
if (channelMerger == null) {
channelMerger = new ChannelMergerNode(ctx, { numberOfInputs: ctx.destination.channelCount });
destinationGain = new GainNode(ctx);
channelMerger.connect(destinationGain);
destinationGain.connect(ctx.destination);
initializeAudioOutput();
}
//This upmix can be removed if correct channel counts are set throughout the app,
// and then strudel could theoretically support surround sound audio files
@ -114,6 +123,7 @@ export const panic = () => {
}
destinationGain.gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01);
destinationGain = null;
channelMerger == null;
};
function getDelay(orbit, delaytime, delayfeedback, t) {

View file

@ -61,3 +61,10 @@ export function nanFallback(value, fallback) {
}
return value;
}
// modulo that works with negative numbers e.g. _mod(-1, 3) = 2. Works on numbers (rather than patterns of numbers, as @mod@ from pattern.mjs does)
export const _mod = (n, m) => ((n % m) + m) % m;
// round to nearest int, negative numbers will output a subtracted index
export const getSoundIndex = (n, numSounds) => {
return _mod(Math.round(nanFallback(n, 0)), numSounds);
};

View file

@ -9,7 +9,7 @@ export default defineConfig({
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.cjs' }[ext]),
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.cjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],

View file

@ -78,13 +78,13 @@ export function buildSamples(
(i < attack
? i / attack // attack
: i < attack + decay // decay
? 1 - ((i - attack) / decay) * (1 - sustainVolume) // decay falloff
: i < attack + decay + sustain // sustain
? sustainVolume // sustain volume
: i < length - delay // release
? ((length - i - delay) / release) * // release falloff
sustainVolume // release volume
: 0); // post release
? 1 - ((i - attack) / decay) * (1 - sustainVolume) // decay falloff
: i < attack + decay + sustain // sustain
? sustainVolume // sustain volume
: i < length - delay // release
? ((length - i - delay) / release) * // release falloff
sustainVolume // release volume
: 0); // post release
s = delay
? s / 2 +

View file

@ -34,10 +34,10 @@
"@strudel.cycles/core": "workspace:*",
"@tonaljs/tonal": "^4.7.2",
"chord-voicings": "^0.0.1",
"webmidi": "^3.1.5"
"webmidi": "^3.1.8"
},
"devDependencies": {
"vite": "^4.3.3",
"vitest": "^0.33.0"
"vite": "^5.0.10",
"vitest": "^1.1.0"
}
}

View file

@ -5,14 +5,20 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { Note, Interval, Scale } from '@tonaljs/tonal';
import { register, _mod } from '@strudel.cycles/core';
import { register, _mod, silence, logger, pure, isNote } from '@strudel.cycles/core';
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
function scaleStep(step, scale) {
scale = scale.replaceAll(':', ' ');
step = Math.ceil(step);
const { intervals, tonic } = Scale.get(scale);
let { intervals, tonic, empty } = Scale.get(scale);
if ((empty && isNote(scale)) || (!empty && !tonic)) {
throw new Error(`incomplete scale. Make sure to use ":" instead of spaces, example: .scale("C:major")`);
} else if (empty) {
throw new Error(`invalid scale "${scale}"`);
}
tonic = tonic || 'C';
const { pc, oct = 3 } = Note.get(tonic);
const octaveOffset = Math.floor(step / intervals.length);
const scaleStep = _mod(step, intervals.length);
@ -160,16 +166,34 @@ export const scale = register('scale', function (scale, pat) {
if (Array.isArray(scale)) {
scale = scale.flat().join(' ');
}
return pat.withHap((hap) => {
const isObject = typeof hap.value === 'object';
let note = isObject ? hap.value.n : hap.value;
if (isObject) {
delete hap.value.n; // remove n so it won't cause trouble
}
const asNumber = Number(note);
if (!isNaN(asNumber)) {
note = scaleStep(asNumber, scale);
}
return hap.withValue(() => (isObject ? { ...hap.value, note } : note)).setContext({ ...hap.context, scale });
});
return (
pat
.fmap((value) => {
const isObject = typeof value === 'object';
let step = isObject ? value.n : value;
if (isObject) {
delete value.n; // remove n so it won't cause trouble
}
if (isNote(step)) {
// legacy..
return pure(step);
}
const asNumber = Number(step);
if (isNaN(asNumber)) {
logger(`[tonal] invalid scale step "${step}", expected number`, 'error');
return silence;
}
try {
const note = scaleStep(asNumber, scale);
value = pure(isObject ? { ...value, note } : note);
} catch (err) {
logger(`[tonal] ${err.message}`, 'error');
value = silence;
}
return value;
})
.outerJoin()
// legacy:
.withHap((hap) => hap.setContext({ ...hap.context, scale }))
);
});

View file

@ -9,7 +9,7 @@ export default defineConfig({
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],

View file

@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
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/>.
*/
import { stack, register } from '@strudel.cycles/core';
import { stack, register, silence, logger } from '@strudel.cycles/core';
import { renderVoicing } from './tonleiter.mjs';
import _voicings from 'chord-voicings';
import { complex, simple } from './ireal.mjs';
@ -184,12 +184,9 @@ export const rootNotes = register('rootNotes', function (octave, pat) {
* If you pass a pattern of strings to voicing, they will be interpreted as chords.
*
* @name voicing
* @param {string} dictionary which voicing dictionary to use.
* @returns Pattern
* @example
* voicing("<C Am F G>")
* @example
* n("0 1 2 3 4 5 6 7").chord("<C Am F G>").voicing()
* n("0 1 2 3").chord("<C Am F G>").voicing()
*/
export const voicing = register('voicing', function (pat) {
return pat
@ -199,11 +196,15 @@ export const voicing = register('voicing', function (pat) {
let { dictionary = 'default', chord, anchor, offset, mode, n, octaves, ...rest } = value;
dictionary =
typeof dictionary === 'string' ? voicingRegistry[dictionary] : { dictionary, mode: 'below', anchor: 'c5' };
let notes = renderVoicing({ ...dictionary, chord, anchor, offset, mode, n, octaves });
return stack(...notes)
.note()
.set(rest); // rest does not include voicing controls anymore!
try {
let notes = renderVoicing({ ...dictionary, chord, anchor, offset, mode, n, octaves });
return stack(...notes)
.note()
.set(rest); // rest does not include voicing controls anymore!
} catch (err) {
logger(`[voicing]: unknown chord "${chord}"`);
return silence;
}
})
.outerJoin();
});

View file

@ -32,12 +32,12 @@
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"acorn": "^8.8.1",
"escodegen": "^2.0.0",
"acorn": "^8.11.3",
"escodegen": "^2.1.0",
"estree-walker": "^3.0.1"
},
"devDependencies": {
"vite": "^4.3.3",
"vitest": "^0.33.0"
"vite": "^5.0.10",
"vitest": "^1.1.0"
}
}

View file

@ -15,7 +15,7 @@ export function transpiler(input, options = {}) {
let miniLocations = [];
const collectMiniLocations = (value, node) => {
const leafLocs = getLeafLocations(`"${value}"`, node.start); // stimmt!
const leafLocs = getLeafLocations(`"${value}"`, node.start, input);
miniLocations = miniLocations.concat(leafLocs);
};
let widgets = [];

View file

@ -9,7 +9,7 @@ export default defineConfig({
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],

View file

@ -40,6 +40,6 @@
"@strudel.cycles/webaudio": "workspace:*"
},
"devDependencies": {
"vite": "^4.3.3"
"vite": "^5.0.10"
}
}

View file

@ -9,7 +9,7 @@ export default defineConfig({
lib: {
entry: resolve(__dirname, 'web.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],

View file

@ -38,6 +38,6 @@
"superdough": "workspace:*"
},
"devDependencies": {
"vite": "^4.3.3"
"vite": "^5.0.10"
}
}

View file

@ -9,7 +9,7 @@ export default defineConfig({
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],

View file

@ -33,7 +33,7 @@
"@strudel.cycles/core": "workspace:*"
},
"devDependencies": {
"vite": "^4.3.3",
"vitest": "^0.33.0"
"vite": "^5.0.10",
"vitest": "^1.1.0"
}
}

View file

@ -9,7 +9,7 @@ export default defineConfig({
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],