diff --git a/.eslintignore b/.eslintignore index 92c95c85..7d807b65 100644 --- a/.eslintignore +++ b/.eslintignore @@ -20,4 +20,5 @@ vite.config.js **/dist /src-tauri/target/**/* reverbGen.mjs -hydra.mjs \ No newline at end of file +hydra.mjs +jsdoc-synonyms.js \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 62e91d36..256a1772 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,10 +21,10 @@ jobs: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: pnpm/action-setup@v2 with: - version: 7 + version: 8.11.0 - uses: actions/setup-node@v3 with: node-version: 18 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 849db7d3..0f6c1181 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,10 +10,10 @@ jobs: node-version: [18] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: pnpm/action-setup@v2 with: - version: 7 + version: 8.11.0 - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} diff --git a/.gitignore b/.gitignore index fe7f9e56..b7704c61 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,87 @@ dev-dist Dirt-Samples tidal-drum-machines webaudiofontdata -src-tauri/target \ No newline at end of file +src-tauri/target + +# BEGIN JetBrains -> END JetBrains + +# for JetBrains IDE users, e.g. WebStorm. Source: https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# END JetBrains -> BEGIN JetBrains diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 467a9391..556df3fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -66,7 +66,7 @@ To get the project up and running for development, make sure you have installed: - [git](https://git-scm.com/) - [node](https://nodejs.org/en/) >= 18 -- [pnpm](https://pnpm.io/) (`npm i pnpm -g`) +- [pnpm](https://pnpm.io/) (`curl -fsSL https://get.pnpm.io/install.sh | env PNPM_VERSION=8.11.0 sh -`) then, do the following: diff --git a/README.md b/README.md index 037a9162..7d96cb20 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,6 @@ There are multiple npm packages you can use to use strudel, or only parts of it, - [`midi`](./packages/midi): webmidi bindings - [`serial`](./packages/serial): webserial bindings - [`tonal`](./packages/tonal): tonal functions -- [`xen`](./packages/xen): microtonal / xenharmonic functions - ... [and there are more](./packages/) Click on the package names to find out more about each one. diff --git a/jsdoc/jsdoc-synonyms.js b/jsdoc/jsdoc-synonyms.js new file mode 100644 index 00000000..09190846 --- /dev/null +++ b/jsdoc/jsdoc-synonyms.js @@ -0,0 +1,17 @@ +/* +jsdoc-synonyms.js - Add support for @synonym tag +Copyright (C) 2023 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 . +*/ + +function defineTags(dictionary) { + dictionary.defineTag('synonyms', { + mustHaveValue: true, + onTagged: function (doclet, tag) { + doclet.synonyms_text = tag.value; + doclet.synonyms = doclet.synonyms_text.split(/[ ,]+/); + }, + }); +} + +module.exports = { defineTags: defineTags }; diff --git a/jsdoc.config.json b/jsdoc/jsdoc.config.json similarity index 78% rename from jsdoc.config.json rename to jsdoc/jsdoc.config.json index ca9c3d81..073b16c5 100644 --- a/jsdoc.config.json +++ b/jsdoc/jsdoc.config.json @@ -3,7 +3,7 @@ "includePattern": ".+\\.(js(doc|x)?|mjs)$", "excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser|dist" }, - "plugins": ["plugins/markdown"], + "plugins": ["plugins/markdown", "jsdoc/jsdoc-synonyms"], "opts": { "destination": "./out/", "recurse": true diff --git a/undocumented.mjs b/jsdoc/undocumented.mjs similarity index 91% rename from undocumented.mjs rename to jsdoc/undocumented.mjs index 281d27d8..9376a8b6 100644 --- a/undocumented.mjs +++ b/jsdoc/undocumented.mjs @@ -65,7 +65,7 @@ async function getUndocumented(path, docs) { } // read doc.json file -const { docs } = JSON.parse(await readFile(resolve(__dirname, 'doc.json'), 'utf8')); +const { docs } = JSON.parse(await readFile(resolve(__dirname, '..', 'doc.json'), 'utf8')); const paths = dependencyTree.toList({ filename: 'index.mjs', @@ -76,7 +76,9 @@ const paths = dependencyTree.toList({ // const paths = ['../packages/core/pattern.mjs', '../packages/core/hap.mjs'].map((rel) => resolve(__dirname, rel)); const undocumented = Object.fromEntries( - await Promise.all(paths.map(async (path) => [path, await getUndocumented(path, docs)])), + await Promise.all( + paths.map(async (path) => [path.replace(resolve(__dirname, '..'), ''), await getUndocumented(path, docs)]), + ), ); console.log(JSON.stringify(undocumented, null, 2)); diff --git a/package.json b/package.json index 36229611..172832ee 100644 --- a/package.json +++ b/package.json @@ -18,12 +18,12 @@ "build": "npm run prebuild && cd website && npm run build", "preview": "cd website && npm run preview", "osc": "cd packages/osc && npm run server", - "jsdoc": "jsdoc packages/ -c jsdoc.config.json", - "jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json", + "jsdoc": "jsdoc packages/ -c jsdoc/jsdoc.config.json", + "jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc/jsdoc.config.json", "lint": "eslint . --ext mjs,js --quiet", "codeformat": "prettier --write .", "format-check": "prettier --check .", - "report-undocumented": "npm run jsdoc-json && node undocumented.mjs > undocumented.json", + "report-undocumented": "npm run jsdoc-json && node jsdoc/undocumented.mjs > undocumented.json", "check": "npm run format-check && npm run lint && npm run test", "iclc": "cd paper && pandoc --template=pandoc/iclc.html --citeproc --number-sections iclc2023.md -o iclc2023.html && pandoc --template=pandoc/iclc.latex --citeproc --number-sections iclc2023.md -o iclc2023.pdf" }, diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index a03658a8..d979ff23 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -381,6 +381,19 @@ const generic_params = [ */ ['coarse'], + /** + * Allows you to set the output channels on the interface + * + * @name channels + * @synonyms ch + * + * @param {number | Pattern} channels pattern the output channels + * @example + * note("e a d b g").channels("3:4") + * + */ + ['channels', 'ch'], + ['phaserrate', 'phasr'], // superdirt only /** @@ -1214,6 +1227,16 @@ const generic_params = [ * @name waveloss */ ['waveloss'], + /* + * Noise crackle density + * + * @name density + * @param {number | Pattern} density between 0 and x + * @example + * s("crackle*4").density("<0.01 0.04 0.2 0.5>".slow(4)) + * + */ + ['density'], // TODO: midi effects? ['dur'], // ['modwheel'], diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 5a9a3fe8..f6969a73 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1985,7 +1985,7 @@ Pattern.prototype.hush = function () { * note("c d e g").palindrome() */ export const palindrome = register('palindrome', function (pat) { - return pat.every(2, rev); + return pat.lastOf(2, rev); }); /** @@ -2189,6 +2189,14 @@ export const duration = register('duration', function (value, pat) { return pat.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(value))); }); +export const hsla = register('hsla', (h, s, l, a, pat) => { + return pat.color(`hsla(${h}turn,${s * 100}%,${l * 100}%,${a})`); +}); + +export const hsl = register('hsl', (h, s, l, pat) => { + return pat.color(`hsl(${h}turn,${s * 100}%,${l * 100}%)`); +}); + /** * Sets the color of the hap in visualizations like pianoroll or highlighting. * @name color diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 3645d594..0bd5229d 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -89,19 +89,22 @@ export function repl({ allTransform = transform; return silence; }; - - for (let i = 1; i < 10; ++i) { - Object.defineProperty(Pattern.prototype, `d${i}`, { - get() { - return this.p(i); - }, - }); - Object.defineProperty(Pattern.prototype, `p${i}`, { - get() { - return this.p(i); - }, - }); - Pattern.prototype[`q${i}`] = silence; + try { + for (let i = 1; i < 10; ++i) { + Object.defineProperty(Pattern.prototype, `d${i}`, { + get() { + return this.p(i); + }, + }); + Object.defineProperty(Pattern.prototype, `p${i}`, { + get() { + return this.p(i); + }, + }); + Pattern.prototype[`q${i}`] = silence; + } + } catch (err) { + // already defined.. } const fit = register('fit', (pat) => diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index f4a03110..1c849499 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -20,6 +20,7 @@ import { slowcat, cat, sequence, + palindrome, polymeter, polymeterSteps, polyrhythm, @@ -571,6 +572,18 @@ describe('Pattern', () => { expect(sequence(1, 2, 3).firstCycle()).toStrictEqual(fastcat(1, 2, 3).firstCycle()); }); }); + describe('palindrome()', () => { + it('Can create palindrome', () => { + expect( + fastcat('a', 'b', 'c') + .palindrome() + .fast(2) + .firstCycle() + .sort((a, b) => a.part.begin.sub(b.part.begin)) + .map((a) => a.value), + ).toStrictEqual(['a', 'b', 'c', 'c', 'b', 'a']); + }); + }); describe('polyrhythm()', () => { it('Can layer up cycles', () => { expect(polyrhythm(['a', 'b'], ['c']).firstCycle()).toStrictEqual( diff --git a/packages/hydra/README.md b/packages/hydra/README.md index d300bb88..0b238692 100644 --- a/packages/hydra/README.md +++ b/packages/hydra/README.md @@ -12,6 +12,12 @@ await initHydra(); Then you can use hydra below! +### options + +You can also pass options to the `initHydra` function. These can be used to set [hydra options](https://github.com/hydra-synth/hydra-synth#api) + these strudel specific options: + +- `feedStrudel`: sends the strudel canvas to `s0`. The strudel canvas is used to draw `pianoroll`, `spiral`, `scope` etc.. + ## Usage via npm ```sh diff --git a/packages/hydra/hydra.mjs b/packages/hydra/hydra.mjs index 692e4c14..97ca5ac4 100644 --- a/packages/hydra/hydra.mjs +++ b/packages/hydra/hydra.mjs @@ -1,14 +1,38 @@ import { getDrawContext } from '@strudel.cycles/core'; -export async function initHydra() { +let latestOptions; + +function appendCanvas(c) { + const { canvas: testCanvas } = getDrawContext(); + c.canvas.id = 'hydra-canvas'; + c.canvas.style.position = 'fixed'; + c.canvas.style.top = '0px'; + testCanvas.after(c.canvas); + return testCanvas; +} + +export async function initHydra(options = {}) { + // reset if options have changed since last init + if (latestOptions && JSON.stringify(latestOptions) !== JSON.stringify(options)) { + document.getElementById('hydra-canvas').remove(); + } + latestOptions = options; + //load and init hydra if (!document.getElementById('hydra-canvas')) { - const { canvas: testCanvas } = getDrawContext(); - await import('https://unpkg.com/hydra-synth'); - const hydraCanvas = testCanvas.cloneNode(true); - hydraCanvas.id = 'hydra-canvas'; - testCanvas.after(hydraCanvas); - new Hydra({ canvas: hydraCanvas, detectAudio: false }); - s0.init({ src: testCanvas }); + console.log('reinit..'); + const { + src = 'https://unpkg.com/hydra-synth', + feedStrudel = false, + ...hydraConfig + } = { detectAudio: false, ...options }; + await import(src); + const hydra = new Hydra(hydraConfig); + if (feedStrudel) { + const { canvas } = getDrawContext(); + canvas.style.display = 'none'; + hydra.synth.s0.init({ src: canvas }); + } + appendCanvas(hydra); } } diff --git a/packages/react/src/components/Autocomplete.jsx b/packages/react/src/components/Autocomplete.jsx index 677baa6e..9a0b9de4 100644 --- a/packages/react/src/components/Autocomplete.jsx +++ b/packages/react/src/components/Autocomplete.jsx @@ -2,16 +2,23 @@ import { createRoot } from 'react-dom/client'; import jsdoc from '../../../../doc.json'; const getDocLabel = (doc) => doc.name || doc.longname; +const getDocSynonyms = (doc) => [getDocLabel(doc), ...(doc.synonyms || [])]; const getInnerText = (html) => { var div = document.createElement('div'); div.innerHTML = html; return div.textContent || div.innerText || ''; }; -export function Autocomplete({ doc }) { +export function Autocomplete({ doc, label = getDocLabel(doc) }) { + const synonyms = getDocSynonyms(doc).filter((a) => a !== label); return (
-

{getDocLabel(doc)}

+

{label}

{' '} + {!!synonyms.length && ( + + Synonyms: {synonyms.join(', ')} + + )}
    {doc.params?.map(({ name, type, description }, i) => ( @@ -48,18 +55,24 @@ const jsdocCompletions = jsdoc.docs !['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)), ) // https://codemirror.net/docs/ref/#autocomplete.Completion - .map((doc) /*: Completion */ => ({ - label: getDocLabel(doc), - // detail: 'xxx', // An optional short piece of information to show (with a different style) after the label. - info: () => { - const node = document.createElement('div'); - // if Autocomplete is non-interactive, it could also be rendered at build time.. - // .. using renderToStaticMarkup - createRoot(node).render(); - return node; - }, - type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type - })); + .reduce( + (acc, doc) /*: Completion */ => + acc.concat( + [getDocLabel(doc), ...(doc.synonyms || [])].map((label) => ({ + label, + // detail: 'xxx', // An optional short piece of information to show (with a different style) after the label. + info: () => { + const node = document.createElement('div'); + // if Autocomplete is non-interactive, it could also be rendered at build time.. + // .. using renderToStaticMarkup + createRoot(node).render(); + return node; + }, + type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type + })), + ), + [], + ); export const strudelAutocomplete = (context /* : CompletionContext */) => { let word = context.matchBefore(/\w*/); diff --git a/packages/react/src/components/Tooltip.jsx b/packages/react/src/components/Tooltip.jsx index a443c123..43a53476 100644 --- a/packages/react/src/components/Tooltip.jsx +++ b/packages/react/src/components/Tooltip.jsx @@ -31,6 +31,9 @@ window.addEventListener( export const strudelTooltip = hoverTooltip( (view, pos, side) => { // Word selection from CodeMirror Hover Tooltip example https://codemirror.net/examples/tooltip/#hover-tooltips + if (!ctrlDown) { + return null; + } let { from, to, text } = view.state.doc.lineAt(pos); let start = pos, end = pos; @@ -47,11 +50,13 @@ export const strudelTooltip = hoverTooltip( // Get entry from Strudel documentation let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0]; if (!entry) { - return null; - } - if (!ctrlDown) { - return null; + // Try for synonyms + entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0]; + if (!entry) { + return null; + } } + return { pos: start, end, @@ -60,7 +65,7 @@ export const strudelTooltip = hoverTooltip( create(view) { let dom = document.createElement('div'); dom.className = 'strudel-tooltip'; - createRoot(dom).render(); + createRoot(dom).render(); return { dom }; }, }; diff --git a/packages/superdough/noise.mjs b/packages/superdough/noise.mjs index 2c8c1d4a..24779470 100644 --- a/packages/superdough/noise.mjs +++ b/packages/superdough/noise.mjs @@ -4,7 +4,7 @@ import { getAudioContext } from './superdough.mjs'; let noiseCache = {}; // lazy generates noise buffers and keeps them forever -function getNoiseBuffer(type) { +function getNoiseBuffer(type, density) { const ac = getAudioContext(); if (noiseCache[type]) { return noiseCache[type]; @@ -34,17 +34,26 @@ function getNoiseBuffer(type) { output[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362; output[i] *= 0.11; b6 = white * 0.115926; + } else if (type === 'crackle') { + const probability = density * 0.01; + if (Math.random() < probability) { + output[i] = Math.random() * 2 - 1; + } else { + output[i] = 0; + } } } - noiseCache[type] = noiseBuffer; + + // Prevent caching to randomize crackles + if (type !== 'crackle') noiseCache[type] = noiseBuffer; return noiseBuffer; } // expects one of noises as type -export function getNoiseOscillator(type = 'white', t) { +export function getNoiseOscillator(type = 'white', t, density = 0.02) { const ac = getAudioContext(); const o = ac.createBufferSource(); - o.buffer = getNoiseBuffer(type); + o.buffer = getNoiseBuffer(type, density); o.loop = true; o.start(t); return { diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 270915fd..3be97615 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -27,28 +27,16 @@ export function getSound(s) { export const resetLoadedSounds = () => soundMap.set({}); let audioContext; + export const getAudioContext = () => { if (!audioContext) { audioContext = new AudioContext(); + const maxChannelCount = audioContext.destination.maxChannelCount; + audioContext.destination.channelCount = maxChannelCount; } return audioContext; }; -let destination; -const getDestination = () => { - const ctx = getAudioContext(); - if (!destination) { - destination = ctx.createGain(); - destination.connect(ctx.destination); - } - return destination; -}; - -export const panic = () => { - getDestination().gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01); - destination = null; -}; - let workletsLoading; function loadWorklets() { @@ -95,6 +83,39 @@ export async function initAudioOnFirstClick(options) { let delays = {}; const maxfeedback = 0.98; +let channelMerger, destinationGain; + +// input: AudioNode, channels: ?Array +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); + } + //This upmix can be removed if correct channel counts are set throughout the app, + // and then strudel could theoretically support surround sound audio files + const stereoMix = new StereoPannerNode(ctx); + input.connect(stereoMix); + + const splitter = new ChannelSplitterNode(ctx, { + numberOfOutputs: stereoMix.channelCount, + }); + stereoMix.connect(splitter); + channels.forEach((ch, i) => { + splitter.connect(channelMerger, i % stereoMix.channelCount, clamp(ch, 0, ctx.destination.channelCount - 1)); + }); +}; + +export const panic = () => { + if (destinationGain == null) { + return; + } + destinationGain.gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01); + destinationGain = null; +}; + function getDelay(orbit, delaytime, delayfeedback, t) { if (delayfeedback > maxfeedback) { //logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`); @@ -104,7 +125,7 @@ function getDelay(orbit, delaytime, delayfeedback, t) { const ac = getAudioContext(); const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback); dly.start?.(t); // for some reason, this throws when audion extension is installed.. - dly.connect(getDestination()); + connectToDestination(dly, [0, 1]); delays[orbit] = dly; } delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t); @@ -163,7 +184,7 @@ function getReverb(orbit, duration, fade, lp, dim, ir) { if (!reverbs[orbit]) { const ac = getAudioContext(); const reverb = ac.createReverb(duration, fade, lp, dim, ir); - reverb.connect(getDestination()); + connectToDestination(reverb, [0, 1]); reverbs[orbit] = reverb; } if ( @@ -241,6 +262,7 @@ export const superdough = async (value, deadline, hapDuration) => { source, gain = 0.8, postgain = 1, + density = 0.03, // filters ftype = '12db', fanchor = 0.5, @@ -268,7 +290,7 @@ export const superdough = async (value, deadline, hapDuration) => { bpsustain = 1, bprelease = 0.01, bandq = 1, - + channels = [1, 2], //phaser phaser, phaserdepth = 0.75, @@ -301,6 +323,10 @@ export const superdough = async (value, deadline, hapDuration) => { compressorRelease, } = value; gain = nanFallback(gain, 1); + + //music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior + channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); + gain *= velocity; // legacy fix for velocity let toDisconnect = []; // audio nodes that will be disconnected when the source has ended const onended = () => { @@ -434,9 +460,9 @@ export const superdough = async (value, deadline, hapDuration) => { } // last gain - const post = gainNode(postgain); + const post = new GainNode(ac, { gain: postgain }); chain.push(post); - post.connect(getDestination()); + connectToDestination(post, channels); // delay let delaySend; diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index dafc2e7c..edc3c01e 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -22,7 +22,7 @@ const fm = (osc, harmonicityRatio, modulationIndex, wave = 'sine') => { }; const waveforms = ['sine', 'square', 'triangle', 'sawtooth']; -const noises = ['pink', 'white', 'brown']; +const noises = ['pink', 'white', 'brown', 'crackle']; export function registerSynthSounds() { [...waveforms, ...noises].forEach((s) => { @@ -36,7 +36,8 @@ export function registerSynthSounds() { if (waveforms.includes(s)) { sound = getOscillator(s, t, value); } else { - sound = getNoiseOscillator(s, t); + let { density } = value; + sound = getNoiseOscillator(s, t, density); } let { node: o, stop, triggerRelease } = sound; diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 34f7a850..3d23c106 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1038,6 +1038,31 @@ exports[`runs examples > example "ceil" example index 0 1`] = ` ] `; +exports[`runs examples > example "channels" example index 0 1`] = ` +[ + "[ 0/1 → 1/5 | note:e channels:[3 4] ]", + "[ 1/5 → 2/5 | note:a channels:[3 4] ]", + "[ 2/5 → 3/5 | note:d channels:[3 4] ]", + "[ 3/5 → 4/5 | note:b channels:[3 4] ]", + "[ 4/5 → 1/1 | note:g channels:[3 4] ]", + "[ 1/1 → 6/5 | note:e channels:[3 4] ]", + "[ 6/5 → 7/5 | note:a channels:[3 4] ]", + "[ 7/5 → 8/5 | note:d channels:[3 4] ]", + "[ 8/5 → 9/5 | note:b channels:[3 4] ]", + "[ 9/5 → 2/1 | note:g channels:[3 4] ]", + "[ 2/1 → 11/5 | note:e channels:[3 4] ]", + "[ 11/5 → 12/5 | note:a channels:[3 4] ]", + "[ 12/5 → 13/5 | note:d channels:[3 4] ]", + "[ 13/5 → 14/5 | note:b channels:[3 4] ]", + "[ 14/5 → 3/1 | note:g channels:[3 4] ]", + "[ 3/1 → 16/5 | note:e channels:[3 4] ]", + "[ 16/5 → 17/5 | note:a channels:[3 4] ]", + "[ 17/5 → 18/5 | note:d channels:[3 4] ]", + "[ 18/5 → 19/5 | note:b channels:[3 4] ]", + "[ 19/5 → 4/1 | note:g channels:[3 4] ]", +] +`; + exports[`runs examples > example "chooseCycles" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:bd ]", @@ -3267,22 +3292,22 @@ exports[`runs examples > example "outside" example index 0 1`] = ` exports[`runs examples > example "palindrome" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:g ]", - "[ 1/4 → 1/2 | note:e ]", - "[ 1/2 → 3/4 | note:d ]", - "[ 3/4 → 1/1 | note:c ]", - "[ 1/1 → 5/4 | note:c ]", - "[ 5/4 → 3/2 | note:d ]", - "[ 3/2 → 7/4 | note:e ]", - "[ 7/4 → 2/1 | note:g ]", - "[ 2/1 → 9/4 | note:g ]", - "[ 9/4 → 5/2 | note:e ]", - "[ 5/2 → 11/4 | note:d ]", - "[ 11/4 → 3/1 | note:c ]", - "[ 3/1 → 13/4 | note:c ]", - "[ 13/4 → 7/2 | note:d ]", - "[ 7/2 → 15/4 | note:e ]", - "[ 15/4 → 4/1 | note:g ]", + "[ 0/1 → 1/4 | note:c ]", + "[ 1/4 → 1/2 | note:d ]", + "[ 1/2 → 3/4 | note:e ]", + "[ 3/4 → 1/1 | note:g ]", + "[ 1/1 → 5/4 | note:g ]", + "[ 5/4 → 3/2 | note:e ]", + "[ 3/2 → 7/4 | note:d ]", + "[ 7/4 → 2/1 | note:c ]", + "[ 2/1 → 9/4 | note:c ]", + "[ 9/4 → 5/2 | note:d ]", + "[ 5/2 → 11/4 | note:e ]", + "[ 11/4 → 3/1 | note:g ]", + "[ 3/1 → 13/4 | note:g ]", + "[ 13/4 → 7/2 | note:e ]", + "[ 7/2 → 15/4 | note:d ]", + "[ 15/4 → 4/1 | note:c ]", ] `; diff --git a/test/__snapshots__/tunes.test.mjs.snap b/test/__snapshots__/tunes.test.mjs.snap index 8d696d75..0a341d12 100644 --- a/test/__snapshots__/tunes.test.mjs.snap +++ b/test/__snapshots__/tunes.test.mjs.snap @@ -8372,16 +8372,16 @@ exports[`renders tunes > tune: hyperpop 1`] = ` exports[`renders tunes > tune: juxUndTollerei 1`] = ` [ - "[ 0/1 → 1/4 | note:bb3 s:sawtooth pan:0 cutoff:1188.2154262966046 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:1 cutoff:1188.2154262966046 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:0 cutoff:1361.2562095290161 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:1 cutoff:1361.2562095290161 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:eb3 s:sawtooth pan:0 cutoff:1524.257063143398 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:1 cutoff:1524.257063143398 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", - "[ (101/200 → 1/1) ⇝ 201/200 | note:65 s:triangle pan:0 cutoff:1601.4815730092653 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", - "[ (101/200 → 1/1) ⇝ 201/200 | note:55 s:triangle pan:1 cutoff:1601.4815730092653 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", - "[ 3/4 → 1/1 | note:c3 s:sawtooth pan:0 cutoff:1670.953955747281 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", - "[ 3/4 → 1/1 | note:bb3 s:sawtooth pan:1 cutoff:1670.953955747281 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:1188.2154262966046 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 cutoff:1188.2154262966046 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:1361.2562095290161 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 cutoff:1361.2562095290161 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:1524.257063143398 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:eb3 s:sawtooth pan:1 cutoff:1524.257063143398 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", + "[ (101/200 → 1/1) ⇝ 201/200 | note:55 s:triangle pan:0 cutoff:1601.4815730092653 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", + "[ (101/200 → 1/1) ⇝ 201/200 | note:65 s:triangle pan:1 cutoff:1601.4815730092653 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", + "[ 3/4 → 1/1 | note:bb3 s:sawtooth pan:0 cutoff:1670.953955747281 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", + "[ 3/4 → 1/1 | note:c3 s:sawtooth pan:1 cutoff:1670.953955747281 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", ] `; diff --git a/undocumented.json b/undocumented.json index 0d28835d..0a807c90 100644 --- a/undocumented.json +++ b/undocumented.json @@ -1,17 +1,17 @@ { - "/home/felix/projects/strudel/packages/core/fraction.mjs": [ + "/packages/core/fraction.mjs": [ "gcd" ], - "/home/felix/projects/strudel/packages/core/timespan.mjs": [ + "/packages/core/timespan.mjs": [ "TimeSpan" ], - "/home/felix/projects/strudel/packages/core/hap.mjs": [ + "/packages/core/hap.mjs": [ "Hap" ], - "/home/felix/projects/strudel/packages/core/state.mjs": [ + "/packages/core/state.mjs": [ "State" ], - "/home/felix/projects/strudel/packages/core/util.mjs": [ + "/packages/core/util.mjs": [ "isNoteWithOctave", "isNote", "tokenizeNote", @@ -34,23 +34,31 @@ "mapArgs", "numeralArgs", "parseFractional", - "fractionalArgs" + "fractionalArgs", + "splitAt", + "zipWith", + "clamp", + "sol2note" ], - "/home/felix/projects/strudel/packages/core/value.mjs": [ + "/packages/core/value.mjs": [ "unionWithObj", "valued", "Value", "map" ], - "/home/felix/projects/strudel/packages/core/drawLine.mjs": [], - "/home/felix/projects/strudel/packages/core/logger.mjs": [ + "/packages/core/drawLine.mjs": [], + "/packages/core/logger.mjs": [ "logKey", "logger" ], - "/home/felix/projects/strudel/packages/core/pattern.mjs": [ + "/packages/core/pattern.mjs": [ "setStringParser", + "polyrhythm", + "pr", + "pm", "isPattern", "reify", + "fastcat", "set", "keep", "keepif", @@ -74,22 +82,33 @@ "func", "compressSpan", "compressspan", + "fastgap", "focusSpan", "focusspan", + "density", + "sparsity", "zoomArc", "zoomarc", + "inv", + "juxby", + "echowith", + "stutWith", + "stutwith", + "iterback", + "chunkback", "bypass", "duration", - "color", "colour", - "striate" + "loopat", + "loopatcps" ], - "/home/felix/projects/strudel/packages/core/controls.mjs": [], - "/home/felix/projects/strudel/packages/core/euclid.mjs": [ + "/packages/core/controls.mjs": [], + "/packages/core/euclid.mjs": [ + "bjork", "euclidrot", "euclidLegatoRot" ], - "/home/felix/projects/strudel/packages/core/signal.mjs": [ + "/packages/core/signal.mjs": [ "steady", "signal", "isaw", @@ -112,34 +131,37 @@ "degradeByWith", "undegrade" ], - "/home/felix/projects/strudel/packages/core/speak.mjs": [ + "/packages/core/speak.mjs": [ "speak" ], - "/home/felix/projects/strudel/packages/core/evaluate.mjs": [ + "/packages/core/evaluate.mjs": [ "evalScope", "evaluate" ], - "/home/felix/projects/strudel/packages/core/zyklus.mjs": [], - "/home/felix/projects/strudel/packages/core/cyclist.mjs": [ + "/packages/core/zyklus.mjs": [], + "/packages/core/cyclist.mjs": [ "Cyclist" ], - "/home/felix/projects/strudel/packages/core/time.mjs": [ + "/packages/core/time.mjs": [ "getTime", "setTime" ], - "/home/felix/projects/strudel/packages/core/repl.mjs": [ - "repl" + "/packages/core/repl.mjs": [ + "repl", + "getTrigger" ], - "/home/felix/projects/strudel/packages/core/draw.mjs": [ + "/packages/core/draw.mjs": [ "getDrawContext", - "cleanupDraw" + "cleanupDraw", + "Framer", + "Drawer" ], - "/home/felix/projects/strudel/packages/core/animate.mjs": [ + "/packages/core/animate.mjs": [ "x", "y", "w", "h", - "a", + "angle", "r", "fill", "smear", @@ -147,85 +169,121 @@ "moveXY", "zoomIn" ], - "/home/felix/projects/strudel/packages/core/pianoroll.mjs": [ - "pianoroll" + "/packages/core/pianoroll.mjs": [ + "getDrawOptions", + "drawPianoroll" ], - "/home/felix/projects/strudel/packages/core/ui.mjs": [ + "/packages/core/spiral.mjs": [], + "/packages/core/ui.mjs": [ "backgroundImage", "cleanupUi" ], - "/home/felix/projects/strudel/packages/core/gist.js": [], - "/home/felix/projects/strudel/packages/core/index.mjs": [], - "/home/felix/projects/strudel/packages/midi/midi.mjs": [ + "/packages/core/gist.js": [], + "/packages/core/index.mjs": [], + "/packages/csound/index.mjs": [ + "loadCSound", + "loadcsound", + "loadCsound", + "csound", + "loadOrc" + ], + "/packages/desktopbridge/utils.mjs": [ + "Invoke", + "isTauri" + ], + "/packages/desktopbridge/midibridge.mjs": [], + "/packages/desktopbridge/oscbridge.mjs": [], + "/packages/desktopbridge/index.mjs": [], + "/packages/midi/midi.mjs": [ "WebMidi", - "enableWebMidi" + "enableWebMidi", + "midin" ], - "/home/felix/projects/strudel/packages/midi/index.mjs": [], - "/home/felix/projects/strudel/packages/mini/krill-parser.js": [], - "/home/felix/projects/strudel/packages/mini/mini.mjs": [ + "/packages/midi/index.mjs": [], + "/packages/mini/krill-parser.js": [], + "/packages/mini/mini.mjs": [ "patternifyAST", + "getLeafLocation", + "mini2ast", + "getLeaves", + "getLeafLocations", "mini", + "m", "h", - "minify" + "minify", + "miniAllStrings" ], - "/home/felix/projects/strudel/packages/mini/index.mjs": [], - "/home/felix/projects/strudel/packages/soundfonts/fontloader.mjs": [ + "/packages/mini/index.mjs": [], + "/packages/soundfonts/gm.mjs": [], + "/packages/soundfonts/fontloader.mjs": [ "getFontBufferSource", - "getFontPitch" + "getFontPitch", + "registerSoundfonts" ], - "/home/felix/projects/strudel/packages/soundfonts/list.mjs": [ + "/packages/soundfonts/list.mjs": [ "instruments", "drums", "instrumentNames" ], - "/home/felix/projects/strudel/packages/soundfonts/sfumato.mjs": [ + "/packages/soundfonts/sfumato.mjs": [ "loadSoundfont" ], - "/home/felix/projects/strudel/packages/soundfonts/index.mjs": [], - "/home/felix/projects/strudel/packages/tonal/tonal.mjs": [], - "/home/felix/projects/strudel/packages/tonal/voicings.mjs": [ - "voicingRegistry", - "setVoicingRange" + "/packages/soundfonts/index.mjs": [], + "/packages/tonal/tonal.mjs": [], + "/packages/tonal/tonleiter.mjs": [ + "pc2chroma", + "rotateChroma", + "chroma2pc", + "tokenizeChord", + "note2pc", + "note2oct", + "note2chroma", + "midi2chroma", + "pitch2chroma", + "step2semitones", + "x2midi", + "scaleStep", + "renderVoicing", + "accidentalOffset", + "Step", + "Note" ], - "/home/felix/projects/strudel/packages/tonal/index.mjs": [], - "/home/felix/projects/strudel/packages/transpiler/transpiler.mjs": [ + "/packages/tonal/ireal.mjs": [ + "simple", + "complex" + ], + "/packages/tonal/voicings.mjs": [ + "voicingRegistry", + "setVoicingRange", + "registerVoicings", + "voicingAlias" + ], + "/packages/tonal/index.mjs": [], + "/packages/transpiler/transpiler.mjs": [ "transpiler" ], - "/home/felix/projects/strudel/packages/transpiler/index.mjs": [ + "/packages/transpiler/index.mjs": [ "evaluate" ], - "/home/felix/projects/strudel/packages/webaudio/feedbackdelay.mjs": [], - "/home/felix/projects/strudel/packages/webaudio/reverb.mjs": [], - "/home/felix/projects/strudel/packages/webaudio/sampler.mjs": [ - "getCachedBuffer", - "getSampleBufferSource", - "loadBuffer", - "reverseBuffer", - "getLoadedBuffer", - "resetLoadedSamples", - "getLoadedSamples" - ], - "/home/felix/projects/strudel/packages/webaudio/vowel.mjs": [ - "vowelFormant" - ], - "/home/felix/projects/strudel/packages/webaudio/webaudio.mjs": [ - "getAudioContext", - "panic", - "initAudio", - "initAudioOnFirstClick", + "/packages/webaudio/webaudio.mjs": [ + "webaudioOutputTrigger", "webaudioOutput", - "webaudioOutputTrigger" + "webaudioScheduler" ], - "/home/felix/projects/strudel/packages/webaudio/index.mjs": [], - "/home/felix/projects/strudel/packages/xen/xen.mjs": [ + "/packages/webaudio/scope.mjs": [ + "drawTimeScope", + "drawFrequencyScope" + ], + "/packages/webaudio/index.mjs": [], + "/packages/xen/xen.mjs": [ "edo", "xen", "tuning" ], - "/home/felix/projects/strudel/packages/xen/tunejs.js": [], - "/home/felix/projects/strudel/packages/xen/tune.mjs": [ + "/packages/xen/tunejs.js": [], + "/packages/xen/tune.mjs": [ "tune" ], - "/home/felix/projects/strudel/packages/xen/index.mjs": [], - "/home/felix/projects/strudel/index.mjs": [] + "/packages/xen/index.mjs": [], + "/index.mjs": [] } diff --git a/website/astro.config.mjs b/website/astro.config.mjs index f8fceaf7..c5ca3317 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -13,24 +13,39 @@ import AstroPWA from '@vite-pwa/astro'; const site = `https://strudel.cc/`; // root url without a path const base = '/'; // base path of the strudel site +const baseNoTrailing = base.endsWith('/') ? base.slice(0, -1) : base; -// this rehype plugin converts relative anchor links to absolute ones -// it wokrs by prepending the absolute page path to anchor links -// example: #gain -> /learn/effects/#gain +// this rehype plugin fixes relative links +// it works by prepending the base + page path to anchor links +// and by prepending the base path to other relative links starting with / // this is necessary when using a base href like -// in this setup, relative anchor links will always link to base, instead of the current page -function absoluteAnchors() { +// examples with base as "mybase": +// #gain -> /mybase/learn/effects/#gain +// /some/page -> /mybase/some/page +function relativeURLFix() { return (tree, file) => { const chunks = file.history[0].split('/src/pages/'); // file.history[0] is the file path const path = chunks[chunks.length - 1].slice(0, -4); // only path inside src/pages, without .mdx return rehypeUrls((url) => { - if (!url.href.startsWith('#')) { + let newHref = baseNoTrailing; + if (url.href.startsWith('#')) { + // special case: a relative anchor link to the current page + newHref += `/${path}/${url.href}`; + } else if (url.href.startsWith('/')) { + // any other relative url starting with / + newHref += url.pathname; + if (url.pathname.indexOf('.') == -1) { + // append trailing slash to resource only if there is no file extension + newHref += url.pathname.endsWith('/') ? '' : '/'; + } + newHref += url.search || ''; + newHref += url.hash || ''; + } else { + // leave this URL alone return; } - const baseWithSlash = base.endsWith('/') ? base : base + '/'; - const absoluteUrl = baseWithSlash + path + url.href; - // console.log(url.href + ' -> ', absoluteUrl); - return absoluteUrl; + // console.log(url.href + ' -> ', newHref); + return newHref; })(tree); }; } @@ -40,7 +55,7 @@ const options = { remarkToc, // E.g. `remark-frontmatter` ], - rehypePlugins: [rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'append' }], absoluteAnchors], + rehypePlugins: [rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'append' }], relativeURLFix], }; // https://astro.build/config diff --git a/website/src/components/HeadCommon.astro b/website/src/components/HeadCommon.astro index 2aa6acfe..ab18500f 100644 --- a/website/src/components/HeadCommon.astro +++ b/website/src/components/HeadCommon.astro @@ -3,7 +3,7 @@ import { pwaInfo } from 'virtual:pwa-info'; import '../styles/index.css'; const { BASE_URL } = import.meta.env; -const base = BASE_URL; +const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; --- @@ -11,20 +11,20 @@ const base = BASE_URL; - + - - + + - + - + diff --git a/website/src/components/Header/Header.astro b/website/src/components/Header/Header.astro index 5912569d..6447d9d6 100644 --- a/website/src/components/Header/Header.astro +++ b/website/src/components/Header/Header.astro @@ -16,6 +16,9 @@ const { currentPage } = Astro.props as Props; // const lang = getLanguageFromURL(currentPage); const langCode = 'en'; // getLanguageFromURL(currentPage); const sidebar = SIDEBAR[langCode]; + +const { BASE_URL } = import.meta.env; +const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; ---