From cdba9efb5981d4000c6c21fbd067e2d319c3977e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 1 Apr 2024 03:17:15 +0200 Subject: [PATCH 001/547] add filter + filterWhen + within --- packages/core/pattern.mjs | 31 +++++++++++++++++++ test/__snapshots__/examples.test.mjs.snap | 37 +++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index daacc946..2cac42c5 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2487,6 +2487,37 @@ Pattern.prototype.tag = function (tag) { return this.withContext((ctx) => ({ ...ctx, tags: (ctx.tags || []).concat([tag]) })); }; +/** + * Filters haps using the given function + * @name filter + * @param {Function} test function to test Hap + * @example + * s("hh!7 oh").filter(hap => hap.value.s==='hh') + */ +export const filter = register('filter', (test, pat) => pat.withHaps((haps) => haps.filter(test))); + +/** + * Filters haps by their begin time + * @name filterWhen + * @noAutocomplete + * @param {Function} test function to test Hap.whole.begin + */ +export const filterWhen = register('filterWhen', (test, pat) => pat.filter((h) => test(h.whole.begin))); + +/** + * Use within to apply a function to only a part of a pattern. + * @name within + * @param {number} start start within cycle (0 - 1) + * @param {number} end end within cycle (0 - 1). Must be > start + * @param {Function} func function to be applied to the sub-pattern + */ +export const within = register('within', (a, b, fn, pat) => + stack( + fn(pat.filterWhen((t) => t.cyclePos() >= a && t.cyclePos() <= b)), + pat.filterWhen((t) => t.cyclePos() < a || t.cyclePos() > b), + ), +); + ////////////////////////////////////////////////////////////////////// // Control-related functions, i.e. ones that manipulate patterns of // objects diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 27074590..0c9c3f8c 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -2560,6 +2560,43 @@ exports[`runs examples > example "fastGap" example index 0 1`] = ` ] `; +exports[`runs examples > example "filter" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:hh ]", + "[ 1/8 → 1/4 | s:hh ]", + "[ 1/4 → 3/8 | s:hh ]", + "[ 3/8 → 1/2 | s:hh ]", + "[ 1/2 → 5/8 | s:hh ]", + "[ 5/8 → 3/4 | s:hh ]", + "[ 3/4 → 7/8 | s:hh ]", + "[ 7/8 → 1/1 | s:oh ]", + "[ 1/1 → 9/8 | s:hh ]", + "[ 9/8 → 5/4 | s:hh ]", + "[ 5/4 → 11/8 | s:hh ]", + "[ 11/8 → 3/2 | s:hh ]", + "[ 3/2 → 13/8 | s:hh ]", + "[ 13/8 → 7/4 | s:hh ]", + "[ 7/4 → 15/8 | s:hh ]", + "[ 15/8 → 2/1 | s:oh ]", + "[ 2/1 → 17/8 | s:hh ]", + "[ 17/8 → 9/4 | s:hh ]", + "[ 9/4 → 19/8 | s:hh ]", + "[ 19/8 → 5/2 | s:hh ]", + "[ 5/2 → 21/8 | s:hh ]", + "[ 21/8 → 11/4 | s:hh ]", + "[ 11/4 → 23/8 | s:hh ]", + "[ 23/8 → 3/1 | s:oh ]", + "[ 3/1 → 25/8 | s:hh ]", + "[ 25/8 → 13/4 | s:hh ]", + "[ 13/4 → 27/8 | s:hh ]", + "[ 27/8 → 7/2 | s:hh ]", + "[ 7/2 → 29/8 | s:hh ]", + "[ 29/8 → 15/4 | s:hh ]", + "[ 15/4 → 31/8 | s:hh ]", + "[ 31/8 → 4/1 | s:oh ]", +] +`; + exports[`runs examples > example "firstOf" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:g3 ]", From e3b6884b86f664ed65ed005e7ec8c5952b16f9eb Mon Sep 17 00:00:00 2001 From: fnordomat <46D46D1246803312401472B5A7427E237B7908CA> Date: Mon, 1 Apr 2024 10:46:32 +0200 Subject: [PATCH 002/547] make soundfont base url configurable --- packages/soundfonts/fontloader.mjs | 17 ++++++++++++++++- packages/soundfonts/index.mjs | 4 ++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index 8cbe3e81..27f1875a 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -1,3 +1,4 @@ +import { persistentMap } from '@nanostores/persistent'; import { noteToMidi, freqToMidi, getSoundIndex } from '@strudel/core'; import { getAudioContext, @@ -9,6 +10,20 @@ import { } from '@strudel/webaudio'; import gm from './gm.mjs'; +export const defaultFontloaderConfig = { + soundfontUrl: 'https://felixroos.github.io/webaudiofontdata/sound' +} + +export const fontloaderConfigMap = persistentMap('strudel-config-fontloader', defaultFontloaderConfig); + +export function setSoundfontUrl(obj) { + fontloaderConfigMap.setKey('soundfontUrl', JSON.stringify(obj)); +} + +export function getSoundfontUrl() { + return JSON.parse(fontloaderConfigMap.get().soundfontUrl); +} + let loadCache = {}; async function loadFont(name) { if (loadCache[name]) { @@ -16,7 +31,7 @@ async function loadFont(name) { } const load = async () => { // TODO: make soundfont source configurable - const url = `https://felixroos.github.io/webaudiofontdata/sound/${name}.js`; + const url = `${getSoundfontUrl()}/${name}.js`; const preset = await fetch(url).then((res) => res.text()); let [_, data] = preset.split('={'); return eval('{' + data); diff --git a/packages/soundfonts/index.mjs b/packages/soundfonts/index.mjs index c54297b5..31792603 100644 --- a/packages/soundfonts/index.mjs +++ b/packages/soundfonts/index.mjs @@ -1,6 +1,6 @@ -import { getFontBufferSource, registerSoundfonts } from './fontloader.mjs'; +import { getFontBufferSource, registerSoundfonts, setSoundfontUrl } from './fontloader.mjs'; import * as soundfontList from './list.mjs'; import { startPresetNote } from 'sfumato'; import { loadSoundfont } from './sfumato.mjs'; -export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList, registerSoundfonts }; +export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList, registerSoundfonts, setSoundfontUrl }; From b233c33f56236ebfed2672cc2b1b783048c71f1f Mon Sep 17 00:00:00 2001 From: fnordomat <46D46D1246803312401472B5A7427E237B7908CA> Date: Mon, 1 Apr 2024 17:14:27 +0200 Subject: [PATCH 003/547] (squash me) save setting in variable, not store --- packages/soundfonts/fontloader.mjs | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index 27f1875a..427f57e3 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -1,4 +1,3 @@ -import { persistentMap } from '@nanostores/persistent'; import { noteToMidi, freqToMidi, getSoundIndex } from '@strudel/core'; import { getAudioContext, @@ -10,18 +9,11 @@ import { } from '@strudel/webaudio'; import gm from './gm.mjs'; -export const defaultFontloaderConfig = { - soundfontUrl: 'https://felixroos.github.io/webaudiofontdata/sound' -} +let defaultSoundfontUrl = 'https://felixroos.github.io/webaudiofontdata/sound'; +let soundfontUrl = defaultSoundfontUrl; -export const fontloaderConfigMap = persistentMap('strudel-config-fontloader', defaultFontloaderConfig); - -export function setSoundfontUrl(obj) { - fontloaderConfigMap.setKey('soundfontUrl', JSON.stringify(obj)); -} - -export function getSoundfontUrl() { - return JSON.parse(fontloaderConfigMap.get().soundfontUrl); +export function setSoundfontUrl(value) { + soundfontUrl = value; } let loadCache = {}; @@ -31,7 +23,7 @@ async function loadFont(name) { } const load = async () => { // TODO: make soundfont source configurable - const url = `${getSoundfontUrl()}/${name}.js`; + const url = `${soundfontUrl}/${name}.js`; const preset = await fetch(url).then((res) => res.text()); let [_, data] = preset.split('={'); return eval('{' + data); From 5ca9afa9e804d3189fd524a61d42299e4df9f2fe Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 15 Apr 2024 19:53:12 -0400 Subject: [PATCH 004/547] restored --- website/src/components/Udels/UdelFrame.jsx | 33 ++++++ website/src/components/Udels/Udels.jsx | 115 +++++++++++++++++++++ website/src/layouts/MainLayout.astro | 2 +- website/src/pages/index.astro | 4 +- website/src/pages/udels/index.astro | 12 +++ website/src/repl/Repl.jsx | 7 +- website/src/repl/panel/PatternsTab.jsx | 4 +- website/src/repl/panel/SettingsTab.jsx | 8 +- website/src/repl/util.mjs | 4 + website/src/settings.mjs | 9 +- 10 files changed, 185 insertions(+), 13 deletions(-) create mode 100644 website/src/components/Udels/UdelFrame.jsx create mode 100644 website/src/components/Udels/Udels.jsx create mode 100644 website/src/pages/udels/index.astro diff --git a/website/src/components/Udels/UdelFrame.jsx b/website/src/components/Udels/UdelFrame.jsx new file mode 100644 index 00000000..78d28944 --- /dev/null +++ b/website/src/components/Udels/UdelFrame.jsx @@ -0,0 +1,33 @@ +import { useRef } from 'react'; + +export function UdelFrame({ onEvaluate, hash, instance }) { + const ref = useRef(); + + window.addEventListener('message', (message) => { + const childWindow = ref?.current?.contentWindow; + if (message == null || message.source !== childWindow) { + return; // Skip message in this event listener + } + onEvaluate(message.data); + }); + + const url = new URL(window.location.origin); + url.hash = hash; + url.searchParams.append('instance', instance); + const source = url.toString(); + + return ( + + ); +} diff --git a/website/src/components/Udels/Udels.jsx b/website/src/components/Udels/Udels.jsx new file mode 100644 index 00000000..b2d81ea9 --- /dev/null +++ b/website/src/components/Udels/Udels.jsx @@ -0,0 +1,115 @@ +import { code2hash } from '@strudel/core'; + +import { UdelFrame } from './UdelFrame'; +import { useState } from 'react'; + +function NumberInput({ value, onChange, label = '', min, max }) { + const [localState, setLocalState] = useState(value); + const [isFocused, setIsFocused] = useState(false); + + return ( + + ); +} +const defaultHash = 'c3RhY2soCiAgCik%3D'; + +const getHashesFromUrl = () => { + return window.location.hash?.slice(1).split(','); +}; +const updateURLHashes = (hashes) => { + const newHash = '#' + hashes.join(','); + window.location.hash = newHash; +}; +export function Udels() { + const hashes = getHashesFromUrl(); + + const [numWindows, setNumWindows] = useState(hashes?.length ?? 1); + const numWindowsOnChange = (num) => { + setNumWindows(num); + const hashes = getHashesFromUrl(); + const newHashes = []; + for (let i = 0; i < num; i++) { + newHashes[i] = hashes[i] ?? defaultHash; + } + updateURLHashes(newHashes); + }; + + const onEvaluate = (key, code) => { + const hashes = getHashesFromUrl(); + hashes[key] = code2hash(code); + updateURLHashes(hashes); + }; + // useEffect(() => { + // prebake(); + // }, []); + + return ( +
+
+ +
+
+ {hashes.map((hash, key) => { + return ( + { + onEvaluate(key, code); + }} + hash={hash} + key={key} + /> + ); + })} +
+ {/* */} +
+ ); +} diff --git a/website/src/layouts/MainLayout.astro b/website/src/layouts/MainLayout.astro index 49952a7e..c6f6653c 100644 --- a/website/src/layouts/MainLayout.astro +++ b/website/src/layouts/MainLayout.astro @@ -30,7 +30,7 @@ const githubEditUrl = `${CONFIG.GITHUB_EDIT_URL}/${currentFile}`; - +
diff --git a/website/src/pages/index.astro b/website/src/pages/index.astro index 760cf448..29cf2fad 100644 --- a/website/src/pages/index.astro +++ b/website/src/pages/index.astro @@ -3,12 +3,12 @@ import HeadCommon from '../components/HeadCommon.astro'; import { Repl } from '../repl/Repl'; --- - + Strudel REPL - + diff --git a/website/src/pages/udels/index.astro b/website/src/pages/udels/index.astro new file mode 100644 index 00000000..4ab699e7 --- /dev/null +++ b/website/src/pages/udels/index.astro @@ -0,0 +1,12 @@ +--- +import { Udels } from '../../components/Udels/Udels.jsx'; + + +const { BASE_URL } = import.meta.env; +const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; +--- + + + + + \ No newline at end of file diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index 9c472eb4..0c1ad91d 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -34,7 +34,7 @@ import Loader from './Loader'; import { Panel } from './panel/Panel'; import { useStore } from '@nanostores/react'; import { prebake } from './prebake.mjs'; -import { getRandomTune, initCode, loadModules, shareCode, ReplContext } from './util.mjs'; +import { getRandomTune, initCode, loadModules, shareCode, ReplContext, isUdels } from './util.mjs'; import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon'; import './Repl.css'; import { setInterval, clearInterval } from 'worker-timers'; @@ -221,6 +221,7 @@ export function Repl({ embedded = false }) { handleEvaluate, }; + const showPanel = !isEmbedded || isUdels(); return (
@@ -246,12 +247,12 @@ export function Repl({ embedded = false }) { } }} > - {panelPosition === 'right' && !isEmbedded && } + {panelPosition === 'right' && showPanel && }
{error && (
{error.message || 'Unknown Error :-/'}
)} - {panelPosition === 'bottom' && !isEmbedded && } + {panelPosition === 'bottom' && showPanel && }
); diff --git a/website/src/repl/panel/PatternsTab.jsx b/website/src/repl/panel/PatternsTab.jsx index f6499d79..cacbe897 100644 --- a/website/src/repl/panel/PatternsTab.jsx +++ b/website/src/repl/panel/PatternsTab.jsx @@ -9,7 +9,7 @@ import { import { useMemo } from 'react'; import { getMetadata } from '../../metadata_parser'; import { useExamplePatterns } from '../useExamplePatterns'; -import { parseJSON } from '../util.mjs'; +import { parseJSON, isUdels } from '../util.mjs'; import { ButtonGroup } from './Forms.jsx'; import { settingsMap, useSettings } from '../../settings.mjs'; @@ -99,7 +99,7 @@ export function PatternsTab({ context }) { }; const viewingPatternID = viewingPatternData?.id; - const autoResetPatternOnChange = !window.parent?.location.pathname.includes('oodles'); + const autoResetPatternOnChange = !isUdels(); return (
diff --git a/website/src/repl/panel/SettingsTab.jsx b/website/src/repl/panel/SettingsTab.jsx index ae290189..36ec12bd 100644 --- a/website/src/repl/panel/SettingsTab.jsx +++ b/website/src/repl/panel/SettingsTab.jsx @@ -1,12 +1,13 @@ import { defaultSettings, settingsMap, useSettings } from '../../settings.mjs'; import { themes } from '@strudel/codemirror'; +import { isUdels } from '../util.mjs'; import { ButtonGroup } from './Forms.jsx'; import { AudioDeviceSelector } from './AudioDeviceSelector.jsx'; -function Checkbox({ label, value, onChange }) { +function Checkbox({ label, value, onChange, disabled = false }) { return ( ); @@ -96,7 +97,7 @@ export function SettingsTab({ started }) { panelPosition, audioDeviceName, } = useSettings(); - + const shouldAlwaysSync = isUdels(); return (
{AudioContext.prototype.setSinkId != null && ( @@ -197,6 +198,7 @@ export function SettingsTab({ started }) { window.location.reload(); } }} + disabled={shouldAlwaysSync} value={isSyncEnabled} /> diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index 6dba7dab..67584760 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -132,6 +132,10 @@ export async function shareCode(codeToShare) { export const ReplContext = createContext(null); +export const isUdels = () => { + return window.parent?.location.pathname.includes('udels'); +}; + export const getAudioDevices = async () => { await navigator.mediaDevices.getUserMedia({ audio: true }); let mediaDevices = await navigator.mediaDevices.enumerateDevices(); diff --git a/website/src/settings.mjs b/website/src/settings.mjs index a70c4202..3f1ca051 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -1,6 +1,7 @@ import { persistentMap } from '@nanostores/persistent'; import { useStore } from '@nanostores/react'; import { register } from '@strudel/core'; +import { isUdels } from './repl/util.mjs'; export const defaultAudioDeviceName = 'System Standard'; @@ -28,8 +29,12 @@ export const defaultSettings = { userPatterns: '{}', audioDeviceName: defaultAudioDeviceName, }; +const search = new URLSearchParams(window.location.search); +// if running multiple instance in one window, it will use the settings for that instance. else default to normal +const instance = parseInt(search.get('instance') ?? '0'); +const settings_key = `strudel-settings${instance > 0 ? instance : ''}`; -export const settingsMap = persistentMap('strudel-settings', defaultSettings); +export const settingsMap = persistentMap(settings_key, defaultSettings); const parseBoolean = (booleanlike) => ([true, 'true'].includes(booleanlike) ? true : false); @@ -54,7 +59,7 @@ export function useSettings() { isTooltipEnabled: parseBoolean(state.isTooltipEnabled), isLineWrappingEnabled: parseBoolean(state.isLineWrappingEnabled), isFlashEnabled: parseBoolean(state.isFlashEnabled), - isSyncEnabled: parseBoolean(state.isSyncEnabled), + isSyncEnabled: isUdels() ? true : parseBoolean(state.isSyncEnabled), fontSize: Number(state.fontSize), panelPosition: state.activeFooter !== '' ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is! userPatterns: userPatterns, From 95ae5191ceedfdaae1f85b8e5ed34926129bb9fa Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 16 May 2024 03:36:35 +0200 Subject: [PATCH 005/547] begin "understanding voicings" --- website/src/config.ts | 1 + website/src/pages/understand/voicings.mdx | 196 ++++++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 website/src/pages/understand/voicings.mdx diff --git a/website/src/config.ts b/website/src/config.ts index 9dee2aac..f85d60d8 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -99,6 +99,7 @@ export const SIDEBAR: Sidebar = { { text: 'Coding syntax', link: 'learn/code' }, { text: 'Pitch', link: 'understand/pitch' }, { text: 'Cycles', link: 'understand/cycles' }, + { text: 'Voicings', link: 'understand/voicings' }, { text: 'Pattern Alignment', link: 'technical-manual/alignment' }, { text: 'Strudel vs Tidal', link: 'learn/strudel-vs-tidal' }, ], diff --git a/website/src/pages/understand/voicings.mdx b/website/src/pages/understand/voicings.mdx new file mode 100644 index 00000000..a36ecd8a --- /dev/null +++ b/website/src/pages/understand/voicings.mdx @@ -0,0 +1,196 @@ +--- +title: Understanding Chod Voicings +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import { PitchSlider } from '../../components/PitchSlider'; +import Box from '@components/Box.astro'; + +# Understanding Chords and Voicings + +Let's dig deeper into how chords and voicings work. +I'll try to keep theory jargon to a minimum, so hopefully this is approachable for anyone interested. + +## What is a chord + +Playing more than one note at a time is generally called a chord. Here's an example: + +").room(.5)`} /> + +Here's the same with midi numbers: + +").room(.5)`} /> + +Here, we have two 3-note chords played in a loop. +You could already stop here and write chords in this style, which is totally fine and gives you control over individual notes. +One downside is that it can be difficult to find good sounding chords and maybe you're yearning for a way to organize chords in some other way.. + +## Labeling Chords + +Chords are typically given different labels depending on the relationship of the notes within. +In the number example above, we have `48,51,55` and `53,57,60`. + +To analyze the relationship of those notes, they are typically compared to some `root`, which is often the lowest note. +In our case, the `roots` would be `48` (= `c3`) and `53` (= `f3`). +We can express the same chords relative to those `roots` like this: + +".add("<48 53>")).room(.5)`} /> + +Now within each chord, each number represents the distance from the root. +A distance between pitches is typically called `interval`, but let's stick to distance for now. + +Now we can see that our 2 chords are actually quite similar, as the only difference is the middle note (and the root of course). +They are part of a group of chords called `triads` which are chords with 3 notes. + +### Triads + +These 4 shapes are the most common types of `triads` you will encounter: + +| shape | label | +| ----- | ---------- | +| 0,3,6 | diminished | +| 0,3,7 | minor | +| 0,4,7 | major | +| 0,4,8 | augmented | + +Here they are in succession: + +".add("60")) +.room(.5)._pitchwheel()`} +/> + +Many types of music often only use minor and major chords, so we already have the knowledge to accompany songs. Here's one: + +\`.add(\`< +a c d f +a e a e +>\`)).room(.5)`} +/> + +These are the chords for "The house of the rising sun" by The Animals. +So far it doesn't sound too exiting but at least it's recognizable.. + +## Voicings + +A `voicing` is one of many ways a certain chord shape could be played. +The term comes from choral music, where chords can be sung in different ways by changing which voice sings which note. +For example we could add 12 to one or more notes in the chord: + +".add("48")) +.room(.5)`} +/> + +Notes that are 12 steps apart (= 1 `octave`) are considered to be equal in a harmonic sense, which is why they get the same note letter. +Here's the same example with note letterns: + +") +.room(.5)`} +/> + +This type of voicings are also called `inversions`. There are many other ways we could `voice` this minor chord: + +".add("48")) +.room(.5)`} +/> + +Here we are changing the flavour of the chord slightly by + +1. doubling notes 12 steps higher, +2. using very wide distances +3. omitting notes + +## Voice Leading + +Let's revisit "The House of the Rising Sun", this time using our newly acquired voicing techniques: + +\`.add(\`< +a c d f +a e a e +>\`)).room(.5)`} + punchcard +/> + +These voicings make the chords sound more connected and less jumpy, compared to the version without voicings. +The way chords interact is also called voice leading, reminiscent of how a choir voice would move through a sequence of chords. + +For example, try singing the top voice in the above example. Then try the same on the example without voice leading. Which one's easier? + +Naturally, there are many ways a progression of chords could be voiced and there is no clear right or wrong. + +## Chord Symbols + +Musicians playing chord-based music often rely on a so called lead sheet, which is a simplified notation of a music piece. +The chords in those lead sheets are notated with symbols that allow a piece to be notated in a very concise manner. +A common way to write the chords "The House of the Rising Sun" would be: + +``` +Am | C | D | F +Am | E | Am | E +``` + +Here, each symbol consists of the `root` of the chord and optionally an `m` to signal it's a minor chord (just the root note means it's major). +We could mirror that notation in strudel using the `pick` function: + +" + .pick({ + Am: "57,60,64", + C: "55,60,64", + D: "50,57,66", + F: "57,60,65", + E: "56,59,64", + }) + .note().room(.5)`} + punchcard +/> + +## The voicing function + +Coming up with good sounding voicings that connect well can be a difficult and time consuming process. +The `chord` and `voicing` functions can be used to automate that: + +").voicing().room(.5)`} punchcard /> + +Here we're also using chord symbols but the voicings will be automatically generated with smooth voice leading. + +## Voicing Dictionaries + +The voicing function internally uses so called `voicing dictionaries`, which can also be customized: + +") + .dict('house').anchor(66) + .voicing().room(.5)`} + punchcard +/> + +In a `voicing dictionary`, each chord symbol is assigned one or more voicings. +The `voicing` function then picks the voicing that is closest to the `anchor` (defaults to `c5`). + +The handy thing about this approach is that a `voicing dictionary` can be used to play any chord progression with automated voice leading! From 83e46037d2b0156e6270ab2f46d533e4bb61d091 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 16 May 2024 08:59:07 +0200 Subject: [PATCH 006/547] typo --- website/src/pages/understand/timbre.mdx | 12 ++++++++++++ website/src/pages/understand/voicings.mdx | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 website/src/pages/understand/timbre.mdx diff --git a/website/src/pages/understand/timbre.mdx b/website/src/pages/understand/timbre.mdx new file mode 100644 index 00000000..47a8e299 --- /dev/null +++ b/website/src/pages/understand/timbre.mdx @@ -0,0 +1,12 @@ +--- +title: Understanding Timbre +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import { PitchSlider } from '../../components/PitchSlider'; +import Box from '@components/Box.astro'; + +# Understanding Timbre + +Let's learn what timbre is! diff --git a/website/src/pages/understand/voicings.mdx b/website/src/pages/understand/voicings.mdx index a36ecd8a..d66e075e 100644 --- a/website/src/pages/understand/voicings.mdx +++ b/website/src/pages/understand/voicings.mdx @@ -1,5 +1,5 @@ --- -title: Understanding Chod Voicings +title: Understanding Chord Voicings layout: ../../layouts/MainLayout.astro --- From 73d49f152af197bcd7e16ac1b0ac75193e3fcf5d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 16 May 2024 10:52:54 +0200 Subject: [PATCH 007/547] refactor sampler: - delegate getSampleBufferSource logic to getSampleInfo + getSampleBuffer - move buffer logic up from onTriggerSample to those functions --- packages/superdough/sampler.mjs | 115 ++++++++++++++++------------- packages/superdough/superdough.mjs | 1 + 2 files changed, 65 insertions(+), 51 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 10087fc6..4e9b8a34 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -22,47 +22,79 @@ function humanFileSize(bytes, si) { return bytes.toFixed(1) + ' ' + units[u]; } -export const getSampleBufferSource = async (s, n, note, speed, freq, bank, resolveUrl) => { - let transpose = 0; - if (freq !== undefined && note !== undefined) { - logger('[sampler] hap has note and freq. ignoring note', 'warning'); - } - let midi = valueToMidi({ freq, note }, 36); - transpose = midi - 36; // C3 is middle C - - const ac = getAudioContext(); - +// deduces relevant info for sample loading from hap.value and sample definition +// it encapsulates the core sampler logic into a pure and synchronous function +// hapValue: Hap.value, samples: sample definition for sound "s" (values in strudel.json format) +export function getSampleInfo(hapValue, samples) { + const { s, n = 0, speed = 1.0 } = hapValue; + let midi = valueToMidi(hapValue, 36); + let transpose = midi - 36; // C3 is middle C; let sampleUrl; let index = 0; - if (Array.isArray(bank)) { - index = getSoundIndex(n, bank.length); - sampleUrl = bank[index]; + if (Array.isArray(samples)) { + index = getSoundIndex(n, samples.length); + sampleUrl = samples[index]; } else { const midiDiff = (noteA) => noteToMidi(noteA) - midi; // object format will expect keys as notes - const closest = Object.keys(bank) + const closest = Object.keys(samples) .filter((k) => !k.startsWith('_')) .reduce( (closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest), null, ); transpose = -midiDiff(closest); // semitones to repitch - index = getSoundIndex(n, bank[closest].length); - sampleUrl = bank[closest][index]; + index = getSoundIndex(n, samples[closest].length); + sampleUrl = samples[closest][index]; } + const label = `${s}:${index}`; + let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12); + return { transpose, sampleUrl, index, midi, label, playbackRate }; +} + +// takes hapValue and returns buffer + playbackRate. +export const getSampleBuffer = async (hapValue, samples, resolveUrl) => { + let { sampleUrl, label, playbackRate } = getSampleInfo(hapValue, samples); if (resolveUrl) { sampleUrl = await resolveUrl(sampleUrl); } - let buffer = await loadBuffer(sampleUrl, ac, s, index); + const ac = getAudioContext(); + const buffer = await loadBuffer(sampleUrl, ac, label); + + if (hapValue.unit === 'c') { + playbackRate = playbackRate * buffer.duration; + } + return { buffer, playbackRate }; +}; + +// creates playback ready AudioBufferSourceNode from hapValue +export const getSampleBufferSource = async (hapValue, samples, resolveUrl) => { + let { buffer, playbackRate } = await getSampleBuffer(hapValue, samples, resolveUrl); if (speed < 0) { // should this be cached? buffer = reverseBuffer(buffer); } + const ac = getAudioContext(); const bufferSource = ac.createBufferSource(); bufferSource.buffer = buffer; - const playbackRate = 1.0 * Math.pow(2, transpose / 12); bufferSource.playbackRate.value = playbackRate; - return bufferSource; + + const { s, loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue; + + // "The computation of the offset into the sound is performed using the sound buffer's natural sample rate, + // rather than the current playback rate, so even if the sound is playing at twice its normal speed, + // the midway point through a 10-second audio buffer is still 5." + const offset = begin * bufferSource.buffer.duration; + // sound names starting with wt_ are looped automatically (wt = wavetable) + const loop = s.startsWith('wt_') ? 1 : hapValue.loop; + if (loop) { + bufferSource.loop = true; + bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset; + bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset; + } + const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value; + const sliceDuration = (end - begin) * bufferDuration; + return { bufferSource, offset, bufferDuration, sliceDuration }; }; export const loadBuffer = (url, ac, s, n = 0) => { @@ -246,41 +278,29 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option const cutGroups = []; -export async function onTriggerSample(t, value, onended, bank, resolveUrl) { +export async function onTriggerSample(t, value, onended, samples, resolveUrl) { let { s, - freq, - unit, nudge = 0, // TODO: is this in seconds? cut, loop, clip = undefined, // if set, samples will be cut off when the hap ends n = 0, - note, speed = 1, // sample playback speed - loopBegin = 0, - begin = 0, - loopEnd = 1, - end = 1, duration, } = value; + // load sample if (speed === 0) { // no playback return; } - loop = s.startsWith('wt_') ? 1 : value.loop; const ac = getAudioContext(); + // destructure adsr here, because the default should be different for synths and samples - let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); - //const soundfont = getSoundfontKey(s); - const time = t + nudge; - const bufferSource = await getSampleBufferSource(s, n, note, speed, freq, bank, resolveUrl); - - // vibrato - let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t); + const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, samples, resolveUrl); // asny stuff above took too long? if (ac.currentTime > t) { @@ -292,26 +312,19 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { logger(`[sampler] could not load "${s}:${n}"`, 'error'); return; } - bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value; - if (unit === 'c') { - // are there other units? - bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration * 1; //cps; - } - // "The computation of the offset into the sound is performed using the sound buffer's natural sample rate, - // rather than the current playback rate, so even if the sound is playing at twice its normal speed, - // the midway point through a 10-second audio buffer is still 5." - const offset = begin * bufferSource.buffer.duration; - if (loop) { - bufferSource.loop = true; - bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset; - bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset; - } + + // vibrato + let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t); + + const time = t + nudge; bufferSource.start(time, offset); + const envGain = ac.createGain(); const node = bufferSource.connect(envGain); + + // if none of these controls is set, the duration of the sound will be set to the duration of the sample slice if (clip == null && loop == null && value.release == null) { - const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value; - duration = (end - begin) * bufferDuration; + duration = sliceDuration; } let holdEnd = t + duration; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index c0ba96e0..ebb54863 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -362,6 +362,7 @@ export const superdough = async (value, t, hapDuration) => { }; if (bank && s) { s = `${bank}_${s}`; + value.s = s; } // get source AudioNode From 226cf356d6e509e41edf555037767bfaf67f8e51 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 16 May 2024 11:05:38 +0200 Subject: [PATCH 008/547] fix: lint --- packages/superdough/sampler.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 4e9b8a34..552821da 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -70,7 +70,7 @@ export const getSampleBuffer = async (hapValue, samples, resolveUrl) => { // creates playback ready AudioBufferSourceNode from hapValue export const getSampleBufferSource = async (hapValue, samples, resolveUrl) => { let { buffer, playbackRate } = await getSampleBuffer(hapValue, samples, resolveUrl); - if (speed < 0) { + if (hapValue.speed < 0) { // should this be cached? buffer = reverseBuffer(buffer); } @@ -85,7 +85,7 @@ export const getSampleBufferSource = async (hapValue, samples, resolveUrl) => { // rather than the current playback rate, so even if the sound is playing at twice its normal speed, // the midway point through a 10-second audio buffer is still 5." const offset = begin * bufferSource.buffer.duration; - // sound names starting with wt_ are looped automatically (wt = wavetable) + const loop = s.startsWith('wt_') ? 1 : hapValue.loop; if (loop) { bufferSource.loop = true; From 69a5d62027ddaeeb1f61c61d7b6888ed6e465bd2 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 17 May 2024 09:42:03 +0200 Subject: [PATCH 009/547] rename "samples" to "bank" --- packages/superdough/sampler.mjs | 34 ++++++++++++++++----------------- website/src/repl/files.mjs | 6 +++--- website/src/repl/idbutils.mjs | 6 +++--- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 552821da..f5e46d6b 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -24,28 +24,28 @@ function humanFileSize(bytes, si) { // deduces relevant info for sample loading from hap.value and sample definition // it encapsulates the core sampler logic into a pure and synchronous function -// hapValue: Hap.value, samples: sample definition for sound "s" (values in strudel.json format) -export function getSampleInfo(hapValue, samples) { +// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format) +export function getSampleInfo(hapValue, bank) { const { s, n = 0, speed = 1.0 } = hapValue; let midi = valueToMidi(hapValue, 36); let transpose = midi - 36; // C3 is middle C; let sampleUrl; let index = 0; - if (Array.isArray(samples)) { - index = getSoundIndex(n, samples.length); - sampleUrl = samples[index]; + if (Array.isArray(bank)) { + index = getSoundIndex(n, bank.length); + sampleUrl = bank[index]; } else { const midiDiff = (noteA) => noteToMidi(noteA) - midi; // object format will expect keys as notes - const closest = Object.keys(samples) + const closest = Object.keys(bank) .filter((k) => !k.startsWith('_')) .reduce( (closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest), null, ); transpose = -midiDiff(closest); // semitones to repitch - index = getSoundIndex(n, samples[closest].length); - sampleUrl = samples[closest][index]; + index = getSoundIndex(n, bank[closest].length); + sampleUrl = bank[closest][index]; } const label = `${s}:${index}`; let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12); @@ -53,8 +53,8 @@ export function getSampleInfo(hapValue, samples) { } // takes hapValue and returns buffer + playbackRate. -export const getSampleBuffer = async (hapValue, samples, resolveUrl) => { - let { sampleUrl, label, playbackRate } = getSampleInfo(hapValue, samples); +export const getSampleBuffer = async (hapValue, bank, resolveUrl) => { + let { sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank); if (resolveUrl) { sampleUrl = await resolveUrl(sampleUrl); } @@ -68,8 +68,8 @@ export const getSampleBuffer = async (hapValue, samples, resolveUrl) => { }; // creates playback ready AudioBufferSourceNode from hapValue -export const getSampleBufferSource = async (hapValue, samples, resolveUrl) => { - let { buffer, playbackRate } = await getSampleBuffer(hapValue, samples, resolveUrl); +export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => { + let { buffer, playbackRate } = await getSampleBuffer(hapValue, bank, resolveUrl); if (hapValue.speed < 0) { // should this be cached? buffer = reverseBuffer(buffer); @@ -264,10 +264,10 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option const { prebake, tag } = options; processSampleMap( sampleMap, - (key, value) => - registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { + (key, bank) => + registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), { type: 'sample', - samples: value, + samples: bank, baseUrl, prebake, tag, @@ -278,7 +278,7 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option const cutGroups = []; -export async function onTriggerSample(t, value, onended, samples, resolveUrl) { +export async function onTriggerSample(t, value, onended, bank, resolveUrl) { let { s, nudge = 0, // TODO: is this in seconds? @@ -300,7 +300,7 @@ export async function onTriggerSample(t, value, onended, samples, resolveUrl) { // destructure adsr here, because the default should be different for synths and samples let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); - const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, samples, resolveUrl); + const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl); // asny stuff above took too long? if (ac.currentTime > t) { diff --git a/website/src/repl/files.mjs b/website/src/repl/files.mjs index 63ac5f85..5295ba55 100644 --- a/website/src/repl/files.mjs +++ b/website/src/repl/files.mjs @@ -23,10 +23,10 @@ async function hasStrudelJson(subpath) { async function loadStrudelJson(subpath) { const contents = await readTextFile(subpath + '/strudel.json', { dir }); const sampleMap = JSON.parse(contents); - processSampleMap(sampleMap, (key, value) => { - registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value, fileResolver(subpath)), { + processSampleMap(sampleMap, (key, bank) => { + registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank, fileResolver(subpath)), { type: 'sample', - samples: value, + samples: bank, fileSystem: true, tag: 'local', }); diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs index 40219c9a..404ca647 100644 --- a/website/src/repl/idbutils.mjs +++ b/website/src/repl/idbutils.mjs @@ -49,10 +49,10 @@ export const registerSamplesFromDB = (config = userSamplesDBConfig, onComplete = }); sounds.forEach((soundPaths, key) => { - const value = Array.from(soundPaths); - registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { + const bank = Array.from(soundPaths); + registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), { type: 'sample', - samples: value, + samples: bank, baseUrl: undefined, prebake: false, tag: undefined, From e29876e7f6d265136d9e8871ace6b1235e95a008 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 31 May 2024 13:57:02 -0400 Subject: [PATCH 010/547] debugging --- website/src/repl/idbutils.mjs | 47 ++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs index 959dc7d1..8a95493b 100644 --- a/website/src/repl/idbutils.mjs +++ b/website/src/repl/idbutils.mjs @@ -12,7 +12,7 @@ export const userSamplesDBConfig = { }; // deletes all of the databases, useful for debugging -const clearIDB = () => { +function clearIDB() { window.indexedDB .databases() .then((r) => { @@ -21,12 +21,30 @@ const clearIDB = () => { .then(() => { alert('All data cleared.'); }); -}; +} // queries the DB, and registers the sounds so they can be played -export const registerSamplesFromDB = (config = userSamplesDBConfig, onComplete = () => {}) => { +export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = () => {}) { openDB(config, (objectStore) => { + // const keys = objectStore.getAllKeys(); + + // keys.onsuccess = (e) => { + // console.log(e); + // const result = e.target.result[0]; + // console.log(result); + + // const q = objectStore.get(result); + // q.onerror = (e) => console.error(e.target.error); + // q.onsuccess = (e) => console.log(e); + // }; + let query = objectStore.getAll(); + query.onerror = (e) => { + logger('User Samples failed to load ', 'error'); + onComplete(); + console.error(e?.target?.error); + }; + query.onsuccess = (event) => { const soundFiles = event.target.result; if (!soundFiles?.length) { @@ -64,7 +82,7 @@ export const registerSamplesFromDB = (config = userSamplesDBConfig, onComplete = onComplete(); }; }); -}; +} // creates a blob from a buffer that can be read async function bufferToDataUrl(buf) { return new Promise((resolve) => { @@ -77,7 +95,7 @@ async function bufferToDataUrl(buf) { }); } //open db and initialize it if necessary -const openDB = (config, onOpened) => { +function openDB(config, onOpened) { const { dbName, version, table, columns } = config; if (typeof window === 'undefined') { return; @@ -102,16 +120,15 @@ const openDB = (config, onOpened) => { dbOpen.onsuccess = () => { const db = dbOpen.result; - - const // lock store for writing - writeTransaction = db.transaction([table], 'readwrite'), - // get object store - objectStore = writeTransaction.objectStore(table); + // lock store for writing + const writeTransaction = db.transaction([table], 'readwrite'); + // get object store + const objectStore = writeTransaction.objectStore(table); onOpened(objectStore, db); }; -}; +} -const processFilesForIDB = async (files) => { +async function processFilesForIDB(files) { return await Promise.all( Array.from(files) .map(async (s) => { @@ -139,9 +156,9 @@ const processFilesForIDB = async (files) => { logger('Something went wrong while processing uploaded files', 'error'); console.error(error); }); -}; +} -export const uploadSamplesToDB = async (config, files) => { +export async function uploadSamplesToDB(config, files) { await processFilesForIDB(files).then((files) => { const onOpened = (objectStore, _db) => { files.forEach((file) => { @@ -153,4 +170,4 @@ export const uploadSamplesToDB = async (config, files) => { }; openDB(config, onOpened); }); -}; +} From 375c68775c99c9b50a4bb3e100a923c46f5fa166 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 1 Jun 2024 15:41:55 +0200 Subject: [PATCH 011/547] make sure draw logic works with multiple repls --- packages/codemirror/codemirror.mjs | 6 +++--- packages/core/evaluate.mjs | 4 ++-- packages/core/repl.mjs | 8 +++++++- packages/draw/draw.mjs | 11 ++++------- packages/draw/pianoroll.mjs | 2 +- packages/transpiler/transpiler.mjs | 3 ++- website/src/docs/MiniRepl.jsx | 2 +- 7 files changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 0a722e1d..99c537f5 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -154,9 +154,9 @@ export class StrudelMirror { this.prebaked = prebake(); autodraw && this.drawFirstFrame(); - this.repl = repl({ ...replOptions, + id, onToggle: (started) => { replOptions?.onToggle?.(started); if (started) { @@ -171,11 +171,11 @@ export class StrudelMirror { } else { this.drawer.stop(); updateMiniLocations(this.editor, []); - cleanupDraw(false); + cleanupDraw(false, id); } }, beforeEval: async () => { - cleanupDraw(); + cleanupDraw(true, id); this.painters = []; const self = this; // this is similar to repl.mjs > injectPatternMethods diff --git a/packages/core/evaluate.mjs b/packages/core/evaluate.mjs index ea63d8df..23cd44c1 100644 --- a/packages/core/evaluate.mjs +++ b/packages/core/evaluate.mjs @@ -37,11 +37,11 @@ function safeEval(str, options = {}) { return Function(body)(); } -export const evaluate = async (code, transpiler) => { +export const evaluate = async (code, transpiler, transpilerOptions) => { let meta = {}; if (transpiler) { // transform syntactically correct js code to semantically usable code - const transpiled = transpiler(code); + const transpiled = transpiler(code, transpilerOptions); code = transpiled.output; meta = transpiled; } diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 5b7bcd71..3f3f5f24 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -19,6 +19,7 @@ export function repl({ sync = false, setInterval, clearInterval, + id, }) { const state = { schedulerError: undefined, @@ -32,6 +33,10 @@ export function repl({ started: false, }; + const transpilerOptions = { + id, + }; + const updateState = (update) => { Object.assign(state, update); state.isDirty = state.code !== state.activeCode; @@ -139,9 +144,10 @@ export function repl({ try { updateState({ code, pending: true }); await injectPatternMethods(); + setTime(() => scheduler.now()); // TODO: refactor? await beforeEval?.({ code }); shouldHush && hush(); - let { pattern, meta } = await _evaluate(code, transpiler); + let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); if (Object.keys(pPatterns).length) { pattern = stack(...Object.values(pPatterns)); } diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs index 81123f46..7fe6441e 100644 --- a/packages/draw/draw.mjs +++ b/packages/draw/draw.mjs @@ -36,8 +36,8 @@ function stopAnimationFrame(id) { delete animationFrames[id]; } } -function stopAllAnimations() { - Object.keys(animationFrames).forEach((id) => stopAnimationFrame(id)); +function stopAllAnimations(replID) { + Object.keys(animationFrames).forEach((id) => (!replID || id.startsWith(replID)) && stopAnimationFrame(id)); } let memory = {}; @@ -72,13 +72,10 @@ Pattern.prototype.draw = function (fn, options) { return this; }; -export const cleanupDraw = (clearScreen = true) => { +export const cleanupDraw = (clearScreen = true, id) => { const ctx = getDrawContext(); clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width); - stopAllAnimations(); - if (window.strudelScheduler) { - clearInterval(window.strudelScheduler); - } + stopAllAnimations(id); }; Pattern.prototype.onPaint = function () { diff --git a/packages/draw/pianoroll.mjs b/packages/draw/pianoroll.mjs index 247213b4..d0480ef5 100644 --- a/packages/draw/pianoroll.mjs +++ b/packages/draw/pianoroll.mjs @@ -266,7 +266,7 @@ export function getDrawOptions(drawTime, options = {}) { let [lookbehind, lookahead] = drawTime; lookbehind = Math.abs(lookbehind); const cycles = lookahead + lookbehind; - const playhead = lookbehind / cycles; + const playhead = cycles !== 0 ? lookbehind / cycles : 0; return { fold: 1, ...options, cycles, playhead }; } diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 6cd01499..2e566305 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -69,6 +69,7 @@ export function transpiler(input, options = {}) { to: node.end, index, type, + id: options.id, }; emitWidgets && widgets.push(widgetConfig); return this.replace(widgetWithLocation(node, widgetConfig)); @@ -162,7 +163,7 @@ export function getWidgetID(widgetConfig) { // that means, if we use the index index of line position as id, less garbage is generated // return `widget_${widgetConfig.to}`; // more gargabe //return `widget_${widgetConfig.index}_${widgetConfig.to}`; // also more garbage - return `widget_${widgetConfig.type}_${widgetConfig.index}`; // less garbage + return `${widgetConfig.id || ''}_widget_${widgetConfig.type}_${widgetConfig.index}`; // less garbage } function widgetWithLocation(node, widgetConfig) { diff --git a/website/src/docs/MiniRepl.jsx b/website/src/docs/MiniRepl.jsx index 03e3d6d3..f8e79a0d 100644 --- a/website/src/docs/MiniRepl.jsx +++ b/website/src/docs/MiniRepl.jsx @@ -34,7 +34,7 @@ export function MiniRepl({ const canvasId = useMemo(() => `canvas-${id}`, [id]); const shouldDraw = !!punchcard || !!claviature; const shouldShowCanvas = !!punchcard; - const drawTime = punchcard ? [0, 4] : [0, 0]; + const drawTime = punchcard ? [0, 4] : [-2, 2]; const [activeNotes, setActiveNotes] = useState([]); const init = useCallback(({ code, shouldDraw }) => { From 0e6a17fc77a004a1a0c6038613ab097c690b1310 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 1 Jun 2024 15:42:09 +0200 Subject: [PATCH 012/547] start visual feedback article --- website/src/config.ts | 1 + website/src/pages/learn/visual-feedback.mdx | 63 +++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 website/src/pages/learn/visual-feedback.mdx diff --git a/website/src/config.ts b/website/src/config.ts index 9dee2aac..dd003c18 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -78,6 +78,7 @@ export const SIDEBAR: Sidebar = { More: [ { text: 'Recipes', link: 'recipes/recipes' }, { text: 'Mini-Notation', link: 'learn/mini-notation' }, + { text: 'Visual Feedback', link: 'learn/visual-feedback' }, { text: 'Offline', link: 'learn/pwa' }, { text: 'Patterns', link: 'technical-manual/patterns' }, { text: 'Music metadata', link: 'learn/metadata' }, diff --git a/website/src/pages/learn/visual-feedback.mdx b/website/src/pages/learn/visual-feedback.mdx new file mode 100644 index 00000000..b854cd0b --- /dev/null +++ b/website/src/pages/learn/visual-feedback.mdx @@ -0,0 +1,63 @@ +--- +title: Visual Feedback +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import { JsDoc } from '../../docs/JsDoc'; + +# Visual Feedback + +There are several function that add visual feedback to your patterns. + +## Mini Notation Highlighting + +When you write mini notation with "double quotes" or \`backticks\`, the active parts of the mini notation will be highlighted: + +*8") +.scale("/4:minor:pentatonic") +.s("supersaw").lpf(300).lpenv("<4 3 2>*4")`} +/> + +You can change the color as well, even pattern it: + +*8") +.scale("/4:minor:pentatonic") +.s("supersaw").lpf(300).lpenv("<4 3 2>*4") +.color("cyan magenta")`} +/> + +## Pianoroll and Punchcard + +*8") +.scale("/4:minor:pentatonic") +.s("supersaw").lpf(300).lpenv("<4 3 2>*4") +._punchcard() +`} +/> + +*8") +.scale("/4:minor:pentatonic") +.s("supersaw").lpf(300).lpenv("<4 3 2>*4") +._pianoroll() +`} +/> + +*8") +.scale("/4:minor:pentatonic") +.s("supersaw").lpf(300).lpenv("<4 3 2>*4") +._spiral() +`} +/> + +{/* */} From 1fca6f25b8cb7d84bf643d13634f43dddb2e9ef4 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 1 Jun 2024 11:35:24 -0400 Subject: [PATCH 013/547] faster upload --- website/src/repl/files.mjs | 3 +- website/src/repl/idbutils.mjs | 78 +++++++++++++++++++++++++---------- 2 files changed, 59 insertions(+), 22 deletions(-) diff --git a/website/src/repl/files.mjs b/website/src/repl/files.mjs index 63ac5f85..3e3db94d 100644 --- a/website/src/repl/files.mjs +++ b/website/src/repl/files.mjs @@ -75,7 +75,8 @@ export const walkFileTree = (node, fn) => { } }; -export const isAudioFile = (filename) => ['wav', 'mp3'].includes(filename.split('.').slice(-1)[0]); +export const isAudioFile = (filename) => + ['wav', 'mp3', 'flac', 'ogg', 'm4a'].includes(filename.split('.').slice(-1)[0]); function uint8ArrayToDataURL(uint8Array) { const blob = new Blob([uint8Array], { type: 'audio/*' }); diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs index 8a95493b..54f98216 100644 --- a/website/src/repl/idbutils.mjs +++ b/website/src/repl/idbutils.mjs @@ -50,32 +50,39 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = if (!soundFiles?.length) { return; } + console.log(soundFiles); const sounds = new Map(); [...soundFiles] .sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' })) - .forEach((soundFile) => { + .forEach((soundFile, i) => { const title = soundFile.title; if (!isAudioFile(title)) { return; } const splitRelativePath = soundFile.id.split('/'); - const parentDirectory = + let parentDirectory = //fallback to file name before period and seperator if no parent directory splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user'; - const soundPath = soundFile.blob; + + const soundPath = bufferToDataUrl(soundFile.buf); + + // const soundPath = soundFile.blob; const soundPaths = sounds.get(parentDirectory) ?? new Set(); soundPaths.add(soundPath); sounds.set(parentDirectory, soundPaths); }); sounds.forEach((soundPaths, key) => { - const value = Array.from(soundPaths); - registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { - type: 'sample', - samples: value, - baseUrl: undefined, - prebake: false, - tag: undefined, + const paths = Array.from(soundPaths); + Promise.all(paths).then((value) => { + // console.log({ value }); + registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { + type: 'sample', + samples: value, + baseUrl: undefined, + prebake: false, + tag: undefined, + }); }); }); logger('imported sounds registered!', 'success'); @@ -94,6 +101,21 @@ async function bufferToDataUrl(buf) { reader.readAsDataURL(blob); }); } + +function bufferToBlob(buf) { + return new Blob([buf], { type: 'application/octet-binary' }); +} + +async function blobToDataUrl(blob) { + return new Promise((resolve) => { + var reader = new FileReader(); + reader.onload = function (event) { + resolve(event.target.result); + }; + reader.readAsDataURL(blob); + }); +} + //open db and initialize it if necessary function openDB(config, onOpened) { const { dbName, version, table, columns } = config; @@ -126,12 +148,13 @@ function openDB(config, onOpened) { const objectStore = writeTransaction.objectStore(table); onOpened(objectStore, db); }; + return dbOpen; } async function processFilesForIDB(files) { - return await Promise.all( + return Promise.all( Array.from(files) - .map(async (s) => { + .map((s) => { const title = s.name; if (!isAudioFile(title)) { @@ -140,16 +163,24 @@ async function processFilesForIDB(files) { //create obscured url to file system that can be fetched const sUrl = URL.createObjectURL(s); //fetch the sound and turn it into a buffer array - const buf = await fetch(sUrl).then((res) => res.arrayBuffer()); + return fetch(sUrl).then((res) => { + return res.arrayBuffer().then((buf) => { + const path = s.webkitRelativePath; + let id = path?.length ? path : title; + if (id == null || title == null || buf == null) { + return; + } + return { + title, + buf, + // blob: base64, + id, + }; + }); + }); + //create a url base64 containing all of the buffer data - const base64 = await bufferToDataUrl(buf); - const path = s.webkitRelativePath; - const id = path?.length ? path : title; - return { - title, - blob: base64, - id, - }; + // const base64 = await bufferToDataUrl(buf); }) .filter(Boolean), ).catch((error) => { @@ -159,14 +190,19 @@ async function processFilesForIDB(files) { } export async function uploadSamplesToDB(config, files) { + logger('procesing user samples...'); await processFilesForIDB(files).then((files) => { + console.log(files); + logger('user samples processed... opening db'); const onOpened = (objectStore, _db) => { + logger('index db opened... writing files to db'); files.forEach((file) => { if (file == null) { return; } objectStore.put(file); }); + logger('user samples written successfully'); }; openDB(config, onOpened); }); From 1fd9ba5dbfad00c0986332e958be7b27f872c532 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 1 Jun 2024 12:11:28 -0400 Subject: [PATCH 014/547] convert to blob --- website/src/repl/idbutils.mjs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs index 54f98216..a6b19179 100644 --- a/website/src/repl/idbutils.mjs +++ b/website/src/repl/idbutils.mjs @@ -64,7 +64,7 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = //fallback to file name before period and seperator if no parent directory splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user'; - const soundPath = bufferToDataUrl(soundFile.buf); + const soundPath = blobToDataUrl(soundFile.blob); // const soundPath = soundFile.blob; const soundPaths = sounds.get(parentDirectory) ?? new Set(); @@ -164,16 +164,16 @@ async function processFilesForIDB(files) { const sUrl = URL.createObjectURL(s); //fetch the sound and turn it into a buffer array return fetch(sUrl).then((res) => { - return res.arrayBuffer().then((buf) => { + return res.blob().then((blob) => { const path = s.webkitRelativePath; let id = path?.length ? path : title; - if (id == null || title == null || buf == null) { + if (id == null || title == null || blob == null) { return; } return { title, - buf, - // blob: base64, + // buf, + blob, id, }; }); From bf7fe2bf755c45b220c95df97ba10e6af3e6b608 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 1 Jun 2024 12:39:45 -0400 Subject: [PATCH 015/547] fixed indexdb --- packages/superdough/superdough.mjs | 2 +- website/src/repl/idbutils.mjs | 90 +++++++++++------------------- 2 files changed, 33 insertions(+), 59 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 018caa9a..e663fc6d 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -99,7 +99,7 @@ export const connectToDestination = (input, channels = [0, 1]) => { //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); + input?.connect(stereoMix); const splitter = new ChannelSplitterNode(ctx, { numberOfOutputs: stereoMix.channelCount, diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs index a6b19179..1c46c5d7 100644 --- a/website/src/repl/idbutils.mjs +++ b/website/src/repl/idbutils.mjs @@ -26,19 +26,7 @@ function clearIDB() { // queries the DB, and registers the sounds so they can be played export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = () => {}) { openDB(config, (objectStore) => { - // const keys = objectStore.getAllKeys(); - - // keys.onsuccess = (e) => { - // console.log(e); - // const result = e.target.result[0]; - // console.log(result); - - // const q = objectStore.get(result); - // q.onerror = (e) => console.error(e.target.error); - // q.onsuccess = (e) => console.log(e); - // }; - - let query = objectStore.getAll(); + const query = objectStore.getAll(); query.onerror = (e) => { logger('User Samples failed to load ', 'error'); onComplete(); @@ -50,32 +38,36 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = if (!soundFiles?.length) { return; } - console.log(soundFiles); const sounds = new Map(); - [...soundFiles] - .sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' })) - .forEach((soundFile, i) => { - const title = soundFile.title; - if (!isAudioFile(title)) { - return; - } - const splitRelativePath = soundFile.id.split('/'); - let parentDirectory = - //fallback to file name before period and seperator if no parent directory - splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user'; - const soundPath = blobToDataUrl(soundFile.blob); + Promise.all( + [...soundFiles] + .sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' })) + .map((soundFile, i) => { + const title = soundFile.title; + if (!isAudioFile(title)) { + return; + } + const splitRelativePath = soundFile.id.split('/'); + let parentDirectory = + //fallback to file name before period and seperator if no parent directory + splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user'; + const blob = soundFile.blob; - // const soundPath = soundFile.blob; - const soundPaths = sounds.get(parentDirectory) ?? new Set(); - soundPaths.add(soundPath); - sounds.set(parentDirectory, soundPaths); - }); + // Files used to be uploaded as base64 strings, After Jan 1 2025 this check can be safely deleted + if (typeof blob === 'string') { + return blob; + } - sounds.forEach((soundPaths, key) => { - const paths = Array.from(soundPaths); - Promise.all(paths).then((value) => { - // console.log({ value }); + return blobToDataUrl(blob).then((soundPath) => { + const soundPaths = sounds.get(parentDirectory) ?? new Set(); + soundPaths.add(soundPath); + sounds.set(parentDirectory, soundPaths); + }); + }), + ).then(() => { + sounds.forEach((soundPaths, key) => { + const value = Array.from(soundPaths); registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { type: 'sample', samples: value, @@ -84,26 +76,12 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = tag: undefined, }); }); - }); - logger('imported sounds registered!', 'success'); - onComplete(); - }; - }); -} -// creates a blob from a buffer that can be read -async function bufferToDataUrl(buf) { - return new Promise((resolve) => { - var blob = new Blob([buf], { type: 'application/octet-binary' }); - var reader = new FileReader(); - reader.onload = function (event) { - resolve(event.target.result); - }; - reader.readAsDataURL(blob); - }); -} -function bufferToBlob(buf) { - return new Blob([buf], { type: 'application/octet-binary' }); + logger('imported sounds registered!', 'success'); + onComplete(); + }); + }; + }); } async function blobToDataUrl(blob) { @@ -172,15 +150,11 @@ async function processFilesForIDB(files) { } return { title, - // buf, blob, id, }; }); }); - - //create a url base64 containing all of the buffer data - // const base64 = await bufferToDataUrl(buf); }) .filter(Boolean), ).catch((error) => { From 321a88892100fa4ea38e50f04f047cd5babdd54f Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 1 Jun 2024 12:42:07 -0400 Subject: [PATCH 016/547] add catch --- website/src/repl/idbutils.mjs | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs index 1c46c5d7..c49362da 100644 --- a/website/src/repl/idbutils.mjs +++ b/website/src/repl/idbutils.mjs @@ -65,21 +65,26 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = sounds.set(parentDirectory, soundPaths); }); }), - ).then(() => { - sounds.forEach((soundPaths, key) => { - const value = Array.from(soundPaths); - registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { - type: 'sample', - samples: value, - baseUrl: undefined, - prebake: false, - tag: undefined, + ) + .then(() => { + sounds.forEach((soundPaths, key) => { + const value = Array.from(soundPaths); + registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { + type: 'sample', + samples: value, + baseUrl: undefined, + prebake: false, + tag: undefined, + }); }); - }); - logger('imported sounds registered!', 'success'); - onComplete(); - }); + logger('imported sounds registered!', 'success'); + onComplete(); + }) + .catch((error) => { + logger('Something went wrong while registering saved samples from the index db', 'error'); + console.error(error); + }); }; }); } From 6b2d080afcfbcd402bf1f97e11d541e034fb1edc Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 1 Jun 2024 19:17:00 -0400 Subject: [PATCH 017/547] remove unessecary change --- packages/superdough/superdough.mjs | 2 +- website/src/repl/idbutils.mjs | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index e663fc6d..018caa9a 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -99,7 +99,7 @@ export const connectToDestination = (input, channels = [0, 1]) => { //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); + input.connect(stereoMix); const splitter = new ChannelSplitterNode(ctx, { numberOfOutputs: stereoMix.channelCount, diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs index c49362da..f5cbf52b 100644 --- a/website/src/repl/idbutils.mjs +++ b/website/src/repl/idbutils.mjs @@ -56,19 +56,24 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = // Files used to be uploaded as base64 strings, After Jan 1 2025 this check can be safely deleted if (typeof blob === 'string') { - return blob; + const soundPaths = sounds.get(parentDirectory) ?? new Set(); + soundPaths.add(blob); + sounds.set(parentDirectory, soundPaths); + return; } return blobToDataUrl(blob).then((soundPath) => { const soundPaths = sounds.get(parentDirectory) ?? new Set(); soundPaths.add(soundPath); sounds.set(parentDirectory, soundPaths); + return; }); }), ) .then(() => { sounds.forEach((soundPaths, key) => { const value = Array.from(soundPaths); + registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { type: 'sample', samples: value, From ffa21ddf0df30e9452e5ee894ef1fff9c101e7e1 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 1 Jun 2024 19:19:47 -0400 Subject: [PATCH 018/547] remove console statement --- website/src/repl/idbutils.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs index f5cbf52b..70ccd8f8 100644 --- a/website/src/repl/idbutils.mjs +++ b/website/src/repl/idbutils.mjs @@ -176,7 +176,6 @@ async function processFilesForIDB(files) { export async function uploadSamplesToDB(config, files) { logger('procesing user samples...'); await processFilesForIDB(files).then((files) => { - console.log(files); logger('user samples processed... opening db'); const onOpened = (objectStore, _db) => { logger('index db opened... writing files to db'); From 94e411aa7aadb025c64ca8d057b7b9da08a50e8c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 2 Jun 2024 02:34:33 +0200 Subject: [PATCH 019/547] add beforeStart callback --- packages/core/cyclist.mjs | 20 ++++++++++++++++---- packages/core/repl.mjs | 8 +++++--- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 08b89375..472e83a8 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -8,8 +8,19 @@ import createClock from './zyklus.mjs'; import { logger } from './logger.mjs'; export class Cyclist { - constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1, setInterval, clearInterval }) { + constructor({ + interval, + onTrigger, + onToggle, + onError, + getTime, + latency = 0.1, + setInterval, + clearInterval, + beforeStart, + }) { this.started = false; + this.beforeStart = beforeStart; this.cps = 0.5; this.num_ticks_since_cps_change = 0; this.lastTick = 0; // absolute time when last tick (clock callback) happened @@ -82,7 +93,8 @@ export class Cyclist { this.started = v; this.onToggle?.(v); } - start() { + async start() { + await this.beforeStart?.(); this.num_ticks_since_cps_change = 0; this.num_cycles_at_cps_change = 0; if (!this.pattern) { @@ -103,10 +115,10 @@ export class Cyclist { this.lastEnd = 0; this.setStarted(false); } - setPattern(pat, autostart = false) { + async setPattern(pat, autostart = false) { this.pattern = pat; if (autostart && !this.started) { - this.start(); + await this.start(); } } setCps(cps = 0.5) { diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 3f3f5f24..f21ab847 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -10,6 +10,7 @@ export function repl({ defaultOutput, onEvalError, beforeEval, + beforeStart, afterEval, getTime, transpiler, @@ -53,6 +54,7 @@ export function repl({ }, setInterval, clearInterval, + beforeStart, }; // NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome @@ -69,9 +71,9 @@ export function repl({ return silence; }; - const setPattern = (pattern, autostart = true) => { + const setPattern = async (pattern, autostart = true) => { pattern = editPattern?.(pattern) || pattern; - scheduler.setPattern(pattern, autostart); + await scheduler.setPattern(pattern, autostart); }; setTime(() => scheduler.now()); // TODO: refactor? @@ -159,7 +161,7 @@ export function repl({ throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.')); } logger(`[eval] code updated`); - setPattern(pattern, autostart); + await setPattern(pattern, autostart); updateState({ miniLocations: meta?.miniLocations || [], widgets: meta?.widgets || [], From faba4a4e4ec05b6303dbea1eb4f7770899637d86 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 2 Jun 2024 03:00:57 +0200 Subject: [PATCH 020/547] implement onPaint with pattern state --- packages/core/pattern.mjs | 8 ++++++++ packages/draw/draw.mjs | 25 +++++++++++++++++++------ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 03ce2e2d..918a924b 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -94,6 +94,14 @@ export class Pattern { return result; } + // runs func on query state + withState(func) { + return this.withHaps((haps, state) => { + func(state); + return haps; + }); + } + /** * see `withValue` * @noAutocomplete diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs index 7fe6441e..e3737600 100644 --- a/packages/draw/draw.mjs +++ b/packages/draw/draw.mjs @@ -74,13 +74,23 @@ Pattern.prototype.draw = function (fn, options) { export const cleanupDraw = (clearScreen = true, id) => { const ctx = getDrawContext(); - clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width); + clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); stopAllAnimations(id); }; -Pattern.prototype.onPaint = function () { - console.warn('[draw] onPaint was not overloaded. Some drawings might not work'); - return this; +Pattern.prototype.onPaint = function (painter) { + return this.withState((state) => { + if (!state.controls.painters) { + state.controls.painters = []; + } + state.controls.painters.push(painter); + }); +}; + +Pattern.prototype.getPainters = function () { + let painters = []; + this.queryArc(0, 0, { painters }); + return painters; }; // const round = (x) => Math.round(x * 1000) / 1000; @@ -119,6 +129,7 @@ export class Drawer { this.visibleHaps = []; this.lastFrame = null; this.drawTime = drawTime; + this.painters = []; this.framer = new Framer( () => { if (!this.scheduler) { @@ -143,7 +154,7 @@ export class Drawer { // add new haps with onset (think right edge bars scrolling in) .concat(haps.filter((h) => h.hasOnset())); const time = phase - lookahead; - onDraw(this.visibleHaps, time, this); + onDraw(this.visibleHaps, time, this, this.painters); }, (err) => { console.warn('draw error', err); @@ -161,11 +172,13 @@ export class Drawer { t = t ?? scheduler.now(); this.scheduler = scheduler; let [_, lookahead] = this.drawTime; + // +0.1 = workaround for weird holes in query.. const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1]; // remove all future haps this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t); + this.painters = []; // will get populated by .onPaint calls attached to the pattern // query future haps - const futureHaps = scheduler.pattern.queryArc(begin, end); // +0.1 = workaround for weird holes in query.. + const futureHaps = scheduler.pattern.queryArc(begin, end, { painters: this.painters }); // append future haps this.visibleHaps = this.visibleHaps.concat(futureHaps); } From bf391ad6ce4352432dd7090a3247cdc175b98114 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 2 Jun 2024 03:03:36 +0200 Subject: [PATCH 021/547] simplify: painters are now handled by Drawer --- packages/codemirror/codemirror.mjs | 35 ++++++++++-------------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 99c537f5..9bb1df09 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -140,16 +140,15 @@ export class StrudelMirror { this.root = root; this.miniLocations = []; this.widgets = []; - this.painters = []; this.drawTime = drawTime; this.drawContext = drawContext; this.onDraw = onDraw || this.draw; this.id = id || s4(); - this.drawer = new Drawer((haps, time) => { + this.drawer = new Drawer((haps, time, _, painters) => { const currentFrame = haps.filter((hap) => hap.isActive(time)); this.highlight(currentFrame, time); - this.onDraw(haps, time, this.painters); + this.onDraw(haps, time, painters); }, drawTime); this.prebaked = prebake(); @@ -160,7 +159,6 @@ export class StrudelMirror { onToggle: (started) => { replOptions?.onToggle?.(started); if (started) { - this.adjustDrawTime(); this.drawer.start(this.repl.scheduler); // stop other repls when this one is started document.dispatchEvent( @@ -171,20 +169,11 @@ export class StrudelMirror { } else { this.drawer.stop(); updateMiniLocations(this.editor, []); - cleanupDraw(false, id); + cleanupDraw(true, id); } }, beforeEval: async () => { cleanupDraw(true, id); - this.painters = []; - const self = this; - // this is similar to repl.mjs > injectPatternMethods - // maybe there is a solution without prototype hacking, but hey, it works - // we need to do this befor every eval to make sure it works with multiple StrudelMirror's side by side - Pattern.prototype.onPaint = function (onPaint) { - self.painters.push(onPaint); - return this; - }; await this.prebaked; await replOptions?.beforeEval?.(); }, @@ -198,8 +187,11 @@ export class StrudelMirror { updateWidgets(this.editor, widgets); updateMiniLocations(this.editor, this.miniLocations); replOptions?.afterEval?.(options); - this.adjustDrawTime(); - this.drawer.invalidate(); + // if no painters are set (.onPaint was not called), then we only need the present moment (for highlighting) + const drawTime = options.pattern.getPainters().length ? this.drawTime : [0, 0]; + this.drawer.setDrawTime(drawTime); + // invalidate drawer after we've set the appropriate drawTime + this.drawer.invalidate(this.repl.scheduler); }, }); this.editor = initEditor({ @@ -234,13 +226,8 @@ export class StrudelMirror { }; document.addEventListener('start-repl', this.onStartRepl); } - // adjusts draw time depending on if there are painters - adjustDrawTime() { - // when no painters are set, [0,0] is enough (just highlighting) - this.drawer.setDrawTime(this.painters.length ? this.drawTime : [0, 0]); - } - draw(haps, time) { - this.painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime)); + draw(haps, time, painters) { + painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime)); } async drawFirstFrame() { if (!this.onDraw) { @@ -252,7 +239,7 @@ export class StrudelMirror { await this.repl.evaluate(this.code, false); this.drawer.invalidate(this.repl.scheduler, -0.001); // draw at -0.001 to avoid haps at 0 to be visualized as active - this.onDraw?.(this.drawer.visibleHaps, -0.001, this.painters); + this.onDraw?.(this.drawer.visibleHaps, -0.001, this.drawer.painters); } catch (err) { console.warn('first frame could not be painted'); } From aadb2fee84035a13aa9f566ee8a4a744bf8fbdd0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 2 Jun 2024 03:05:26 +0200 Subject: [PATCH 022/547] support onload inline viz with autodraw --- website/src/docs/MiniRepl.jsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/website/src/docs/MiniRepl.jsx b/website/src/docs/MiniRepl.jsx index f8e79a0d..37ace3d6 100644 --- a/website/src/docs/MiniRepl.jsx +++ b/website/src/docs/MiniRepl.jsx @@ -1,7 +1,7 @@ import { useState, useRef, useCallback, useMemo, useEffect } from 'react'; import { Icon } from './Icon'; import { silence, noteToMidi, _mod } from '@strudel/core'; -import { getPunchcardPainter } from '@strudel/draw'; +import { getDrawContext, getPunchcardPainter } from '@strudel/draw'; import { transpiler } from '@strudel/transpiler'; import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel/webaudio'; import { StrudelMirror } from '@strudel/codemirror'; @@ -28,24 +28,25 @@ export function MiniRepl({ claviature, claviatureLabels, maxHeight, + autodraw, }) { const code = tunes ? tunes[0] : tune; const id = useMemo(() => s4(), []); const canvasId = useMemo(() => `canvas-${id}`, [id]); - const shouldDraw = !!punchcard || !!claviature; + autodraw = !!punchcard || !!claviature || !!autodraw; const shouldShowCanvas = !!punchcard; const drawTime = punchcard ? [0, 4] : [-2, 2]; const [activeNotes, setActiveNotes] = useState([]); - const init = useCallback(({ code, shouldDraw }) => { - const drawContext = shouldDraw ? document.querySelector('#' + canvasId)?.getContext('2d') : null; + const init = useCallback(({ code, autodraw }) => { + const drawContext = canvasId ? document.querySelector('#' + canvasId)?.getContext('2d') : getDrawContext(); const editor = new StrudelMirror({ id, defaultOutput: webaudioOutput, getTime: () => getAudioContext().currentTime, transpiler, - autodraw: !!shouldDraw, + autodraw, root: containerRef.current, initialCode: '// LOADING', pattern: silence, @@ -73,7 +74,7 @@ export function MiniRepl({ onUpdateState: (state) => { setReplState({ ...state }); }, - beforeEval: () => audioReady, // not doing this in prebake to make sure viz is drawn + beforeStart: () => audioReady, afterEval: ({ code }) => setVersionDefaultsFrom(code), }); // init settings @@ -152,7 +153,7 @@ export function MiniRepl({ ref={(el) => { if (!editorRef.current) { containerRef.current = el; - init({ code, shouldDraw }); + init({ code, autodraw }); } }} >
From a7650f1af355b19f5057f051cc71f14400df121d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 2 Jun 2024 03:08:16 +0200 Subject: [PATCH 023/547] add autodraw flag + scope --- website/src/pages/learn/visual-feedback.mdx | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/website/src/pages/learn/visual-feedback.mdx b/website/src/pages/learn/visual-feedback.mdx index b854cd0b..cd76ca79 100644 --- a/website/src/pages/learn/visual-feedback.mdx +++ b/website/src/pages/learn/visual-feedback.mdx @@ -18,7 +18,7 @@ When you write mini notation with "double quotes" or \`backticks\`, the active p client:idle tune={`n("<0 2 1 3 2>*8") .scale("/4:minor:pentatonic") -.s("supersaw").lpf(300).lpenv("<4 3 2>*4")`} +.s("supersaw").lpf(300).lpenv("<4 3 2>\*4")`} /> You can change the color as well, even pattern it: @@ -40,17 +40,21 @@ You can change the color as well, even pattern it: .s("supersaw").lpf(300).lpenv("<4 3 2>*4") ._punchcard() `} + autodraw /> *8") .scale("/4:minor:pentatonic") -.s("supersaw").lpf(300).lpenv("<4 3 2>*4") -._pianoroll() +.s("supersaw").lpf(300).lpenv("<4 3 2>\*4") +.\_pianoroll() `} + autodraw /> +## Spiral + *8") @@ -60,4 +64,15 @@ You can change the color as well, even pattern it: `} /> +## Scope + +*8") +.scale("/4:minor:pentatonic") +.s("supersaw").lpf(300).lpenv("<4 3 2>*4") +._scope() +`} +/> + {/* */} From 3dc5fd0e7e328a44f79656f8d8ed99c5ade51493 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 2 Jun 2024 10:48:20 +0200 Subject: [PATCH 024/547] fix: editPattern bug --- packages/core/repl.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index f21ab847..016eca3e 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -74,6 +74,7 @@ export function repl({ const setPattern = async (pattern, autostart = true) => { pattern = editPattern?.(pattern) || pattern; await scheduler.setPattern(pattern, autostart); + return pattern; }; setTime(() => scheduler.now()); // TODO: refactor? @@ -161,7 +162,7 @@ export function repl({ throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.')); } logger(`[eval] code updated`); - await setPattern(pattern, autostart); + pattern = await setPattern(pattern, autostart); updateState({ miniLocations: meta?.miniLocations || [], widgets: meta?.widgets || [], From bf9086768517d5fba18eedae7d633a9ce0cdd69f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 2 Jun 2024 10:49:07 +0200 Subject: [PATCH 025/547] fix: mini repl punchcard + claviature flags --- website/src/docs/MiniRepl.jsx | 10 ++++++--- website/src/pages/learn/visual-feedback.mdx | 23 ++++++++++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/website/src/docs/MiniRepl.jsx b/website/src/docs/MiniRepl.jsx index 37ace3d6..dd856d26 100644 --- a/website/src/docs/MiniRepl.jsx +++ b/website/src/docs/MiniRepl.jsx @@ -29,13 +29,17 @@ export function MiniRepl({ claviatureLabels, maxHeight, autodraw, + drawTime, }) { const code = tunes ? tunes[0] : tune; const id = useMemo(() => s4(), []); const canvasId = useMemo(() => `canvas-${id}`, [id]); autodraw = !!punchcard || !!claviature || !!autodraw; const shouldShowCanvas = !!punchcard; - const drawTime = punchcard ? [0, 4] : [-2, 2]; + drawTime = drawTime ?? punchcard ? [0, 4] : [-2, 2]; + if (claviature) { + drawTime = [0, 0]; + } const [activeNotes, setActiveNotes] = useState([]); const init = useCallback(({ code, autodraw }) => { @@ -57,7 +61,7 @@ export function MiniRepl({ pat = pat.onTrigger(onTrigger, false); } if (claviature) { - editor?.painters.push((ctx, time, haps, drawTime) => { + pat = pat.onPaint((ctx, time, haps, drawTime) => { const active = haps .map((hap) => hap.value.note) .filter(Boolean) @@ -66,7 +70,7 @@ export function MiniRepl({ }); } if (punchcard) { - editor?.painters.push(getPunchcardPainter({ labels: !!punchcardLabels })); + pat = pat.punchcard({ labels: !!punchcardLabels }); } return pat; }, diff --git a/website/src/pages/learn/visual-feedback.mdx b/website/src/pages/learn/visual-feedback.mdx index cd76ca79..ed5b16c2 100644 --- a/website/src/pages/learn/visual-feedback.mdx +++ b/website/src/pages/learn/visual-feedback.mdx @@ -38,8 +38,7 @@ You can change the color as well, even pattern it: tune={`n("<0 2 1 3 2>*8") .scale("/4:minor:pentatonic") .s("supersaw").lpf(300).lpenv("<4 3 2>*4") -._punchcard() -`} +._punchcard()`} autodraw /> @@ -48,8 +47,7 @@ You can change the color as well, even pattern it: tune={`n("<0 2 1 3 2>*8") .scale("/4:minor:pentatonic") .s("supersaw").lpf(300).lpenv("<4 3 2>\*4") -.\_pianoroll() -`} +.\_pianoroll()`} autodraw /> @@ -60,8 +58,8 @@ You can change the color as well, even pattern it: tune={`n("<0 2 1 3 2>*8") .scale("/4:minor:pentatonic") .s("supersaw").lpf(300).lpenv("<4 3 2>*4") -._spiral() -`} +._spiral()`} + autodraw /> ## Scope @@ -71,8 +69,17 @@ You can change the color as well, even pattern it: tune={`n("<0 2 1 3 2>*8") .scale("/4:minor:pentatonic") .s("supersaw").lpf(300).lpenv("<4 3 2>*4") -._scope() -`} +._scope()`} +/> + +## Pitchwheel + +*8") +.scale("/4:minor:pentatonic") +.s("supersaw").lpf(300).lpenv("<4 3 2>*4") +._pitchwheel()`} /> {/* */} From 4a87df642685889927777d478b90063616d89fe4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 2 Jun 2024 14:00:47 +0200 Subject: [PATCH 026/547] small example tweaks --- website/src/pages/workshop/first-effects.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/src/pages/workshop/first-effects.mdx b/website/src/pages/workshop/first-effects.mdx index 4a152224..220a7cbc 100644 --- a/website/src/pages/workshop/first-effects.mdx +++ b/website/src/pages/workshop/first-effects.mdx @@ -154,7 +154,7 @@ Can you guess what they do? tune={`stack( note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2") .sound("gm_electric_guitar_muted"), -sound("").bank("RolandTR707") +sound("bd rim").bank("RolandTR707") ).delay(".5")`} /> @@ -202,7 +202,7 @@ Add a delay too! tune={`stack( note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2") .sound("gm_electric_guitar_muted").delay(.5), -sound("").bank("RolandTR707").delay(.5), +sound("bd rim").bank("RolandTR707").delay(.5), n("<4 [3@3 4] [<2 0> ~@16] ~>") .scale("D4:minor").sound("gm_accordion:2") .room(2).gain(.5) @@ -216,7 +216,7 @@ Let's add a bass to make this complete: tune={`stack( note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2") .sound("gm_electric_guitar_muted").delay(.5), -sound("").bank("RolandTR707").delay(.5), +sound("bd rim").bank("RolandTR707").delay(.5), n("<4 [3@3 4] [<2 0> ~@16] ~>") .scale("D4:minor").sound("gm_accordion:2") .room(2).gain(.4), @@ -266,7 +266,7 @@ By the way, inside Mini-Notation, `fast` is `*` and `slow` is `/`. Instead of changing values stepwise, we can also control them with signals: - + From d0510f862a022b86d0881e814e1ab73f1e235c25 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 2 Jun 2024 14:02:39 +0200 Subject: [PATCH 027/547] clear hydra in mini repl --- website/src/docs/MiniRepl.jsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/src/docs/MiniRepl.jsx b/website/src/docs/MiniRepl.jsx index dd856d26..3559b341 100644 --- a/website/src/docs/MiniRepl.jsx +++ b/website/src/docs/MiniRepl.jsx @@ -1,6 +1,7 @@ import { useState, useRef, useCallback, useMemo, useEffect } from 'react'; import { Icon } from './Icon'; import { silence, noteToMidi, _mod } from '@strudel/core'; +import { clearHydra } from '@strudel/hydra'; import { getDrawContext, getPunchcardPainter } from '@strudel/draw'; import { transpiler } from '@strudel/transpiler'; import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel/webaudio'; @@ -78,6 +79,11 @@ export function MiniRepl({ onUpdateState: (state) => { setReplState({ ...state }); }, + onToggle: (playing) => { + if (!playing) { + clearHydra(); + } + }, beforeStart: () => audioReady, afterEval: ({ code }) => setVersionDefaultsFrom(code), }); From b6e277f1ea76eaea8ccfa67d73776eb4e92a2e65 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 2 Jun 2024 14:03:05 +0200 Subject: [PATCH 028/547] use inline scope in examples --- packages/core/controls.mjs | 16 ++++++++++------ test/runtime.mjs | 4 ++++ website/src/pages/learn/synths.mdx | 23 ++++++++++++----------- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 2e8d40f7..82f94394 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -191,7 +191,7 @@ export const { attack, att } = registerControl('attack', 'att'); * note("c e g b g e") * .fm(4) * .fmh("<1 2 1.5 1.61>") - * .scope() + * ._scope() * */ export const { fmh } = registerControl(['fmh', 'fmi'], 'fmh'); @@ -205,7 +205,7 @@ export const { fmh } = registerControl(['fmh', 'fmi'], 'fmh'); * @example * note("c e g b g e") * .fm("<0 1 2 8 32>") - * .scope() + * ._scope() * */ export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm'); @@ -221,7 +221,7 @@ export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm'); * .fmdecay(.2) * .fmsustain(0) * .fmenv("") - * .scope() + * ._scope() * */ export const { fmenv } = registerControl('fmenv'); @@ -234,7 +234,7 @@ export const { fmenv } = registerControl('fmenv'); * note("c e g b g e") * .fm(4) * .fmattack("<0 .05 .1 .2>") - * .scope() + * ._scope() * */ export const { fmattack } = registerControl('fmattack'); @@ -248,7 +248,7 @@ export const { fmattack } = registerControl('fmattack'); * .fm(4) * .fmdecay("<.01 .05 .1 .2>") * .fmsustain(.4) - * .scope() + * ._scope() * */ export const { fmdecay } = registerControl('fmdecay'); @@ -262,7 +262,7 @@ export const { fmdecay } = registerControl('fmdecay'); * .fm(4) * .fmdecay(.1) * .fmsustain("<1 .75 .5 0>") - * .scope() + * ._scope() * */ export const { fmsustain } = registerControl('fmsustain'); @@ -800,10 +800,12 @@ export const { fanchor } = registerControl('fanchor'); * @example * note("a e") * .vib("<.5 1 2 4 8 16>") + * ._scope() * @example * // change the modulation depth with ":" * note("a e") * .vib("<.5 1 2 4 8 16>:12") + * ._scope() */ export const { vib, vibrato, v } = registerControl(['vib', 'vibmod'], 'vibrato', 'v'); /** @@ -824,10 +826,12 @@ export const { noise } = registerControl('noise'); * @example * note("a e").vib(4) * .vibmod("<.25 .5 1 2 12>") + * ._scope() * @example * // change the vibrato frequency with ":" * note("a e") * .vibmod("<.25 .5 1 2 12>:8") + * ._scope() */ export const { vibmod, vmod } = registerControl(['vibmod', 'vib'], 'vmod'); export const { hcutoff, hpf, hp } = registerControl(['hcutoff', 'hresonance', 'hpenv'], 'hpf', 'hp'); diff --git a/test/runtime.mjs b/test/runtime.mjs index f14e18e2..7b9350eb 100644 --- a/test/runtime.mjs +++ b/test/runtime.mjs @@ -122,6 +122,10 @@ strudel.Pattern.prototype.midi = function () { return this; }; +strudel.Pattern.prototype._scope = function () { + return this; +}; + const uiHelpersMocked = { backgroundImage: id, }; diff --git a/website/src/pages/learn/synths.mdx b/website/src/pages/learn/synths.mdx index 36385d54..21a9b917 100644 --- a/website/src/pages/learn/synths.mdx +++ b/website/src/pages/learn/synths.mdx @@ -18,7 +18,7 @@ The basic waveforms are `sine`, `sawtooth`, `square` and `triangle`, which can b client:idle tune={`note("c2 >".fast(2)) .sound("") -.scope()`} +._scope()`} /> If you don't set a `sound` but a `note` the default value for `sound` is `triangle`! @@ -28,23 +28,23 @@ If you don't set a `sound` but a `note` the default value for `sound` is `triang You can also use noise as a source by setting the waveform to: `white`, `pink` or `brown`. These are different flavours of noise, here written from hard to soft. -").scope()`} /> +")._scope()`} /> Here's a more musical example of how to use noise for hihats: *8") -.decay(.04).sustain(0).scope()`} +.decay(.04).sustain(0)._scope()`} /> Some amount of pink noise can also be added to any oscillator by using the `noise` paremeter: -").scope()`} /> +")._scope()`} /> You can also use the `crackle` type to play some subtle noise crackles. You can control noise amount by using the `density` parameter: -".slow(2)).scope()`} /> +".slow(2))._scope()`} /> ### Additive Synthesis @@ -55,7 +55,7 @@ To tame the harsh sound of the basic waveforms, we can set the `n` control to li tune={`note("c2 >".fast(2)) .sound("sawtooth") .n("<32 16 8 4>") -.scope()`} +._scope()`} /> When the `n` control is used on a basic waveform, it defines the number of harmonic partials the sound is getting. @@ -65,7 +65,7 @@ You can also set `n` directly in mini notation with `sound`: client:idle tune={`note("c2 >".fast(2)) .sound("sawtooth:<32 16 8 4>") -.scope()`} +._scope()`} /> Note for tidal users: `n` in tidal is synonymous to `note` for synths only. @@ -119,13 +119,14 @@ Any sample preceded by the `wt_` prefix will be loaded as a wavetable. This mean ") .n("<1 2 3 4 5 6 7 8 9 10>/2").room(0.5).size(0.9) .s('wt_flute').velocity(0.25).often(n => n.ply(2)) .release(0.125).decay("<0.1 0.25 0.3 0.4>").sustain(0) -.cutoff(2000).scope({}).cutoff("<1000 2000 4000>").fast(4)`} +.cutoff(2000).cutoff("<1000 2000 4000>").fast(4) +._scope() +`} /> ## ZZFX @@ -159,7 +160,7 @@ It has 20 parameters in total, here is a snippet that uses all: .tremolo(0) // 0-1 lfo volume modulation amount //.duration(.2) // overwrite strudel event duration //.gain(1) // change volume - .scope() // vizualise waveform (not zzfx related) + ._scope() // vizualise waveform (not zzfx related) `} /> From 6adf1ff9770230d75a60984a3fc70bd1d45a8ada Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 2 Jun 2024 14:03:30 +0200 Subject: [PATCH 029/547] snapshot --- test/__snapshots__/examples.test.mjs.snap | 288 +++++++++++----------- 1 file changed, 144 insertions(+), 144 deletions(-) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 3ff7eae2..dc76c13a 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -2717,175 +2717,175 @@ exports[`runs examples > example "floor" example index 0 1`] = ` exports[`runs examples > example "fm" example index 0 1`] = ` [ - "[ 0/1 → 1/6 | note:c fmi:0 analyze:1 ]", - "[ 1/6 → 1/3 | note:e fmi:0 analyze:1 ]", - "[ 1/3 → 1/2 | note:g fmi:0 analyze:1 ]", - "[ 1/2 → 2/3 | note:b fmi:0 analyze:1 ]", - "[ 2/3 → 5/6 | note:g fmi:0 analyze:1 ]", - "[ 5/6 → 1/1 | note:e fmi:0 analyze:1 ]", - "[ 1/1 → 7/6 | note:c fmi:1 analyze:1 ]", - "[ 7/6 → 4/3 | note:e fmi:1 analyze:1 ]", - "[ 4/3 → 3/2 | note:g fmi:1 analyze:1 ]", - "[ 3/2 → 5/3 | note:b fmi:1 analyze:1 ]", - "[ 5/3 → 11/6 | note:g fmi:1 analyze:1 ]", - "[ 11/6 → 2/1 | note:e fmi:1 analyze:1 ]", - "[ 2/1 → 13/6 | note:c fmi:2 analyze:1 ]", - "[ 13/6 → 7/3 | note:e fmi:2 analyze:1 ]", - "[ 7/3 → 5/2 | note:g fmi:2 analyze:1 ]", - "[ 5/2 → 8/3 | note:b fmi:2 analyze:1 ]", - "[ 8/3 → 17/6 | note:g fmi:2 analyze:1 ]", - "[ 17/6 → 3/1 | note:e fmi:2 analyze:1 ]", - "[ 3/1 → 19/6 | note:c fmi:8 analyze:1 ]", - "[ 19/6 → 10/3 | note:e fmi:8 analyze:1 ]", - "[ 10/3 → 7/2 | note:g fmi:8 analyze:1 ]", - "[ 7/2 → 11/3 | note:b fmi:8 analyze:1 ]", - "[ 11/3 → 23/6 | note:g fmi:8 analyze:1 ]", - "[ 23/6 → 4/1 | note:e fmi:8 analyze:1 ]", + "[ 0/1 → 1/6 | note:c fmi:0 ]", + "[ 1/6 → 1/3 | note:e fmi:0 ]", + "[ 1/3 → 1/2 | note:g fmi:0 ]", + "[ 1/2 → 2/3 | note:b fmi:0 ]", + "[ 2/3 → 5/6 | note:g fmi:0 ]", + "[ 5/6 → 1/1 | note:e fmi:0 ]", + "[ 1/1 → 7/6 | note:c fmi:1 ]", + "[ 7/6 → 4/3 | note:e fmi:1 ]", + "[ 4/3 → 3/2 | note:g fmi:1 ]", + "[ 3/2 → 5/3 | note:b fmi:1 ]", + "[ 5/3 → 11/6 | note:g fmi:1 ]", + "[ 11/6 → 2/1 | note:e fmi:1 ]", + "[ 2/1 → 13/6 | note:c fmi:2 ]", + "[ 13/6 → 7/3 | note:e fmi:2 ]", + "[ 7/3 → 5/2 | note:g fmi:2 ]", + "[ 5/2 → 8/3 | note:b fmi:2 ]", + "[ 8/3 → 17/6 | note:g fmi:2 ]", + "[ 17/6 → 3/1 | note:e fmi:2 ]", + "[ 3/1 → 19/6 | note:c fmi:8 ]", + "[ 19/6 → 10/3 | note:e fmi:8 ]", + "[ 10/3 → 7/2 | note:g fmi:8 ]", + "[ 7/2 → 11/3 | note:b fmi:8 ]", + "[ 11/3 → 23/6 | note:g fmi:8 ]", + "[ 23/6 → 4/1 | note:e fmi:8 ]", ] `; exports[`runs examples > example "fmattack" example index 0 1`] = ` [ - "[ 0/1 → 1/6 | note:c fmi:4 fmattack:0 analyze:1 ]", - "[ 1/6 → 1/3 | note:e fmi:4 fmattack:0 analyze:1 ]", - "[ 1/3 → 1/2 | note:g fmi:4 fmattack:0 analyze:1 ]", - "[ 1/2 → 2/3 | note:b fmi:4 fmattack:0 analyze:1 ]", - "[ 2/3 → 5/6 | note:g fmi:4 fmattack:0 analyze:1 ]", - "[ 5/6 → 1/1 | note:e fmi:4 fmattack:0 analyze:1 ]", - "[ 1/1 → 7/6 | note:c fmi:4 fmattack:0.05 analyze:1 ]", - "[ 7/6 → 4/3 | note:e fmi:4 fmattack:0.05 analyze:1 ]", - "[ 4/3 → 3/2 | note:g fmi:4 fmattack:0.05 analyze:1 ]", - "[ 3/2 → 5/3 | note:b fmi:4 fmattack:0.05 analyze:1 ]", - "[ 5/3 → 11/6 | note:g fmi:4 fmattack:0.05 analyze:1 ]", - "[ 11/6 → 2/1 | note:e fmi:4 fmattack:0.05 analyze:1 ]", - "[ 2/1 → 13/6 | note:c fmi:4 fmattack:0.1 analyze:1 ]", - "[ 13/6 → 7/3 | note:e fmi:4 fmattack:0.1 analyze:1 ]", - "[ 7/3 → 5/2 | note:g fmi:4 fmattack:0.1 analyze:1 ]", - "[ 5/2 → 8/3 | note:b fmi:4 fmattack:0.1 analyze:1 ]", - "[ 8/3 → 17/6 | note:g fmi:4 fmattack:0.1 analyze:1 ]", - "[ 17/6 → 3/1 | note:e fmi:4 fmattack:0.1 analyze:1 ]", - "[ 3/1 → 19/6 | note:c fmi:4 fmattack:0.2 analyze:1 ]", - "[ 19/6 → 10/3 | note:e fmi:4 fmattack:0.2 analyze:1 ]", - "[ 10/3 → 7/2 | note:g fmi:4 fmattack:0.2 analyze:1 ]", - "[ 7/2 → 11/3 | note:b fmi:4 fmattack:0.2 analyze:1 ]", - "[ 11/3 → 23/6 | note:g fmi:4 fmattack:0.2 analyze:1 ]", - "[ 23/6 → 4/1 | note:e fmi:4 fmattack:0.2 analyze:1 ]", + "[ 0/1 → 1/6 | note:c fmi:4 fmattack:0 ]", + "[ 1/6 → 1/3 | note:e fmi:4 fmattack:0 ]", + "[ 1/3 → 1/2 | note:g fmi:4 fmattack:0 ]", + "[ 1/2 → 2/3 | note:b fmi:4 fmattack:0 ]", + "[ 2/3 → 5/6 | note:g fmi:4 fmattack:0 ]", + "[ 5/6 → 1/1 | note:e fmi:4 fmattack:0 ]", + "[ 1/1 → 7/6 | note:c fmi:4 fmattack:0.05 ]", + "[ 7/6 → 4/3 | note:e fmi:4 fmattack:0.05 ]", + "[ 4/3 → 3/2 | note:g fmi:4 fmattack:0.05 ]", + "[ 3/2 → 5/3 | note:b fmi:4 fmattack:0.05 ]", + "[ 5/3 → 11/6 | note:g fmi:4 fmattack:0.05 ]", + "[ 11/6 → 2/1 | note:e fmi:4 fmattack:0.05 ]", + "[ 2/1 → 13/6 | note:c fmi:4 fmattack:0.1 ]", + "[ 13/6 → 7/3 | note:e fmi:4 fmattack:0.1 ]", + "[ 7/3 → 5/2 | note:g fmi:4 fmattack:0.1 ]", + "[ 5/2 → 8/3 | note:b fmi:4 fmattack:0.1 ]", + "[ 8/3 → 17/6 | note:g fmi:4 fmattack:0.1 ]", + "[ 17/6 → 3/1 | note:e fmi:4 fmattack:0.1 ]", + "[ 3/1 → 19/6 | note:c fmi:4 fmattack:0.2 ]", + "[ 19/6 → 10/3 | note:e fmi:4 fmattack:0.2 ]", + "[ 10/3 → 7/2 | note:g fmi:4 fmattack:0.2 ]", + "[ 7/2 → 11/3 | note:b fmi:4 fmattack:0.2 ]", + "[ 11/3 → 23/6 | note:g fmi:4 fmattack:0.2 ]", + "[ 23/6 → 4/1 | note:e fmi:4 fmattack:0.2 ]", ] `; exports[`runs examples > example "fmdecay" example index 0 1`] = ` [ - "[ 0/1 → 1/6 | note:c fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]", - "[ 1/6 → 1/3 | note:e fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]", - "[ 1/3 → 1/2 | note:g fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]", - "[ 1/2 → 2/3 | note:b fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]", - "[ 2/3 → 5/6 | note:g fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]", - "[ 5/6 → 1/1 | note:e fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]", - "[ 1/1 → 7/6 | note:c fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]", - "[ 7/6 → 4/3 | note:e fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]", - "[ 4/3 → 3/2 | note:g fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]", - "[ 3/2 → 5/3 | note:b fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]", - "[ 5/3 → 11/6 | note:g fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]", - "[ 11/6 → 2/1 | note:e fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]", - "[ 2/1 → 13/6 | note:c fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]", - "[ 13/6 → 7/3 | note:e fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]", - "[ 7/3 → 5/2 | note:g fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]", - "[ 5/2 → 8/3 | note:b fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]", - "[ 8/3 → 17/6 | note:g fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]", - "[ 17/6 → 3/1 | note:e fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]", - "[ 3/1 → 19/6 | note:c fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]", - "[ 19/6 → 10/3 | note:e fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]", - "[ 10/3 → 7/2 | note:g fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]", - "[ 7/2 → 11/3 | note:b fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]", - "[ 11/3 → 23/6 | note:g fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]", - "[ 23/6 → 4/1 | note:e fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]", + "[ 0/1 → 1/6 | note:c fmi:4 fmdecay:0.01 fmsustain:0.4 ]", + "[ 1/6 → 1/3 | note:e fmi:4 fmdecay:0.01 fmsustain:0.4 ]", + "[ 1/3 → 1/2 | note:g fmi:4 fmdecay:0.01 fmsustain:0.4 ]", + "[ 1/2 → 2/3 | note:b fmi:4 fmdecay:0.01 fmsustain:0.4 ]", + "[ 2/3 → 5/6 | note:g fmi:4 fmdecay:0.01 fmsustain:0.4 ]", + "[ 5/6 → 1/1 | note:e fmi:4 fmdecay:0.01 fmsustain:0.4 ]", + "[ 1/1 → 7/6 | note:c fmi:4 fmdecay:0.05 fmsustain:0.4 ]", + "[ 7/6 → 4/3 | note:e fmi:4 fmdecay:0.05 fmsustain:0.4 ]", + "[ 4/3 → 3/2 | note:g fmi:4 fmdecay:0.05 fmsustain:0.4 ]", + "[ 3/2 → 5/3 | note:b fmi:4 fmdecay:0.05 fmsustain:0.4 ]", + "[ 5/3 → 11/6 | note:g fmi:4 fmdecay:0.05 fmsustain:0.4 ]", + "[ 11/6 → 2/1 | note:e fmi:4 fmdecay:0.05 fmsustain:0.4 ]", + "[ 2/1 → 13/6 | note:c fmi:4 fmdecay:0.1 fmsustain:0.4 ]", + "[ 13/6 → 7/3 | note:e fmi:4 fmdecay:0.1 fmsustain:0.4 ]", + "[ 7/3 → 5/2 | note:g fmi:4 fmdecay:0.1 fmsustain:0.4 ]", + "[ 5/2 → 8/3 | note:b fmi:4 fmdecay:0.1 fmsustain:0.4 ]", + "[ 8/3 → 17/6 | note:g fmi:4 fmdecay:0.1 fmsustain:0.4 ]", + "[ 17/6 → 3/1 | note:e fmi:4 fmdecay:0.1 fmsustain:0.4 ]", + "[ 3/1 → 19/6 | note:c fmi:4 fmdecay:0.2 fmsustain:0.4 ]", + "[ 19/6 → 10/3 | note:e fmi:4 fmdecay:0.2 fmsustain:0.4 ]", + "[ 10/3 → 7/2 | note:g fmi:4 fmdecay:0.2 fmsustain:0.4 ]", + "[ 7/2 → 11/3 | note:b fmi:4 fmdecay:0.2 fmsustain:0.4 ]", + "[ 11/3 → 23/6 | note:g fmi:4 fmdecay:0.2 fmsustain:0.4 ]", + "[ 23/6 → 4/1 | note:e fmi:4 fmdecay:0.2 fmsustain:0.4 ]", ] `; exports[`runs examples > example "fmenv" example index 0 1`] = ` [ - "[ 0/1 → 1/6 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", - "[ 1/6 → 1/3 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", - "[ 1/3 → 1/2 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", - "[ 1/2 → 2/3 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", - "[ 2/3 → 5/6 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", - "[ 5/6 → 1/1 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", - "[ 1/1 → 7/6 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", - "[ 7/6 → 4/3 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", - "[ 4/3 → 3/2 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", - "[ 3/2 → 5/3 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", - "[ 5/3 → 11/6 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", - "[ 11/6 → 2/1 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", - "[ 2/1 → 13/6 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", - "[ 13/6 → 7/3 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", - "[ 7/3 → 5/2 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", - "[ 5/2 → 8/3 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", - "[ 8/3 → 17/6 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", - "[ 17/6 → 3/1 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", - "[ 3/1 → 19/6 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", - "[ 19/6 → 10/3 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", - "[ 10/3 → 7/2 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", - "[ 7/2 → 11/3 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", - "[ 11/3 → 23/6 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", - "[ 23/6 → 4/1 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", + "[ 0/1 → 1/6 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]", + "[ 1/6 → 1/3 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]", + "[ 1/3 → 1/2 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]", + "[ 1/2 → 2/3 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]", + "[ 2/3 → 5/6 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]", + "[ 5/6 → 1/1 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]", + "[ 1/1 → 7/6 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]", + "[ 7/6 → 4/3 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]", + "[ 4/3 → 3/2 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]", + "[ 3/2 → 5/3 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]", + "[ 5/3 → 11/6 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]", + "[ 11/6 → 2/1 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]", + "[ 2/1 → 13/6 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]", + "[ 13/6 → 7/3 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]", + "[ 7/3 → 5/2 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]", + "[ 5/2 → 8/3 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]", + "[ 8/3 → 17/6 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]", + "[ 17/6 → 3/1 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]", + "[ 3/1 → 19/6 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]", + "[ 19/6 → 10/3 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]", + "[ 10/3 → 7/2 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]", + "[ 7/2 → 11/3 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]", + "[ 11/3 → 23/6 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]", + "[ 23/6 → 4/1 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]", ] `; exports[`runs examples > example "fmh" example index 0 1`] = ` [ - "[ 0/1 → 1/6 | note:c fmi:4 fmh:1 analyze:1 ]", - "[ 1/6 → 1/3 | note:e fmi:4 fmh:1 analyze:1 ]", - "[ 1/3 → 1/2 | note:g fmi:4 fmh:1 analyze:1 ]", - "[ 1/2 → 2/3 | note:b fmi:4 fmh:1 analyze:1 ]", - "[ 2/3 → 5/6 | note:g fmi:4 fmh:1 analyze:1 ]", - "[ 5/6 → 1/1 | note:e fmi:4 fmh:1 analyze:1 ]", - "[ 1/1 → 7/6 | note:c fmi:4 fmh:2 analyze:1 ]", - "[ 7/6 → 4/3 | note:e fmi:4 fmh:2 analyze:1 ]", - "[ 4/3 → 3/2 | note:g fmi:4 fmh:2 analyze:1 ]", - "[ 3/2 → 5/3 | note:b fmi:4 fmh:2 analyze:1 ]", - "[ 5/3 → 11/6 | note:g fmi:4 fmh:2 analyze:1 ]", - "[ 11/6 → 2/1 | note:e fmi:4 fmh:2 analyze:1 ]", - "[ 2/1 → 13/6 | note:c fmi:4 fmh:1.5 analyze:1 ]", - "[ 13/6 → 7/3 | note:e fmi:4 fmh:1.5 analyze:1 ]", - "[ 7/3 → 5/2 | note:g fmi:4 fmh:1.5 analyze:1 ]", - "[ 5/2 → 8/3 | note:b fmi:4 fmh:1.5 analyze:1 ]", - "[ 8/3 → 17/6 | note:g fmi:4 fmh:1.5 analyze:1 ]", - "[ 17/6 → 3/1 | note:e fmi:4 fmh:1.5 analyze:1 ]", - "[ 3/1 → 19/6 | note:c fmi:4 fmh:1.61 analyze:1 ]", - "[ 19/6 → 10/3 | note:e fmi:4 fmh:1.61 analyze:1 ]", - "[ 10/3 → 7/2 | note:g fmi:4 fmh:1.61 analyze:1 ]", - "[ 7/2 → 11/3 | note:b fmi:4 fmh:1.61 analyze:1 ]", - "[ 11/3 → 23/6 | note:g fmi:4 fmh:1.61 analyze:1 ]", - "[ 23/6 → 4/1 | note:e fmi:4 fmh:1.61 analyze:1 ]", + "[ 0/1 → 1/6 | note:c fmi:4 fmh:1 ]", + "[ 1/6 → 1/3 | note:e fmi:4 fmh:1 ]", + "[ 1/3 → 1/2 | note:g fmi:4 fmh:1 ]", + "[ 1/2 → 2/3 | note:b fmi:4 fmh:1 ]", + "[ 2/3 → 5/6 | note:g fmi:4 fmh:1 ]", + "[ 5/6 → 1/1 | note:e fmi:4 fmh:1 ]", + "[ 1/1 → 7/6 | note:c fmi:4 fmh:2 ]", + "[ 7/6 → 4/3 | note:e fmi:4 fmh:2 ]", + "[ 4/3 → 3/2 | note:g fmi:4 fmh:2 ]", + "[ 3/2 → 5/3 | note:b fmi:4 fmh:2 ]", + "[ 5/3 → 11/6 | note:g fmi:4 fmh:2 ]", + "[ 11/6 → 2/1 | note:e fmi:4 fmh:2 ]", + "[ 2/1 → 13/6 | note:c fmi:4 fmh:1.5 ]", + "[ 13/6 → 7/3 | note:e fmi:4 fmh:1.5 ]", + "[ 7/3 → 5/2 | note:g fmi:4 fmh:1.5 ]", + "[ 5/2 → 8/3 | note:b fmi:4 fmh:1.5 ]", + "[ 8/3 → 17/6 | note:g fmi:4 fmh:1.5 ]", + "[ 17/6 → 3/1 | note:e fmi:4 fmh:1.5 ]", + "[ 3/1 → 19/6 | note:c fmi:4 fmh:1.61 ]", + "[ 19/6 → 10/3 | note:e fmi:4 fmh:1.61 ]", + "[ 10/3 → 7/2 | note:g fmi:4 fmh:1.61 ]", + "[ 7/2 → 11/3 | note:b fmi:4 fmh:1.61 ]", + "[ 11/3 → 23/6 | note:g fmi:4 fmh:1.61 ]", + "[ 23/6 → 4/1 | note:e fmi:4 fmh:1.61 ]", ] `; exports[`runs examples > example "fmsustain" example index 0 1`] = ` [ - "[ 0/1 → 1/6 | note:c fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]", - "[ 1/6 → 1/3 | note:e fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]", - "[ 1/3 → 1/2 | note:g fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]", - "[ 1/2 → 2/3 | note:b fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]", - "[ 2/3 → 5/6 | note:g fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]", - "[ 5/6 → 1/1 | note:e fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]", - "[ 1/1 → 7/6 | note:c fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]", - "[ 7/6 → 4/3 | note:e fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]", - "[ 4/3 → 3/2 | note:g fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]", - "[ 3/2 → 5/3 | note:b fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]", - "[ 5/3 → 11/6 | note:g fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]", - "[ 11/6 → 2/1 | note:e fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]", - "[ 2/1 → 13/6 | note:c fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]", - "[ 13/6 → 7/3 | note:e fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]", - "[ 7/3 → 5/2 | note:g fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]", - "[ 5/2 → 8/3 | note:b fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]", - "[ 8/3 → 17/6 | note:g fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]", - "[ 17/6 → 3/1 | note:e fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]", - "[ 3/1 → 19/6 | note:c fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]", - "[ 19/6 → 10/3 | note:e fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]", - "[ 10/3 → 7/2 | note:g fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]", - "[ 7/2 → 11/3 | note:b fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]", - "[ 11/3 → 23/6 | note:g fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]", - "[ 23/6 → 4/1 | note:e fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]", + "[ 0/1 → 1/6 | note:c fmi:4 fmdecay:0.1 fmsustain:1 ]", + "[ 1/6 → 1/3 | note:e fmi:4 fmdecay:0.1 fmsustain:1 ]", + "[ 1/3 → 1/2 | note:g fmi:4 fmdecay:0.1 fmsustain:1 ]", + "[ 1/2 → 2/3 | note:b fmi:4 fmdecay:0.1 fmsustain:1 ]", + "[ 2/3 → 5/6 | note:g fmi:4 fmdecay:0.1 fmsustain:1 ]", + "[ 5/6 → 1/1 | note:e fmi:4 fmdecay:0.1 fmsustain:1 ]", + "[ 1/1 → 7/6 | note:c fmi:4 fmdecay:0.1 fmsustain:0.75 ]", + "[ 7/6 → 4/3 | note:e fmi:4 fmdecay:0.1 fmsustain:0.75 ]", + "[ 4/3 → 3/2 | note:g fmi:4 fmdecay:0.1 fmsustain:0.75 ]", + "[ 3/2 → 5/3 | note:b fmi:4 fmdecay:0.1 fmsustain:0.75 ]", + "[ 5/3 → 11/6 | note:g fmi:4 fmdecay:0.1 fmsustain:0.75 ]", + "[ 11/6 → 2/1 | note:e fmi:4 fmdecay:0.1 fmsustain:0.75 ]", + "[ 2/1 → 13/6 | note:c fmi:4 fmdecay:0.1 fmsustain:0.5 ]", + "[ 13/6 → 7/3 | note:e fmi:4 fmdecay:0.1 fmsustain:0.5 ]", + "[ 7/3 → 5/2 | note:g fmi:4 fmdecay:0.1 fmsustain:0.5 ]", + "[ 5/2 → 8/3 | note:b fmi:4 fmdecay:0.1 fmsustain:0.5 ]", + "[ 8/3 → 17/6 | note:g fmi:4 fmdecay:0.1 fmsustain:0.5 ]", + "[ 17/6 → 3/1 | note:e fmi:4 fmdecay:0.1 fmsustain:0.5 ]", + "[ 3/1 → 19/6 | note:c fmi:4 fmdecay:0.1 fmsustain:0 ]", + "[ 19/6 → 10/3 | note:e fmi:4 fmdecay:0.1 fmsustain:0 ]", + "[ 10/3 → 7/2 | note:g fmi:4 fmdecay:0.1 fmsustain:0 ]", + "[ 7/2 → 11/3 | note:b fmi:4 fmdecay:0.1 fmsustain:0 ]", + "[ 11/3 → 23/6 | note:g fmi:4 fmdecay:0.1 fmsustain:0 ]", + "[ 23/6 → 4/1 | note:e fmi:4 fmdecay:0.1 fmsustain:0 ]", ] `; From e6b55baed0714e9a454036d69daf6650b9e06d61 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 2 Jun 2024 14:08:56 +0200 Subject: [PATCH 030/547] fix: step based naming --- packages/core/pattern.mjs | 8 ++++--- website/src/pages/learn/factories.mdx | 30 +++++++++++++-------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 918a924b..a8a4f7da 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2531,7 +2531,7 @@ export function _polymeterListSteps(steps, ...args) { * @param {any[]} patterns one or more patterns * @example * // the same as "{c d, e f g}%4" - * s_polymeterSteps(4, "c d", "e f g") + * s_polymeterSteps(4, "c d", "e f g").note() */ export function s_polymeterSteps(steps, ...args) { if (args.length == 0) { @@ -2549,8 +2549,8 @@ export function s_polymeterSteps(steps, ...args) { * *EXPERIMENTAL* - Combines the given lists of patterns with the same pulse, creating polymeters when different sized sequences are used. * @synonyms pm * @example - * // The same as "{c eb g, c2 g2}" - * s_polymeter("c eb g", "c2 g2") + * // The same as note("{c eb g, c2 g2}") + * s_polymeter("c eb g", "c2 g2").note() * */ export function s_polymeter(...args) { @@ -2579,6 +2579,8 @@ export function s_polymeter(...args) { /** Sequences patterns like `seq`, but each pattern has a length, relative to the whole. * This length can either be provided as a [length, pattern] pair, or inferred from * the pattern's 'tactus', generally inferred by the mininotation. Has the alias `timecat`. + * @name s_cat + * @synonyms timeCat, timecat * @return {Pattern} * @example * s_cat([3,"e3"],[1, "g3"]).note() diff --git a/website/src/pages/learn/factories.mdx b/website/src/pages/learn/factories.mdx index 0d93fb21..a823bfed 100644 --- a/website/src/pages/learn/factories.mdx +++ b/website/src/pages/learn/factories.mdx @@ -11,15 +11,15 @@ import { JsDoc } from '../../docs/JsDoc'; The following functions will return a pattern. These are the equivalents used by the Mini Notation: -| function | mini | -| ------------------------------ | ---------------- | -| `cat(x, y)` | `""` | -| `seq(x, y)` | `"x y"` | -| `stack(x, y)` | `"x,y"` | -| `timeCat([3,x],[2,y])` | `"x@3 y@2"` | -| `polymeter([a, b, c], [x, y])` | `"{a b c, x y}"` | -| `polymeterSteps(2, x, y, z)` | `"{x y z}%2"` | -| `silence` | `"~"` | +| function | mini | +| -------------------------------- | ---------------- | +| `cat(x, y)` | `""` | +| `seq(x, y)` | `"x y"` | +| `stack(x, y)` | `"x,y"` | +| `s_cat([3,x],[2,y])` | `"x@3 y@2"` | +| `s_polymeter([a, b, c], [x, y])` | `"{a b c, x y}"` | +| `s_polymeterSteps(2, x, y, z)` | `"{x y z}%2"` | +| `silence` | `"~"` | ## cat @@ -45,21 +45,21 @@ As a chained function: -## timeCat +## s_cat - + ## arrange -## polymeter +## s_polymeter - + -## polymeterSteps +## s_polymeterSteps - + ## silence From 25981092b184688ca0e3c45931d29af227c87ad6 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 2 Jun 2024 14:11:52 +0200 Subject: [PATCH 031/547] snapshot --- test/__snapshots__/examples.test.mjs.snap | 112 +++++++++++----------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index dc76c13a..0cd653e3 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -6196,67 +6196,67 @@ exports[`runs examples > example "s_cat" example index 1 1`] = ` exports[`runs examples > example "s_polymeter" example index 0 1`] = ` [ - "[ 0/1 → 1/3 | c ]", - "[ 0/1 → 1/3 | c2 ]", - "[ 1/3 → 2/3 | eb ]", - "[ 1/3 → 2/3 | g2 ]", - "[ 2/3 → 1/1 | g ]", - "[ 2/3 → 1/1 | c2 ]", - "[ 1/1 → 4/3 | c ]", - "[ 1/1 → 4/3 | g2 ]", - "[ 4/3 → 5/3 | eb ]", - "[ 4/3 → 5/3 | c2 ]", - "[ 5/3 → 2/1 | g ]", - "[ 5/3 → 2/1 | g2 ]", - "[ 2/1 → 7/3 | c ]", - "[ 2/1 → 7/3 | c2 ]", - "[ 7/3 → 8/3 | eb ]", - "[ 7/3 → 8/3 | g2 ]", - "[ 8/3 → 3/1 | g ]", - "[ 8/3 → 3/1 | c2 ]", - "[ 3/1 → 10/3 | c ]", - "[ 3/1 → 10/3 | g2 ]", - "[ 10/3 → 11/3 | eb ]", - "[ 10/3 → 11/3 | c2 ]", - "[ 11/3 → 4/1 | g ]", - "[ 11/3 → 4/1 | g2 ]", + "[ 0/1 → 1/3 | note:c ]", + "[ 0/1 → 1/3 | note:c2 ]", + "[ 1/3 → 2/3 | note:eb ]", + "[ 1/3 → 2/3 | note:g2 ]", + "[ 2/3 → 1/1 | note:g ]", + "[ 2/3 → 1/1 | note:c2 ]", + "[ 1/1 → 4/3 | note:c ]", + "[ 1/1 → 4/3 | note:g2 ]", + "[ 4/3 → 5/3 | note:eb ]", + "[ 4/3 → 5/3 | note:c2 ]", + "[ 5/3 → 2/1 | note:g ]", + "[ 5/3 → 2/1 | note:g2 ]", + "[ 2/1 → 7/3 | note:c ]", + "[ 2/1 → 7/3 | note:c2 ]", + "[ 7/3 → 8/3 | note:eb ]", + "[ 7/3 → 8/3 | note:g2 ]", + "[ 8/3 → 3/1 | note:g ]", + "[ 8/3 → 3/1 | note:c2 ]", + "[ 3/1 → 10/3 | note:c ]", + "[ 3/1 → 10/3 | note:g2 ]", + "[ 10/3 → 11/3 | note:eb ]", + "[ 10/3 → 11/3 | note:c2 ]", + "[ 11/3 → 4/1 | note:g ]", + "[ 11/3 → 4/1 | note:g2 ]", ] `; exports[`runs examples > example "s_polymeterSteps" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | c ]", - "[ 0/1 → 1/4 | e ]", - "[ 1/4 → 1/2 | d ]", - "[ 1/4 → 1/2 | f ]", - "[ 1/2 → 3/4 | c ]", - "[ 1/2 → 3/4 | g ]", - "[ 3/4 → 1/1 | d ]", - "[ 3/4 → 1/1 | e ]", - "[ 1/1 → 5/4 | c ]", - "[ 1/1 → 5/4 | f ]", - "[ 5/4 → 3/2 | d ]", - "[ 5/4 → 3/2 | g ]", - "[ 3/2 → 7/4 | c ]", - "[ 3/2 → 7/4 | e ]", - "[ 7/4 → 2/1 | d ]", - "[ 7/4 → 2/1 | f ]", - "[ 2/1 → 9/4 | c ]", - "[ 2/1 → 9/4 | g ]", - "[ 9/4 → 5/2 | d ]", - "[ 9/4 → 5/2 | e ]", - "[ 5/2 → 11/4 | c ]", - "[ 5/2 → 11/4 | f ]", - "[ 11/4 → 3/1 | d ]", - "[ 11/4 → 3/1 | g ]", - "[ 3/1 → 13/4 | c ]", - "[ 3/1 → 13/4 | e ]", - "[ 13/4 → 7/2 | d ]", - "[ 13/4 → 7/2 | f ]", - "[ 7/2 → 15/4 | c ]", - "[ 7/2 → 15/4 | g ]", - "[ 15/4 → 4/1 | d ]", - "[ 15/4 → 4/1 | e ]", + "[ 0/1 → 1/4 | note:c ]", + "[ 0/1 → 1/4 | note:e ]", + "[ 1/4 → 1/2 | note:d ]", + "[ 1/4 → 1/2 | note:f ]", + "[ 1/2 → 3/4 | note:c ]", + "[ 1/2 → 3/4 | note:g ]", + "[ 3/4 → 1/1 | note:d ]", + "[ 3/4 → 1/1 | note:e ]", + "[ 1/1 → 5/4 | note:c ]", + "[ 1/1 → 5/4 | note:f ]", + "[ 5/4 → 3/2 | note:d ]", + "[ 5/4 → 3/2 | note:g ]", + "[ 3/2 → 7/4 | note:c ]", + "[ 3/2 → 7/4 | note:e ]", + "[ 7/4 → 2/1 | note:d ]", + "[ 7/4 → 2/1 | note:f ]", + "[ 2/1 → 9/4 | note:c ]", + "[ 2/1 → 9/4 | note:g ]", + "[ 9/4 → 5/2 | note:d ]", + "[ 9/4 → 5/2 | note:e ]", + "[ 5/2 → 11/4 | note:c ]", + "[ 5/2 → 11/4 | note:f ]", + "[ 11/4 → 3/1 | note:d ]", + "[ 11/4 → 3/1 | note:g ]", + "[ 3/1 → 13/4 | note:c ]", + "[ 3/1 → 13/4 | note:e ]", + "[ 13/4 → 7/2 | note:d ]", + "[ 13/4 → 7/2 | note:f ]", + "[ 7/2 → 15/4 | note:c ]", + "[ 7/2 → 15/4 | note:g ]", + "[ 15/4 → 4/1 | note:d ]", + "[ 15/4 → 4/1 | note:e ]", ] `; From 8c73ad088a6020603c955e669fe362a3aa77e621 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 2 Jun 2024 12:32:54 -0400 Subject: [PATCH 032/547] support aac --- website/src/repl/files.mjs | 2 +- website/src/repl/panel/ImportSoundsButton.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/repl/files.mjs b/website/src/repl/files.mjs index 3e3db94d..d2b95f6a 100644 --- a/website/src/repl/files.mjs +++ b/website/src/repl/files.mjs @@ -76,7 +76,7 @@ export const walkFileTree = (node, fn) => { }; export const isAudioFile = (filename) => - ['wav', 'mp3', 'flac', 'ogg', 'm4a'].includes(filename.split('.').slice(-1)[0]); + ['wav', 'mp3', 'flac', 'ogg', 'm4a', 'aac'].includes(filename.split('.').slice(-1)[0]); function uint8ArrayToDataURL(uint8Array) { const blob = new Blob([uint8Array], { type: 'audio/*' }); diff --git a/website/src/repl/panel/ImportSoundsButton.jsx b/website/src/repl/panel/ImportSoundsButton.jsx index 7a97b9e0..da45705f 100644 --- a/website/src/repl/panel/ImportSoundsButton.jsx +++ b/website/src/repl/panel/ImportSoundsButton.jsx @@ -33,7 +33,7 @@ export default function ImportSoundsButton({ onComplete }) { directory="" webkitdirectory="" multiple - accept="audio/*, .wav, .mp3, .m4a, .flac" + accept="audio/*, .wav, .mp3, .m4a, .flac, .aac, .ogg" onChange={() => { onChange(); }} From 1acb675f5ab7709c3f8b06007aa374ff3936c8a0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 2 Jun 2024 23:12:31 +0200 Subject: [PATCH 033/547] doc: visual functions --- packages/core/controls.mjs | 4 +- packages/draw/pianoroll.mjs | 9 ++- packages/draw/pitchwheel.mjs | 17 +++++ packages/draw/spiral.mjs | 27 +++++++ packages/webaudio/scope.mjs | 2 +- website/src/docs/MiniRepl.jsx | 4 +- website/src/pages/learn/visual-feedback.mdx | 81 ++++++++++++--------- 7 files changed, 104 insertions(+), 40 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 82f94394..94f46f44 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -392,7 +392,7 @@ export const { loop } = registerControl('loop'); * @synonyms loopb * @example * s("space").loop(1) - * .loopBegin("<0 .125 .25>").scope() + * .loopBegin("<0 .125 .25>")._scope() */ export const { loopBegin, loopb } = registerControl('loopBegin', 'loopb'); /** @@ -405,7 +405,7 @@ export const { loopBegin, loopb } = registerControl('loopBegin', 'loopb'); * @synonyms loope * @example * s("space").loop(1) - * .loopEnd("<1 .75 .5 .25>").scope() + * .loopEnd("<1 .75 .5 .25>")._scope() */ export const { loopEnd, loope } = registerControl('loopEnd', 'loope'); /** diff --git a/packages/draw/pianoroll.mjs b/packages/draw/pianoroll.mjs index d0480ef5..2c6742cd 100644 --- a/packages/draw/pianoroll.mjs +++ b/packages/draw/pianoroll.mjs @@ -67,6 +67,7 @@ Pattern.prototype.pianoroll = function (options = {}) { * Displays a midi-style piano roll * * @name pianoroll + * @synonyms punchcard * @param {Object} options Object containing all the optional following parameters as key value pairs: * @param {integer} cycles number of cycles to be displayed at the same time - defaults to 4 * @param {number} playhead location of the active notes on the time axis - 0 to 1, defaults to 0.5 @@ -94,7 +95,11 @@ Pattern.prototype.pianoroll = function (options = {}) { * @param {boolean} autorange automatically calculate the minMidi and maxMidi parameters - 0 by default * * @example - * note("C2 A2 G2").euclid(5,8).s('piano').clip(1).color('salmon').pianoroll({vertical:1, labels:1}) + * note("c2 a2 eb2") + * .euclid(5,8) + * .s('sawtooth') + * .lpenv(4).lpf(300) + * ._pianoroll({ labels: 1 }) */ export function pianoroll({ time, @@ -113,7 +118,7 @@ export function pianoroll({ maxMidi = 90, autorange = 0, timeframe: timeframeProp, - fold = 0, + fold = 1, vertical = 0, labels = false, fill = 1, diff --git a/packages/draw/pitchwheel.mjs b/packages/draw/pitchwheel.mjs index 8d3cfe61..ba76df07 100644 --- a/packages/draw/pitchwheel.mjs +++ b/packages/draw/pitchwheel.mjs @@ -113,6 +113,23 @@ export function pitchwheel({ return; } +/** + * Renders a pitch circle to visualize frequencies within one octave + * @name pitchwheel + * @param {number} hapcircles + * @param {number} circle + * @param {number} edo + * @param {string} root + * @param {number} thickness + * @param {number} hapRadius + * @param {string} mode + * @param {number} margin + * @example + * n("0 .. 12").scale("C:chromatic") + * .s("sawtooth") + * .lpf(500) + * ._pitchwheel() + */ Pattern.prototype.pitchwheel = function (options = {}) { let { ctx = getDrawContext(), id = 1 } = options; return this.tag(id).onPaint((_, time, haps) => diff --git a/packages/draw/spiral.mjs b/packages/draw/spiral.mjs index 22f93f5d..cebf3d37 100644 --- a/packages/draw/spiral.mjs +++ b/packages/draw/spiral.mjs @@ -125,6 +125,33 @@ function drawSpiral(options) { }); } +/** + * Displays a spiral visual. + * + * @name spiral + * @param {Object} options Object containing all the optional following parameters as key value pairs: + * @param {number} stretch controls the rotations per cycle ratio, where 1 = 1 cycle / 360 degrees + * @param {number} size the diameter of the spiral + * @param {number} thickness line thickness + * @param {string} cap style of line ends: butt (default), round, square + * @param {string} inset number of rotations before spiral starts (default 3) + * @param {string} playheadColor color of playhead, defaults to white + * @param {number} playheadLength length of playhead in rotations, defaults to 0.02 + * @param {number} playheadThickness thickness of playheadrotations, defaults to thickness + * @param {number} padding space around spiral + * @param {number} steady steadyness of spiral vs playhead. 1 = spiral doesn't move, playhead does. + * @param {number} activeColor color of active segment. defaults to foreground of theme + * @param {number} inactiveColor color of inactive segments. defaults to gutterForeground of theme + * @param {boolean} colorizeInactive wether or not to colorize inactive segments, defaults to 0 + * @param {boolean} fade wether or not past and future should fade out. defaults to 1 + * @param {boolean} logSpiral wether or not the spiral should be logarithmic. defaults to 0 + * @example + * note("c2 a2 eb2") + * .euclid(5,8) + * .s('sawtooth') + * .lpenv(4).lpf(300) + * ._spiral({ steady: .96 }) + */ Pattern.prototype.spiral = function (options = {}) { return this.onPaint((ctx, time, haps, drawTime) => drawSpiral({ ctx, time, haps, drawTime, ...options })); }; diff --git a/packages/webaudio/scope.mjs b/packages/webaudio/scope.mjs index fbf4c8fc..e423b20f 100644 --- a/packages/webaudio/scope.mjs +++ b/packages/webaudio/scope.mjs @@ -130,7 +130,7 @@ Pattern.prototype.fscope = function (config = {}) { * @param {number} pos y-position relative to screen height. 0 = top, 1 = bottom of screen * @param {number} trigger amplitude value that is used to align the scope. defaults to 0. * @example - * s("sawtooth").scope() + * s("sawtooth")._scope() */ let latestColor = {}; Pattern.prototype.tscope = function (config = {}) { diff --git a/website/src/docs/MiniRepl.jsx b/website/src/docs/MiniRepl.jsx index 3559b341..56548471 100644 --- a/website/src/docs/MiniRepl.jsx +++ b/website/src/docs/MiniRepl.jsx @@ -34,9 +34,9 @@ export function MiniRepl({ }) { const code = tunes ? tunes[0] : tune; const id = useMemo(() => s4(), []); - const canvasId = useMemo(() => `canvas-${id}`, [id]); - autodraw = !!punchcard || !!claviature || !!autodraw; const shouldShowCanvas = !!punchcard; + const canvasId = shouldShowCanvas ? useMemo(() => `canvas-${id}`, [id]) : null; + autodraw = !!punchcard || !!claviature || !!autodraw; drawTime = drawTime ?? punchcard ? [0, 4] : [-2, 2]; if (claviature) { drawTime = [0, 0]; diff --git a/website/src/pages/learn/visual-feedback.mdx b/website/src/pages/learn/visual-feedback.mdx index ed5b16c2..25eae30c 100644 --- a/website/src/pages/learn/visual-feedback.mdx +++ b/website/src/pages/learn/visual-feedback.mdx @@ -31,55 +31,70 @@ You can change the color as well, even pattern it: .color("cyan magenta")`} /> -## Pianoroll and Punchcard +## Global vs Inline Visuals + +The following functions all come with in 2 variants. + +**Without prefix**: renders the visual to the background of the page: + + + +**With `_` prefix**: renders the visual inside the code. Allows for multiple visuals + + + +Here we see the 2 variants for `punchcard`. The same goes for all others below. +To improve readability the following demos will all use the inline variant. + +## Punchcard / Pianoroll + +These 2 functions render a pianoroll style visual. +The only difference between the 2 is that `pianoroll` will render the pattern directly, +while `punchcard` will also take the transformations into account that occur afterwards: *8") -.scale("/4:minor:pentatonic") -.s("supersaw").lpf(300).lpenv("<4 3 2>*4") -._punchcard()`} + tune={`note("c a f e").color("white") +._punchcard() +.color("cyan")`} autodraw /> +Here, the `color` is still visible in the visual, even if it is applied after `_punchcard`. +On the contrary, the color is not visible when using `_pianoroll`: + *8") -.scale("/4:minor:pentatonic") -.s("supersaw").lpf(300).lpenv("<4 3 2>\*4") -.\_pianoroll()`} + tune={`note("c a f e").color("white") +._pianoroll() +.color("cyan")`} autodraw /> +import Box from '@components/Box.astro'; + +
+ + + +`punchcard` is less resource intensive because it uses the same data as used for the mini notation highlighting. + + + +The visual can be customized by passing options. Those options are the same for both functions. + +What follows is the API doc of all the options you can pass: + + + ## Spiral -*8") -.scale("/4:minor:pentatonic") -.s("supersaw").lpf(300).lpenv("<4 3 2>*4") -._spiral()`} - autodraw -/> + ## Scope -*8") -.scale("/4:minor:pentatonic") -.s("supersaw").lpf(300).lpenv("<4 3 2>*4") -._scope()`} -/> + ## Pitchwheel -*8") -.scale("/4:minor:pentatonic") -.s("supersaw").lpf(300).lpenv("<4 3 2>*4") -._pitchwheel()`} -/> - -{/* */} + From 357fbf8c825076e8eb508536abe22db1e46576e0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 2 Jun 2024 23:14:32 +0200 Subject: [PATCH 034/547] snapshot --- test/__snapshots__/examples.test.mjs.snap | 170 +++++++++++++++++----- test/runtime.mjs | 9 ++ 2 files changed, 139 insertions(+), 40 deletions(-) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 0cd653e3..3c1e39f8 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -4042,19 +4042,19 @@ exports[`runs examples > example "loopAtCps" example index 0 1`] = ` exports[`runs examples > example "loopBegin" example index 0 1`] = ` [ - "[ 0/1 → 1/1 | s:space loop:1 loopBegin:0 analyze:1 ]", - "[ 1/1 → 2/1 | s:space loop:1 loopBegin:0.125 analyze:1 ]", - "[ 2/1 → 3/1 | s:space loop:1 loopBegin:0.25 analyze:1 ]", - "[ 3/1 → 4/1 | s:space loop:1 loopBegin:0 analyze:1 ]", + "[ 0/1 → 1/1 | s:space loop:1 loopBegin:0 ]", + "[ 1/1 → 2/1 | s:space loop:1 loopBegin:0.125 ]", + "[ 2/1 → 3/1 | s:space loop:1 loopBegin:0.25 ]", + "[ 3/1 → 4/1 | s:space loop:1 loopBegin:0 ]", ] `; exports[`runs examples > example "loopEnd" example index 0 1`] = ` [ - "[ 0/1 → 1/1 | s:space loop:1 loopEnd:1 analyze:1 ]", - "[ 1/1 → 2/1 | s:space loop:1 loopEnd:0.75 analyze:1 ]", - "[ 2/1 → 3/1 | s:space loop:1 loopEnd:0.5 analyze:1 ]", - "[ 3/1 → 4/1 | s:space loop:1 loopEnd:0.25 analyze:1 ]", + "[ 0/1 → 1/1 | s:space loop:1 loopEnd:1 ]", + "[ 1/1 → 2/1 | s:space loop:1 loopEnd:0.75 ]", + "[ 2/1 → 3/1 | s:space loop:1 loopEnd:0.5 ]", + "[ 3/1 → 4/1 | s:space loop:1 loopEnd:0.25 ]", ] `; @@ -5085,34 +5085,34 @@ exports[`runs examples > example "phasersweep" example index 0 1`] = ` exports[`runs examples > example "pianoroll" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | note:C2 s:piano clip:1 color:salmon ]", - "[ (1/4 → 1/3) ⇝ 3/8 | note:C2 s:piano clip:1 color:salmon ]", - "[ 1/4 ⇜ (1/3 → 3/8) | note:A2 s:piano clip:1 color:salmon ]", - "[ 3/8 → 1/2 | note:A2 s:piano clip:1 color:salmon ]", - "[ (5/8 → 2/3) ⇝ 3/4 | note:A2 s:piano clip:1 color:salmon ]", - "[ 5/8 ⇜ (2/3 → 3/4) | note:G2 s:piano clip:1 color:salmon ]", - "[ 3/4 → 7/8 | note:G2 s:piano clip:1 color:salmon ]", - "[ 1/1 → 9/8 | note:C2 s:piano clip:1 color:salmon ]", - "[ (5/4 → 4/3) ⇝ 11/8 | note:C2 s:piano clip:1 color:salmon ]", - "[ 5/4 ⇜ (4/3 → 11/8) | note:A2 s:piano clip:1 color:salmon ]", - "[ 11/8 → 3/2 | note:A2 s:piano clip:1 color:salmon ]", - "[ (13/8 → 5/3) ⇝ 7/4 | note:A2 s:piano clip:1 color:salmon ]", - "[ 13/8 ⇜ (5/3 → 7/4) | note:G2 s:piano clip:1 color:salmon ]", - "[ 7/4 → 15/8 | note:G2 s:piano clip:1 color:salmon ]", - "[ 2/1 → 17/8 | note:C2 s:piano clip:1 color:salmon ]", - "[ (9/4 → 7/3) ⇝ 19/8 | note:C2 s:piano clip:1 color:salmon ]", - "[ 9/4 ⇜ (7/3 → 19/8) | note:A2 s:piano clip:1 color:salmon ]", - "[ 19/8 → 5/2 | note:A2 s:piano clip:1 color:salmon ]", - "[ (21/8 → 8/3) ⇝ 11/4 | note:A2 s:piano clip:1 color:salmon ]", - "[ 21/8 ⇜ (8/3 → 11/4) | note:G2 s:piano clip:1 color:salmon ]", - "[ 11/4 → 23/8 | note:G2 s:piano clip:1 color:salmon ]", - "[ 3/1 → 25/8 | note:C2 s:piano clip:1 color:salmon ]", - "[ (13/4 → 10/3) ⇝ 27/8 | note:C2 s:piano clip:1 color:salmon ]", - "[ 13/4 ⇜ (10/3 → 27/8) | note:A2 s:piano clip:1 color:salmon ]", - "[ 27/8 → 7/2 | note:A2 s:piano clip:1 color:salmon ]", - "[ (29/8 → 11/3) ⇝ 15/4 | note:A2 s:piano clip:1 color:salmon ]", - "[ 29/8 ⇜ (11/3 → 15/4) | note:G2 s:piano clip:1 color:salmon ]", - "[ 15/4 → 31/8 | note:G2 s:piano clip:1 color:salmon ]", + "[ 0/1 → 1/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ (1/4 → 1/3) ⇝ 3/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 1/4 ⇜ (1/3 → 3/8) | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 3/8 → 1/2 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ (5/8 → 2/3) ⇝ 3/4 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 5/8 ⇜ (2/3 → 3/4) | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 3/4 → 7/8 | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 1/1 → 9/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ (5/4 → 4/3) ⇝ 11/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 5/4 ⇜ (4/3 → 11/8) | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 11/8 → 3/2 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ (13/8 → 5/3) ⇝ 7/4 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 13/8 ⇜ (5/3 → 7/4) | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 7/4 → 15/8 | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 2/1 → 17/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ (9/4 → 7/3) ⇝ 19/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 9/4 ⇜ (7/3 → 19/8) | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 19/8 → 5/2 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ (21/8 → 8/3) ⇝ 11/4 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 21/8 ⇜ (8/3 → 11/4) | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 11/4 → 23/8 | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 3/1 → 25/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ (13/4 → 10/3) ⇝ 27/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 13/4 ⇜ (10/3 → 27/8) | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 27/8 → 7/2 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ (29/8 → 11/3) ⇝ 15/4 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 29/8 ⇜ (11/3 → 15/4) | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 15/4 → 31/8 | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]", ] `; @@ -5234,6 +5234,63 @@ exports[`runs examples > example "pickF" example index 1 1`] = ` ] `; +exports[`runs examples > example "pitchwheel" example index 0 1`] = ` +[ + "[ 0/1 → 1/13 | note:C3 s:sawtooth cutoff:500 ]", + "[ 1/13 → 2/13 | note:Db3 s:sawtooth cutoff:500 ]", + "[ 2/13 → 3/13 | note:D3 s:sawtooth cutoff:500 ]", + "[ 3/13 → 4/13 | note:Eb3 s:sawtooth cutoff:500 ]", + "[ 4/13 → 5/13 | note:E3 s:sawtooth cutoff:500 ]", + "[ 5/13 → 6/13 | note:F3 s:sawtooth cutoff:500 ]", + "[ 6/13 → 7/13 | note:Gb3 s:sawtooth cutoff:500 ]", + "[ 7/13 → 8/13 | note:G3 s:sawtooth cutoff:500 ]", + "[ 8/13 → 9/13 | note:Ab3 s:sawtooth cutoff:500 ]", + "[ 9/13 → 10/13 | note:A3 s:sawtooth cutoff:500 ]", + "[ 10/13 → 11/13 | note:Bb3 s:sawtooth cutoff:500 ]", + "[ 11/13 → 12/13 | note:B3 s:sawtooth cutoff:500 ]", + "[ 12/13 → 1/1 | note:C4 s:sawtooth cutoff:500 ]", + "[ 1/1 → 14/13 | note:C3 s:sawtooth cutoff:500 ]", + "[ 14/13 → 15/13 | note:Db3 s:sawtooth cutoff:500 ]", + "[ 15/13 → 16/13 | note:D3 s:sawtooth cutoff:500 ]", + "[ 16/13 → 17/13 | note:Eb3 s:sawtooth cutoff:500 ]", + "[ 17/13 → 18/13 | note:E3 s:sawtooth cutoff:500 ]", + "[ 18/13 → 19/13 | note:F3 s:sawtooth cutoff:500 ]", + "[ 19/13 → 20/13 | note:Gb3 s:sawtooth cutoff:500 ]", + "[ 20/13 → 21/13 | note:G3 s:sawtooth cutoff:500 ]", + "[ 21/13 → 22/13 | note:Ab3 s:sawtooth cutoff:500 ]", + "[ 22/13 → 23/13 | note:A3 s:sawtooth cutoff:500 ]", + "[ 23/13 → 24/13 | note:Bb3 s:sawtooth cutoff:500 ]", + "[ 24/13 → 25/13 | note:B3 s:sawtooth cutoff:500 ]", + "[ 25/13 → 2/1 | note:C4 s:sawtooth cutoff:500 ]", + "[ 2/1 → 27/13 | note:C3 s:sawtooth cutoff:500 ]", + "[ 27/13 → 28/13 | note:Db3 s:sawtooth cutoff:500 ]", + "[ 28/13 → 29/13 | note:D3 s:sawtooth cutoff:500 ]", + "[ 29/13 → 30/13 | note:Eb3 s:sawtooth cutoff:500 ]", + "[ 30/13 → 31/13 | note:E3 s:sawtooth cutoff:500 ]", + "[ 31/13 → 32/13 | note:F3 s:sawtooth cutoff:500 ]", + "[ 32/13 → 33/13 | note:Gb3 s:sawtooth cutoff:500 ]", + "[ 33/13 → 34/13 | note:G3 s:sawtooth cutoff:500 ]", + "[ 34/13 → 35/13 | note:Ab3 s:sawtooth cutoff:500 ]", + "[ 35/13 → 36/13 | note:A3 s:sawtooth cutoff:500 ]", + "[ 36/13 → 37/13 | note:Bb3 s:sawtooth cutoff:500 ]", + "[ 37/13 → 38/13 | note:B3 s:sawtooth cutoff:500 ]", + "[ 38/13 → 3/1 | note:C4 s:sawtooth cutoff:500 ]", + "[ 3/1 → 40/13 | note:C3 s:sawtooth cutoff:500 ]", + "[ 40/13 → 41/13 | note:Db3 s:sawtooth cutoff:500 ]", + "[ 41/13 → 42/13 | note:D3 s:sawtooth cutoff:500 ]", + "[ 42/13 → 43/13 | note:Eb3 s:sawtooth cutoff:500 ]", + "[ 43/13 → 44/13 | note:E3 s:sawtooth cutoff:500 ]", + "[ 44/13 → 45/13 | note:F3 s:sawtooth cutoff:500 ]", + "[ 45/13 → 46/13 | note:Gb3 s:sawtooth cutoff:500 ]", + "[ 46/13 → 47/13 | note:G3 s:sawtooth cutoff:500 ]", + "[ 47/13 → 48/13 | note:Ab3 s:sawtooth cutoff:500 ]", + "[ 48/13 → 49/13 | note:A3 s:sawtooth cutoff:500 ]", + "[ 49/13 → 50/13 | note:Bb3 s:sawtooth cutoff:500 ]", + "[ 50/13 → 51/13 | note:B3 s:sawtooth cutoff:500 ]", + "[ 51/13 → 4/1 | note:C4 s:sawtooth cutoff:500 ]", +] +`; + exports[`runs examples > example "ply" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:bd ]", @@ -6554,10 +6611,10 @@ exports[`runs examples > example "scaleTranspose" example index 0 1`] = ` exports[`runs examples > example "scope" example index 0 1`] = ` [ - "[ 0/1 → 1/1 | s:sawtooth analyze:1 ]", - "[ 1/1 → 2/1 | s:sawtooth analyze:1 ]", - "[ 2/1 → 3/1 | s:sawtooth analyze:1 ]", - "[ 3/1 → 4/1 | s:sawtooth analyze:1 ]", + "[ 0/1 → 1/1 | s:sawtooth ]", + "[ 1/1 → 2/1 | s:sawtooth ]", + "[ 2/1 → 3/1 | s:sawtooth ]", + "[ 3/1 → 4/1 | s:sawtooth ]", ] `; @@ -7158,6 +7215,39 @@ exports[`runs examples > example "speed" example index 1 1`] = ` ] `; +exports[`runs examples > example "spiral" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ (1/4 → 1/3) ⇝ 3/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 1/4 ⇜ (1/3 → 3/8) | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 3/8 → 1/2 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ (5/8 → 2/3) ⇝ 3/4 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 5/8 ⇜ (2/3 → 3/4) | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 3/4 → 7/8 | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 1/1 → 9/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ (5/4 → 4/3) ⇝ 11/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 5/4 ⇜ (4/3 → 11/8) | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 11/8 → 3/2 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ (13/8 → 5/3) ⇝ 7/4 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 13/8 ⇜ (5/3 → 7/4) | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 7/4 → 15/8 | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 2/1 → 17/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ (9/4 → 7/3) ⇝ 19/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 9/4 ⇜ (7/3 → 19/8) | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 19/8 → 5/2 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ (21/8 → 8/3) ⇝ 11/4 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 21/8 ⇜ (8/3 → 11/4) | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 11/4 → 23/8 | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 3/1 → 25/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ (13/4 → 10/3) ⇝ 27/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 13/4 ⇜ (10/3 → 27/8) | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 27/8 → 7/2 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ (29/8 → 11/3) ⇝ 15/4 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 29/8 ⇜ (11/3 → 15/4) | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]", + "[ 15/4 → 31/8 | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]", +] +`; + exports[`runs examples > example "splice" example index 0 1`] = ` [ "[ 0/1 → 1/8 | speed:1 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]", diff --git a/test/runtime.mjs b/test/runtime.mjs index 7b9350eb..ca92b196 100644 --- a/test/runtime.mjs +++ b/test/runtime.mjs @@ -125,6 +125,15 @@ strudel.Pattern.prototype.midi = function () { strudel.Pattern.prototype._scope = function () { return this; }; +strudel.Pattern.prototype._spiral = function () { + return this; +}; +strudel.Pattern.prototype._pitchwheel = function () { + return this; +}; +strudel.Pattern.prototype._pianoroll = function () { + return this; +}; const uiHelpersMocked = { backgroundImage: id, From 300022d8ac2b1ed6dfd55f47fe5b11c883694bb3 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 3 Jun 2024 20:34:51 +0200 Subject: [PATCH 035/547] no custom line height --- packages/codemirror/themes/algoboy.mjs | 2 +- packages/codemirror/themes/teletext.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/codemirror/themes/algoboy.mjs b/packages/codemirror/themes/algoboy.mjs index eba95b98..fd7db580 100644 --- a/packages/codemirror/themes/algoboy.mjs +++ b/packages/codemirror/themes/algoboy.mjs @@ -12,7 +12,7 @@ export const settings = { gutterBackground: 'transparent', gutterForeground: '#0f380f', light: true, - customStyle: '.cm-line { line-height: 1 }', + // customStyle: '.cm-line { line-height: 1 }', }; export default createTheme({ theme: 'light', diff --git a/packages/codemirror/themes/teletext.mjs b/packages/codemirror/themes/teletext.mjs index ddbf7b33..630329c1 100644 --- a/packages/codemirror/themes/teletext.mjs +++ b/packages/codemirror/themes/teletext.mjs @@ -17,7 +17,7 @@ export const settings = { lineBackground: '#00000040', gutterBackground: 'transparent', gutterForeground: '#8a919966', - customStyle: '.cm-line { line-height: 1 }', + // customStyle: '.cm-line { line-height: 1 }', }; let punctuation = colorD; From c05bbf34220b3ae2c1417237a72933d86259cf59 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 3 Jun 2024 23:53:08 +0200 Subject: [PATCH 036/547] don't clear hydra in minirepl for now --- website/src/docs/MiniRepl.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/docs/MiniRepl.jsx b/website/src/docs/MiniRepl.jsx index 56548471..0884786e 100644 --- a/website/src/docs/MiniRepl.jsx +++ b/website/src/docs/MiniRepl.jsx @@ -81,7 +81,7 @@ export function MiniRepl({ }, onToggle: (playing) => { if (!playing) { - clearHydra(); + // clearHydra(); // TBD: doesn't work with multiple MiniRepl's on a page } }, beforeStart: () => audioReady, From 771eb31d3f4829ed5457d4f1ac4ab326c2816054 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 4 Jun 2024 00:05:51 +0200 Subject: [PATCH 037/547] use labels instead of stacks --- packages/core/controls.mjs | 6 +- website/src/pages/learn/hydra.mdx | 16 ++-- website/src/pages/learn/input-output.mdx | 12 +-- website/src/pages/learn/samples.mdx | 16 ++-- website/src/pages/learn/strudel-vs-tidal.mdx | 27 ++----- website/src/pages/learn/tonal.mdx | 14 ---- website/src/pages/workshop/first-effects.mdx | 79 ++++++++----------- website/src/pages/workshop/first-notes.mdx | 26 ++++++ .../src/pages/workshop/pattern-effects.mdx | 36 ++++----- website/src/pages/workshop/recap.mdx | 1 + 10 files changed, 103 insertions(+), 130 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 94f46f44..5aa4b46b 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1163,10 +1163,8 @@ export const { octave } = registerControl('octave'); * @name orbit * @param {number | Pattern} number * @example - * stack( - * s("hh*6").delay(.5).delaytime(.25).orbit(1), - * s("~ sd ~ sd").delay(.5).delaytime(.125).orbit(2) - * ) + * $: s("hh*6").delay(.5).delaytime(.25).orbit(1) + * $: s("~ sd ~ sd").delay(.5).delaytime(.125).orbit(2) */ export const { orbit } = registerControl('orbit'); // TODO: what is this? not found in tidal doc Answer: gain is limited to maximum of 2. This allows you to go over that diff --git a/website/src/pages/learn/hydra.mdx b/website/src/pages/learn/hydra.mdx index d7a692dd..2bf5fab4 100644 --- a/website/src/pages/learn/hydra.mdx +++ b/website/src/pages/learn/hydra.mdx @@ -90,11 +90,13 @@ src(s0).kaleid(H("<4 5 6>")) .modulateScale(osc(2,-0.25,1)) .out() // -stack( - s("bd*4,[hh:0:<.5 1>]*8,~ rim").bank("RolandTR909").speed(.9), - note("[>>]*3").s("sawtooth") - .room(.75).sometimes(add(note(12))).clip(.3) - .lpa(.05).lpenv(-4).lpf(2000).lpq(8).ftype('24db') -).fft(4) - .scope({pos:0,smear:.95})`} + +$: s("bd*4,[hh:0:<.5 1>]*8,~ rim").bank("RolandTR909").speed(.9) + +$: note("[>>]\*3").s("sawtooth") + +.room(.75).sometimes(add(note(12))).clip(.3) +.lpa(.05).lpenv(-4).lpf(2000).lpq(8).ftype('24db') + +all(x=>x.fft(4).scope({pos:0,smear:.95}))`} /> diff --git a/website/src/pages/learn/input-output.mdx b/website/src/pages/learn/input-output.mdx index de44b4f9..0151e6eb 100644 --- a/website/src/pages/learn/input-output.mdx +++ b/website/src/pages/learn/input-output.mdx @@ -20,11 +20,7 @@ Strudel also supports midi via [webmidi](https://npmjs.com/package/webmidi). Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (Linux) for internal midi messages. If no outputName is given, it uses the first midi output it finds. -".voicings('lefthand'), "").note() - .midi()`} -/> +").voicing().midi()`} /> In the console, you will see a log of the available MIDI devices as soon as you run the code, e.g. `Midi connected! Using "Midi Through Port-0".` @@ -45,10 +41,8 @@ But you can also control cc messages separately like this: # SuperDirt API diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index c2c166b3..44259617 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -268,11 +268,10 @@ With it, you can enter any sample name(s) to query from [freesound.org](https:// You can also generate artificial voice samples with any text, in multiple languages. @@ -282,10 +281,9 @@ Note that the language code and the gender parameters are optional and default t client:idle tune={`samples('shabda/speech:the_drum,forever') samples('shabda/speech/fr-FR/m:magnifique') -stack( - s("the_drum*2").chop(16).speed(rand.range(0.85,1.1)), - s("forever magnifique").slow(4).late(0.125) -)`} + +$: s("the_drum*2").chop(16).speed(rand.range(0.85,1.1)) +$: s("forever magnifique").slow(4).late(0.125)`} /> # Sampler Effects diff --git a/website/src/pages/learn/strudel-vs-tidal.mdx b/website/src/pages/learn/strudel-vs-tidal.mdx index 0c7213c7..40ea188f 100644 --- a/website/src/pages/learn/strudel-vs-tidal.mdx +++ b/website/src/pages/learn/strudel-vs-tidal.mdx @@ -112,29 +112,14 @@ Also, samples are always loaded from a URL rather than from the disk, although [ ## Evaluation The Strudel REPL does not support [block based evaluation](https://github.com/tidalcycles/strudel/issues/34) yet. -You can use the following "workaround" to create multiple patterns that can be turned on and off: +You can use labeled statements and `_` to mute: -``` -let a = note("c a f e") + ## Tempo diff --git a/website/src/pages/learn/tonal.mdx b/website/src/pages/learn/tonal.mdx index a5a02508..202b8481 100644 --- a/website/src/pages/learn/tonal.mdx +++ b/website/src/pages/learn/tonal.mdx @@ -55,20 +55,6 @@ Transposes notes inside the scale by the number of steps: .note()`} /> -### voicings(range?) - -Turns chord symbols into voicings, using the smoothest voice leading possible: - -".voicings('lefthand'), - "" -).note()`} -/> - -Note: This function might be removed, as `voicing` (without s) is a newer implementation. - ### rootNotes(octave = 2) Turns chord symbols into root notes of chords in given octave. diff --git a/website/src/pages/workshop/first-effects.mdx b/website/src/pages/workshop/first-effects.mdx index 220a7cbc..99e72a13 100644 --- a/website/src/pages/workshop/first-effects.mdx +++ b/website/src/pages/workshop/first-effects.mdx @@ -60,11 +60,10 @@ We will learn how to automate with waves later... @@ -76,31 +75,21 @@ Rhythm is all about dynamics! -**stacks within stacks** - Let's combine all of the above into a little tune: ") -.sound("sawtooth").lpf("200 1000 200 1000"), -note("<[c3,g3,e4] [bb2,f3,d4] [a2,f3,c4] [bb2,g3,eb4]>") -.sound("sawtooth").vowel("") -)`} + tune={`$: sound("hh*8").gain("[.25 1]*4") + +$: sound("bd*4,[~ sd:1]*2") + +$: note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>") +.sound("sawtooth").lpf("200 1000 200 1000") + +$: note("<[c3,g3,e4] [bb2,f3,d4] [a2,f3,c4] [bb2,g3,eb4]>") +.sound("sawtooth").vowel("")`} /> - - -Try to identify the individual parts of the stacks, pay attention to where the commas are. -The 3 parts (drums, bassline, chords) are exactly as earlier, just stacked together, separated by comma. - - - **shape the sound with an adsr envelope** ~]]*2") -.sound("gm_electric_guitar_muted"), -sound("bd rim").bank("RolandTR707") -).delay(".5")`} + tune={`$: note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2") + .sound("gm_electric_guitar_muted").delay(.5) + +$: sound("bd rim").bank("RolandTR707").delay(".5")`} /> @@ -199,31 +187,32 @@ Add a delay too! ~]]*2") -.sound("gm_electric_guitar_muted").delay(.5), -sound("bd rim").bank("RolandTR707").delay(.5), -n("<4 [3@3 4] [<2 0> ~@16] ~>") + tune={`$: note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2") +.sound("gm_electric_guitar_muted").delay(.5) + +$: sound("bd rim").bank("RolandTR707").delay(.5) + +$: n("<4 [3@3 4] [<2 0> ~@16] ~>") .scale("D4:minor").sound("gm_accordion:2") -.room(2).gain(.5) -)`} +.room(2).gain(.5)`} /> Let's add a bass to make this complete: ~]]*2") -.sound("gm_electric_guitar_muted").delay(.5), -sound("bd rim").bank("RolandTR707").delay(.5), -n("<4 [3@3 4] [<2 0> ~@16] ~>") + tune={`$: note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2") +.sound("gm_electric_guitar_muted").delay(.5) + +$: sound("bd rim").bank("RolandTR707").delay(.5) + +$: n("<4 [3@3 4] [<2 0> ~@16] ~>") .scale("D4:minor").sound("gm_accordion:2") -.room(2).gain(.4), -n("[0 [~ 0] 4 [3 2] [0 ~] [0 ~] <0 2> ~]/2") +.room(2).gain(.4) + +$: n("[0 [~ 0] 4 [3 2] [0 ~] [0 ~] <0 2> ~]/2") .scale("D2:minor") -.sound("sawtooth,triangle").lpf(800) -)`} +.sound("sawtooth,triangle").lpf(800)`} /> diff --git a/website/src/pages/workshop/first-notes.mdx b/website/src/pages/workshop/first-notes.mdx index 71a21764..f0ea5569 100644 --- a/website/src/pages/workshop/first-notes.mdx +++ b/website/src/pages/workshop/first-notes.mdx @@ -319,6 +319,7 @@ New functions: | note | set pitch as number or letter | | | scale | interpret `n` as scale degree | | | stack | play patterns in parallel (read on) | | +| $: | play patterns in parallel | | ## Examples @@ -377,4 +378,29 @@ It's called `stack` 😙 )`} /> +Since Strudel 1.1.0, there is another, simpler way to write multiple patterns in parallel. Just write `$:` before every part: + +") + .sound("gm_synth_bass_1").lpf(800) + +$: n(\`< + [~ 0] 2 [0 2] [~ 2] + [~ 0] 1 [0 1] [~ 1] + [~ 0] 3 [0 3] [~ 3] + [~ 0] 2 [0 2] [~ 2] + >*4\`).scale("C4:minor") + .sound("gm_synth_strings_1") + +$: sound("bd*4, [~ ]*2, [~ hh]*4") +.bank("RolandTR909")`} +/> + + + +Try changing `$` to `_$` to mute a part! + + + This is starting to sound like actual music! We have sounds, we have notes, now the last piece of the puzzle is missing: [effects](/workshop/first-effects) diff --git a/website/src/pages/workshop/pattern-effects.mdx b/website/src/pages/workshop/pattern-effects.mdx index 06ebd6b0..119a6123 100644 --- a/website/src/pages/workshop/pattern-effects.mdx +++ b/website/src/pages/workshop/pattern-effects.mdx @@ -25,20 +25,16 @@ This is the same as: Let's visualize what happens here: @@ -56,11 +52,9 @@ This is like doing @@ -110,17 +104,17 @@ We can add as often as we like: [~ <4 1>]".add("<0 [0,2,4]>")) + tune={`$: n("0 [2 4] <3 5> [~ <4 1>]".add("<0 [0,2,4]>")) .scale("C5:minor") .sound("gm_xylophone") - .room(.4).delay(.125), - note("c2 [eb3,g3]".add("<0 <1 -1>>")) + .room(.4).delay(.125) + +$: note("c2 [eb3,g3]".add("<0 <1 -1>>")) .adsr("[.1 0]:.2:[1 0]") .sound("gm_acoustic_bass") - .room(.5), - n("0 1 [2 3] 2").sound("jazz").jux(rev) -)`} + .room(.5) + +$: n("0 1 [2 3] 2").sound("jazz").jux(rev)`} /> **ply** @@ -165,7 +159,7 @@ off is also useful for modifying other sounds, and can even be nested: client:visible tune={`s("bd sd [rim bd] sd,[~ hh]*4").bank("CasioRZ1") .off(2/16, x=>x.speed(1.5).gain(.25) - .off(3/16, y=>y.vowel("*8")))`} + .off(3/16, y=>y.vowel("*8")))`} /> | name | description | example | diff --git a/website/src/pages/workshop/recap.mdx b/website/src/pages/workshop/recap.mdx index fda7a171..a800de53 100644 --- a/website/src/pages/workshop/recap.mdx +++ b/website/src/pages/workshop/recap.mdx @@ -40,6 +40,7 @@ This page is just a listing of all functions covered in the workshop! | note | set pitch as number or letter | | | n + scale | set note in scale | | | stack | play patterns in parallel | | +| $: | play patterns in parallel | | ## Audio Effects From 666c695c63a00d4b0f4a1c7bc683a7b451f645ae Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 4 Jun 2024 00:10:46 +0200 Subject: [PATCH 038/547] don't use labeled statements in example for now --- packages/core/controls.mjs | 6 ++++-- test/runtime.mjs | 1 + website/src/pages/workshop/pattern-effects.mdx | 2 -- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 5aa4b46b..94f46f44 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1163,8 +1163,10 @@ export const { octave } = registerControl('octave'); * @name orbit * @param {number | Pattern} number * @example - * $: s("hh*6").delay(.5).delaytime(.25).orbit(1) - * $: s("~ sd ~ sd").delay(.5).delaytime(.125).orbit(2) + * stack( + * s("hh*6").delay(.5).delaytime(.25).orbit(1), + * s("~ sd ~ sd").delay(.5).delaytime(.125).orbit(2) + * ) */ export const { orbit } = registerControl('orbit'); // TODO: what is this? not found in tidal doc Answer: gain is limited to maximum of 2. This allows you to go over that diff --git a/test/runtime.mjs b/test/runtime.mjs index ca92b196..19b71627 100644 --- a/test/runtime.mjs +++ b/test/runtime.mjs @@ -191,6 +191,7 @@ evalScope( }, ); +// TBD: use transpiler to support labeled statements export const queryCode = async (code, cycles = 1) => { const { pattern } = await evaluate(code); const haps = pattern.sortHapsByPart().queryArc(0, cycles); diff --git a/website/src/pages/workshop/pattern-effects.mdx b/website/src/pages/workshop/pattern-effects.mdx index 119a6123..c9cb7b46 100644 --- a/website/src/pages/workshop/pattern-effects.mdx +++ b/website/src/pages/workshop/pattern-effects.mdx @@ -108,12 +108,10 @@ We can add as often as we like: .scale("C5:minor") .sound("gm_xylophone") .room(.4).delay(.125) - $: note("c2 [eb3,g3]".add("<0 <1 -1>>")) .adsr("[.1 0]:.2:[1 0]") .sound("gm_acoustic_bass") .room(.5) - $: n("0 1 [2 3] 2").sound("jazz").jux(rev)`} /> From 269d6dde31873fe536431df26a4e5411b0b034b1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 4 Jun 2024 00:22:49 +0200 Subject: [PATCH 039/547] remove stack from workshop --- website/src/pages/workshop/first-notes.mdx | 32 ++++++---------------- website/src/pages/workshop/recap.mdx | 1 - 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/website/src/pages/workshop/first-notes.mdx b/website/src/pages/workshop/first-notes.mdx index f0ea5569..f3d74ddc 100644 --- a/website/src/pages/workshop/first-notes.mdx +++ b/website/src/pages/workshop/first-notes.mdx @@ -314,12 +314,11 @@ Let's recap what we've learned in this chapter: New functions: -| Name | Description | Example | -| ----- | ----------------------------------- | --------------------------------------------------------------------------------- | -| note | set pitch as number or letter | | -| scale | interpret `n` as scale degree | | -| stack | play patterns in parallel (read on) | | -| $: | play patterns in parallel | | +| Name | Description | Example | +| ----- | ----------------------------- | --------------------------------------------------------------------------------- | +| note | set pitch as number or letter | | +| scale | interpret `n` as scale degree | | +| $: | play patterns in parallel | | ## Examples @@ -357,28 +356,13 @@ New functions: -It's called `stack` 😙 +You can use `$:` 😙 -") - .sound("gm_synth_bass_1").lpf(800), - n(\`< - [~ 0] 2 [0 2] [~ 2] - [~ 0] 1 [0 1] [~ 1] - [~ 0] 3 [0 3] [~ 3] - [~ 0] 2 [0 2] [~ 2] - >*4\`).scale("C4:minor") - .sound("gm_synth_strings_1"), - sound("bd*4, [~ ]*2, [~ hh]*4") - .bank("RolandTR909") -)`} -/> +## Playing multiple patterns -Since Strudel 1.1.0, there is another, simpler way to write multiple patterns in parallel. Just write `$:` before every part: +If you want to play multiple patterns at the same time, make sure to write `$:` before each: | | n + scale | set note in scale | | -| stack | play patterns in parallel | | | $: | play patterns in parallel | | ## Audio Effects From e62d5759e74a544a1d9e56e89b630a08fb825829 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 10 Jun 2024 00:00:43 -0400 Subject: [PATCH 040/547] revert --- packages/superdough/worklets.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index b06513e7..62697ef9 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -463,4 +463,4 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { } } -registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor); +registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor); \ No newline at end of file From e6f3b1f1efca40727230bc1bec88f091094550af Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 10 Jun 2024 00:06:35 -0400 Subject: [PATCH 041/547] copied files --- packages/superdough/ola-processor.js | 178 ++++++++++++++++++++++++++ packages/superdough/worklets.mjs | 180 ++++++++++++++++++++++++++- 2 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 packages/superdough/ola-processor.js diff --git a/packages/superdough/ola-processor.js b/packages/superdough/ola-processor.js new file mode 100644 index 00000000..36ed95bd --- /dev/null +++ b/packages/superdough/ola-processor.js @@ -0,0 +1,178 @@ +"use strict"; + +const WEBAUDIO_BLOCK_SIZE = 128; + +/** Overlap-Add Node */ +class OLAProcessor extends AudioWorkletProcessor { + constructor(options) { + super(options); + + this.nbInputs = options.numberOfInputs; + this.nbOutputs = options.numberOfOutputs; + + this.blockSize = options.processorOptions.blockSize; + // TODO for now, the only support hop size is the size of a web audio block + this.hopSize = WEBAUDIO_BLOCK_SIZE; + + this.nbOverlaps = this.blockSize / this.hopSize; + + // pre-allocate input buffers (will be reallocated if needed) + this.inputBuffers = new Array(this.nbInputs); + this.inputBuffersHead = new Array(this.nbInputs); + this.inputBuffersToSend = new Array(this.nbInputs); + // default to 1 channel per input until we know more + for (let i = 0; i < this.nbInputs; i++) { + this.allocateInputChannels(i, 1); + } + // pre-allocate input buffers (will be reallocated if needed) + this.outputBuffers = new Array(this.nbOutputs); + this.outputBuffersToRetrieve = new Array(this.nbOutputs); + // default to 1 channel per output until we know more + for (let i = 0; i < this.nbOutputs; i++) { + this.allocateOutputChannels(i, 1); + } + } + + /** Handles dynamic reallocation of input/output channels buffer + (channel numbers may lety during lifecycle) **/ + reallocateChannelsIfNeeded(inputs, outputs) { + for (let i = 0; i < this.nbInputs; i++) { + let nbChannels = inputs[i].length; + if (nbChannels != this.inputBuffers[i].length) { + this.allocateInputChannels(i, nbChannels); + } + } + + for (let i = 0; i < this.nbOutputs; i++) { + let nbChannels = outputs[i].length; + if (nbChannels != this.outputBuffers[i].length) { + this.allocateOutputChannels(i, nbChannels); + } + } + } + + allocateInputChannels(inputIndex, nbChannels) { + // allocate input buffers + + this.inputBuffers[inputIndex] = new Array(nbChannels); + for (let i = 0; i < nbChannels; i++) { + this.inputBuffers[inputIndex][i] = new Float32Array(this.blockSize + WEBAUDIO_BLOCK_SIZE); + this.inputBuffers[inputIndex][i].fill(0); + } + + // allocate input buffers to send and head pointers to copy from + // (cannot directly send a pointer/subarray because input may be modified) + this.inputBuffersHead[inputIndex] = new Array(nbChannels); + this.inputBuffersToSend[inputIndex] = new Array(nbChannels); + for (let i = 0; i < nbChannels; i++) { + this.inputBuffersHead[inputIndex][i] = this.inputBuffers[inputIndex][i] .subarray(0, this.blockSize); + this.inputBuffersToSend[inputIndex][i] = new Float32Array(this.blockSize); + } + } + + allocateOutputChannels(outputIndex, nbChannels) { + // allocate output buffers + this.outputBuffers[outputIndex] = new Array(nbChannels); + for (let i = 0; i < nbChannels; i++) { + this.outputBuffers[outputIndex][i] = new Float32Array(this.blockSize); + this.outputBuffers[outputIndex][i].fill(0); + } + + // allocate output buffers to retrieve + // (cannot send a pointer/subarray because new output has to be add to exising output) + this.outputBuffersToRetrieve[outputIndex] = new Array(nbChannels); + for (let i = 0; i < nbChannels; i++) { + this.outputBuffersToRetrieve[outputIndex][i] = new Float32Array(this.blockSize); + this.outputBuffersToRetrieve[outputIndex][i].fill(0); + } + } + + /** Read next web audio block to input buffers **/ + readInputs(inputs) { + // when playback is paused, we may stop receiving new samples + if (inputs[0].length && inputs[0][0].length == 0) { + for (let i = 0; i < this.nbInputs; i++) { + for (let j = 0; j < this.inputBuffers[i].length; j++) { + this.inputBuffers[i][j].fill(0, this.blockSize); + } + } + return; + } + + for (let i = 0; i < this.nbInputs; i++) { + for (let j = 0; j < this.inputBuffers[i].length; j++) { + let webAudioBlock = inputs[i][j]; + this.inputBuffers[i][j].set(webAudioBlock, this.blockSize); + } + } + } + + /** Write next web audio block from output buffers **/ + writeOutputs(outputs) { + for (let i = 0; i < this.nbInputs; i++) { + for (let j = 0; j < this.inputBuffers[i].length; j++) { + let webAudioBlock = this.outputBuffers[i][j].subarray(0, WEBAUDIO_BLOCK_SIZE); + outputs[i][j].set(webAudioBlock); + } + } + } + + /** Shift left content of input buffers to receive new web audio block **/ + shiftInputBuffers() { + for (let i = 0; i < this.nbInputs; i++) { + for (let j = 0; j < this.inputBuffers[i].length; j++) { + this.inputBuffers[i][j].copyWithin(0, WEBAUDIO_BLOCK_SIZE); + } + } + } + + /** Shift left content of output buffers to receive new web audio block **/ + shiftOutputBuffers() { + for (let i = 0; i < this.nbOutputs; i++) { + for (let j = 0; j < this.outputBuffers[i].length; j++) { + this.outputBuffers[i][j].copyWithin(0, WEBAUDIO_BLOCK_SIZE); + this.outputBuffers[i][j].subarray(this.blockSize - WEBAUDIO_BLOCK_SIZE).fill(0); + } + } + } + + /** Copy contents of input buffers to buffer actually sent to process **/ + prepareInputBuffersToSend() { + for (let i = 0; i < this.nbInputs; i++) { + for (let j = 0; j < this.inputBuffers[i].length; j++) { + this.inputBuffersToSend[i][j].set(this.inputBuffersHead[i][j]); + } + } + } + + /** Add contents of output buffers just processed to output buffers **/ + handleOutputBuffersToRetrieve() { + for (let i = 0; i < this.nbOutputs; i++) { + for (let j = 0; j < this.outputBuffers[i].length; j++) { + for (let k = 0; k < this.blockSize; k++) { + this.outputBuffers[i][j][k] += this.outputBuffersToRetrieve[i][j][k] / this.nbOverlaps; + } + } + } + } + + process(inputs, outputs, params) { + this.reallocateChannelsIfNeeded(inputs, outputs); + + this.readInputs(inputs); + this.shiftInputBuffers(); + this.prepareInputBuffersToSend() + this.processOLA(this.inputBuffersToSend, this.outputBuffersToRetrieve, params); + this.handleOutputBuffersToRetrieve(); + this.writeOutputs(outputs); + this.shiftOutputBuffers(); + + return true; + } + + processOLA(inputs, outputs, params) { + console.assert(false, "Not overriden"); + } +} + +export default OLAProcessor; \ No newline at end of file diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 62697ef9..3ecc987c 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1,6 +1,10 @@ // coarse, crush, and shape processors adapted from dktr0's webdirt: https://github.com/dktr0/WebDirt/blob/5ce3d698362c54d6e1b68acc47eb2955ac62c793/dist/AudioWorklets.js // LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE // TOFIX: THIS FILE DOES NOT SUPPORT IMPORTS ON DEPOLYMENT + +import OLAProcessor from './ola-processor.js'; +import FFT from 'fft.js'; + const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const _mod = (n, m) => ((n % m) + m) % m; @@ -463,4 +467,178 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { } } -registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor); \ No newline at end of file +registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor); + + + +const BUFFERED_BLOCK_SIZE = 2048; + +function genHannWindow(length) { + let win = new Float32Array(length); + for (var i = 0; i < length; i++) { + win[i] = 0.5 * (1 - Math.cos(2 * Math.PI * i / length)); + } + return win; +} + +class PhaseVocoderProcessor extends OLAProcessor { + static get parameterDescriptors() { + return [{ + name: 'pitchFactor', + defaultValue: 1.0 + }]; + } + + constructor(options) { + options.processorOptions = { + blockSize: BUFFERED_BLOCK_SIZE, + }; + super(options); + + this.fftSize = this.blockSize; + this.timeCursor = 0; + + this.hannWindow = genHannWindow(this.blockSize); + + // prepare FFT and pre-allocate buffers + this.fft = new FFT(this.fftSize); + this.freqComplexBuffer = this.fft.createComplexArray(); + this.freqComplexBufferShifted = this.fft.createComplexArray(); + this.timeComplexBuffer = this.fft.createComplexArray(); + this.magnitudes = new Float32Array(this.fftSize / 2 + 1); + this.peakIndexes = new Int32Array(this.magnitudes.length); + this.nbPeaks = 0; + } + + processOLA(inputs, outputs, parameters) { + // no automation, take last value + const pitchFactor = parameters.pitchFactor[parameters.pitchFactor.length - 1]; + + for (var i = 0; i < this.nbInputs; i++) { + for (var j = 0; j < inputs[i].length; j++) { + // big assumption here: output is symetric to input + var input = inputs[i][j]; + var output = outputs[i][j]; + + this.applyHannWindow(input); + + this.fft.realTransform(this.freqComplexBuffer, input); + + this.computeMagnitudes(); + this.findPeaks(); + this.shiftPeaks(pitchFactor); + + this.fft.completeSpectrum(this.freqComplexBufferShifted); + this.fft.inverseTransform(this.timeComplexBuffer, this.freqComplexBufferShifted); + this.fft.fromComplexArray(this.timeComplexBuffer, output); + + this.applyHannWindow(output); + } + } + + this.timeCursor += this.hopSize; + } + + /** Apply Hann window in-place */ + applyHannWindow(input) { + for (var i = 0; i < this.blockSize; i++) { + input[i] = input[i] * this.hannWindow[i]; + } + } + + /** Compute squared magnitudes for peak finding **/ + computeMagnitudes() { + var i = 0, j = 0; + while (i < this.magnitudes.length) { + let real = this.freqComplexBuffer[j]; + let imag = this.freqComplexBuffer[j + 1]; + // no need to sqrt for peak finding + this.magnitudes[i] = real ** 2 + imag ** 2; + i+=1; + j+=2; + } + } + + /** Find peaks in spectrum magnitudes **/ + findPeaks() { + this.nbPeaks = 0; + var i = 2; + let end = this.magnitudes.length - 2; + + while (i < end) { + let mag = this.magnitudes[i]; + + if (this.magnitudes[i - 1] >= mag || this.magnitudes[i - 2] >= mag) { + i++; + continue; + } + if (this.magnitudes[i + 1] >= mag || this.magnitudes[i + 2] >= mag) { + i++; + continue; + } + + this.peakIndexes[this.nbPeaks] = i; + this.nbPeaks++; + i += 2; + } + } + + /** Shift peaks and regions of influence by pitchFactor into new specturm */ + shiftPeaks(pitchFactor) { + // zero-fill new spectrum + this.freqComplexBufferShifted.fill(0); + + for (var i = 0; i < this.nbPeaks; i++) { + let peakIndex = this.peakIndexes[i]; + let peakIndexShifted = Math.round(peakIndex * pitchFactor); + + if (peakIndexShifted > this.magnitudes.length) { + break; + } + + // find region of influence + var startIndex = 0; + var endIndex = this.fftSize; + if (i > 0) { + let peakIndexBefore = this.peakIndexes[i - 1]; + startIndex = peakIndex - Math.floor((peakIndex - peakIndexBefore) / 2); + } + if (i < this.nbPeaks - 1) { + let peakIndexAfter = this.peakIndexes[i + 1]; + endIndex = peakIndex + Math.ceil((peakIndexAfter - peakIndex) / 2); + } + + // shift whole region of influence around peak to shifted peak + let startOffset = startIndex - peakIndex; + let endOffset = endIndex - peakIndex; + for (var j = startOffset; j < endOffset; j++) { + let binIndex = peakIndex + j; + let binIndexShifted = peakIndexShifted + j; + + if (binIndexShifted >= this.magnitudes.length) { + break; + } + + // apply phase correction + let omegaDelta = 2 * Math.PI * (binIndexShifted - binIndex) / this.fftSize; + let phaseShiftReal = Math.cos(omegaDelta * this.timeCursor); + let phaseShiftImag = Math.sin(omegaDelta * this.timeCursor); + + let indexReal = binIndex * 2; + let indexImag = indexReal + 1; + let valueReal = this.freqComplexBuffer[indexReal]; + let valueImag = this.freqComplexBuffer[indexImag]; + + let valueShiftedReal = valueReal * phaseShiftReal - valueImag * phaseShiftImag; + let valueShiftedImag = valueReal * phaseShiftImag + valueImag * phaseShiftReal; + + let indexShiftedReal = binIndexShifted * 2; + let indexShiftedImag = indexShiftedReal + 1; + this.freqComplexBufferShifted[indexShiftedReal] += valueShiftedReal; + this.freqComplexBufferShifted[indexShiftedImag] += valueShiftedImag; + } + } + } +} + +registerProcessor("phase-vocoder-processor", PhaseVocoderProcessor); \ No newline at end of file From 758b94776dfcef50b7fbe92dcfecc0a67c12e27b Mon Sep 17 00:00:00 2001 From: Matthew Kaney Date: Mon, 10 Jun 2024 19:10:17 -0400 Subject: [PATCH 042/547] Configure base url for @strudel/web package --- packages/web/vite.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/web/vite.config.js b/packages/web/vite.config.js index bd14f5a9..99b93956 100644 --- a/packages/web/vite.config.js +++ b/packages/web/vite.config.js @@ -5,6 +5,7 @@ import replace from '@rollup/plugin-replace'; // https://vitejs.dev/config/ export default defineConfig({ + base: './', plugins: [], build: { lib: { From 065b24a2ee8c9441678a74679406ccc7714b838f Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 10 Jun 2024 23:51:38 -0400 Subject: [PATCH 043/547] working --- package.json | 1 + packages/core/controls.mjs | 15 + packages/superdough/fft.js | 516 +++++++++++++++++++++++++++ packages/superdough/ola-processor.js | 9 +- packages/superdough/package.json | 1 + packages/superdough/superdough.mjs | 3 + packages/superdough/worklets.mjs | 13 +- pnpm-lock.yaml | 33 ++ 8 files changed, 586 insertions(+), 5 deletions(-) create mode 100644 packages/superdough/fft.js diff --git a/package.json b/package.json index 8aefa102..e8bf591a 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "eslint": "^8.56.0", "eslint-plugin-import": "^2.29.1", "events": "^3.3.0", + "fft-js": "^0.0.12", "jsdoc": "^4.0.2", "jsdoc-json": "^2.0.2", "jsdoc-to-markdown": "^8.0.0", diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 94f46f44..e0f29eaf 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1383,6 +1383,19 @@ export const { compressorRelease } = registerControl('compressorRelease'); * */ export const { speed } = registerControl('speed'); + +/** + * Changes the speed of sample playback, i.e. a cheap way of changing pitch. + * + * @name stretch + * @param {number | Pattern} factor -inf to inf, negative numbers play the sample backwards. + * @example + * s("bd*6").speed("1 2 4 1 -2 -4") + * @example + * speed("1 1.5*2 [2 1.1]").s("piano").clip(1) + * + */ +export const { stretch } = registerControl('stretch'); /** * Used in conjunction with `speed`, accepts values of "r" (rate, default behavior), "c" (cycles), or "s" (seconds). Using `unit "c"` means `speed` will be interpreted in units of cycles, e.g. `speed "1"` means samples will be stretched to fill a cycle. Using `unit "s"` means the playback speed will be adjusted so that the duration is the number of seconds specified by `speed`. * @@ -1393,6 +1406,8 @@ export const { speed } = registerControl('speed'); * @superdirtOnly * */ + + export const { unit } = registerControl('unit'); /** * Made by Calum Gunn. Reminiscent of some weird mixture of filter, ring-modulator and pitch-shifter. The SuperCollider manual defines Squiz as: diff --git a/packages/superdough/fft.js b/packages/superdough/fft.js new file mode 100644 index 00000000..0c478693 --- /dev/null +++ b/packages/superdough/fft.js @@ -0,0 +1,516 @@ +'use strict'; +// sourced from https://github.com/indutny/fft.js/ +// LICENSE +// This software is licensed under the MIT License. +// Copyright Fedor Indutny, 2017. +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +export default class FFT { + constructor(size) { + this.size = size | 0; + if (this.size <= 1 || (this.size & (this.size - 1)) !== 0) + throw new Error('FFT size must be a power of two and bigger than 1'); + + this._csize = size << 1; + + // NOTE: Use of `var` is intentional for old V8 versions + var table = new Array(this.size * 2); + for (var i = 0; i < table.length; i += 2) { + const angle = Math.PI * i / this.size; + table[i] = Math.cos(angle); + table[i + 1] = -Math.sin(angle); + } + this.table = table; + + // Find size's power of two + var power = 0; + for (var t = 1; this.size > t; t <<= 1) + power++; + + // Calculate initial step's width: + // * If we are full radix-4 - it is 2x smaller to give inital len=8 + // * Otherwise it is the same as `power` to give len=4 + this._width = power % 2 === 0 ? power - 1 : power; + + // Pre-compute bit-reversal patterns + this._bitrev = new Array(1 << this._width); + for (var j = 0; j < this._bitrev.length; j++) { + this._bitrev[j] = 0; + for (var shift = 0; shift < this._width; shift += 2) { + var revShift = this._width - shift - 2; + this._bitrev[j] |= ((j >>> shift) & 3) << revShift; + } + } + + this._out = null; + this._data = null; + this._inv = 0; + } + fromComplexArray(complex, storage) { + var res = storage || new Array(complex.length >>> 1); + for (var i = 0; i < complex.length; i += 2) + res[i >>> 1] = complex[i]; + return res; + } + createComplexArray() { + const res = new Array(this._csize); + for (var i = 0; i < res.length; i++) + res[i] = 0; + return res; + } + toComplexArray(input, storage) { + var res = storage || this.createComplexArray(); + for (var i = 0; i < res.length; i += 2) { + res[i] = input[i >>> 1]; + res[i + 1] = 0; + } + return res; + } + completeSpectrum(spectrum) { + var size = this._csize; + var half = size >>> 1; + for (var i = 2; i < half; i += 2) { + spectrum[size - i] = spectrum[i]; + spectrum[size - i + 1] = -spectrum[i + 1]; + } + } + transform(out, data) { + if (out === data) + throw new Error('Input and output buffers must be different'); + + this._out = out; + this._data = data; + this._inv = 0; + this._transform4(); + this._out = null; + this._data = null; + } + realTransform(out, data) { + if (out === data) + throw new Error('Input and output buffers must be different'); + + this._out = out; + this._data = data; + this._inv = 0; + this._realTransform4(); + this._out = null; + this._data = null; + } + inverseTransform(out, data) { + if (out === data) + throw new Error('Input and output buffers must be different'); + + this._out = out; + this._data = data; + this._inv = 1; + this._transform4(); + for (var i = 0; i < out.length; i++) + out[i] /= this.size; + this._out = null; + this._data = null; + } + // radix-4 implementation + // + // NOTE: Uses of `var` are intentional for older V8 version that do not + // support both `let compound assignments` and `const phi` + _transform4() { + var out = this._out; + var size = this._csize; + + // Initial step (permute and transform) + var width = this._width; + var step = 1 << width; + var len = (size / step) << 1; + + var outOff; + var t; + var bitrev = this._bitrev; + if (len === 4) { + for (outOff = 0, t = 0; outOff < size; outOff += len, t++) { + const off = bitrev[t]; + this._singleTransform2(outOff, off, step); + } + } else { + // len === 8 + for (outOff = 0, t = 0; outOff < size; outOff += len, t++) { + const off = bitrev[t]; + this._singleTransform4(outOff, off, step); + } + } + + // Loop through steps in decreasing order + var inv = this._inv ? -1 : 1; + var table = this.table; + for (step >>= 2; step >= 2; step >>= 2) { + len = (size / step) << 1; + var quarterLen = len >>> 2; + + // Loop through offsets in the data + for (outOff = 0; outOff < size; outOff += len) { + // Full case + var limit = outOff + quarterLen; + for (var i = outOff, k = 0; i < limit; i += 2, k += step) { + const A = i; + const B = A + quarterLen; + const C = B + quarterLen; + const D = C + quarterLen; + + // Original values + const Ar = out[A]; + const Ai = out[A + 1]; + const Br = out[B]; + const Bi = out[B + 1]; + const Cr = out[C]; + const Ci = out[C + 1]; + const Dr = out[D]; + const Di = out[D + 1]; + + // Middle values + const MAr = Ar; + const MAi = Ai; + + const tableBr = table[k]; + const tableBi = inv * table[k + 1]; + const MBr = Br * tableBr - Bi * tableBi; + const MBi = Br * tableBi + Bi * tableBr; + + const tableCr = table[2 * k]; + const tableCi = inv * table[2 * k + 1]; + const MCr = Cr * tableCr - Ci * tableCi; + const MCi = Cr * tableCi + Ci * tableCr; + + const tableDr = table[3 * k]; + const tableDi = inv * table[3 * k + 1]; + const MDr = Dr * tableDr - Di * tableDi; + const MDi = Dr * tableDi + Di * tableDr; + + // Pre-Final values + const T0r = MAr + MCr; + const T0i = MAi + MCi; + const T1r = MAr - MCr; + const T1i = MAi - MCi; + const T2r = MBr + MDr; + const T2i = MBi + MDi; + const T3r = inv * (MBr - MDr); + const T3i = inv * (MBi - MDi); + + // Final values + const FAr = T0r + T2r; + const FAi = T0i + T2i; + + const FCr = T0r - T2r; + const FCi = T0i - T2i; + + const FBr = T1r + T3i; + const FBi = T1i - T3r; + + const FDr = T1r - T3i; + const FDi = T1i + T3r; + + out[A] = FAr; + out[A + 1] = FAi; + out[B] = FBr; + out[B + 1] = FBi; + out[C] = FCr; + out[C + 1] = FCi; + out[D] = FDr; + out[D + 1] = FDi; + } + } + } + } + // radix-2 implementation + // + // NOTE: Only called for len=4 + _singleTransform2(outOff, off, + step) { + const out = this._out; + const data = this._data; + + const evenR = data[off]; + const evenI = data[off + 1]; + const oddR = data[off + step]; + const oddI = data[off + step + 1]; + + const leftR = evenR + oddR; + const leftI = evenI + oddI; + const rightR = evenR - oddR; + const rightI = evenI - oddI; + + out[outOff] = leftR; + out[outOff + 1] = leftI; + out[outOff + 2] = rightR; + out[outOff + 3] = rightI; + } + // radix-4 + // + // NOTE: Only called for len=8 + _singleTransform4(outOff, off, + step) { + const out = this._out; + const data = this._data; + const inv = this._inv ? -1 : 1; + const step2 = step * 2; + const step3 = step * 3; + + // Original values + const Ar = data[off]; + const Ai = data[off + 1]; + const Br = data[off + step]; + const Bi = data[off + step + 1]; + const Cr = data[off + step2]; + const Ci = data[off + step2 + 1]; + const Dr = data[off + step3]; + const Di = data[off + step3 + 1]; + + // Pre-Final values + const T0r = Ar + Cr; + const T0i = Ai + Ci; + const T1r = Ar - Cr; + const T1i = Ai - Ci; + const T2r = Br + Dr; + const T2i = Bi + Di; + const T3r = inv * (Br - Dr); + const T3i = inv * (Bi - Di); + + // Final values + const FAr = T0r + T2r; + const FAi = T0i + T2i; + + const FBr = T1r + T3i; + const FBi = T1i - T3r; + + const FCr = T0r - T2r; + const FCi = T0i - T2i; + + const FDr = T1r - T3i; + const FDi = T1i + T3r; + + out[outOff] = FAr; + out[outOff + 1] = FAi; + out[outOff + 2] = FBr; + out[outOff + 3] = FBi; + out[outOff + 4] = FCr; + out[outOff + 5] = FCi; + out[outOff + 6] = FDr; + out[outOff + 7] = FDi; + } + // Real input radix-4 implementation + _realTransform4() { + var out = this._out; + var size = this._csize; + + // Initial step (permute and transform) + var width = this._width; + var step = 1 << width; + var len = (size / step) << 1; + + var outOff; + var t; + var bitrev = this._bitrev; + if (len === 4) { + for (outOff = 0, t = 0; outOff < size; outOff += len, t++) { + const off = bitrev[t]; + this._singleRealTransform2(outOff, off >>> 1, step >>> 1); + } + } else { + // len === 8 + for (outOff = 0, t = 0; outOff < size; outOff += len, t++) { + const off = bitrev[t]; + this._singleRealTransform4(outOff, off >>> 1, step >>> 1); + } + } + + // Loop through steps in decreasing order + var inv = this._inv ? -1 : 1; + var table = this.table; + for (step >>= 2; step >= 2; step >>= 2) { + len = (size / step) << 1; + var halfLen = len >>> 1; + var quarterLen = halfLen >>> 1; + var hquarterLen = quarterLen >>> 1; + + // Loop through offsets in the data + for (outOff = 0; outOff < size; outOff += len) { + for (var i = 0, k = 0; i <= hquarterLen; i += 2, k += step) { + var A = outOff + i; + var B = A + quarterLen; + var C = B + quarterLen; + var D = C + quarterLen; + + // Original values + var Ar = out[A]; + var Ai = out[A + 1]; + var Br = out[B]; + var Bi = out[B + 1]; + var Cr = out[C]; + var Ci = out[C + 1]; + var Dr = out[D]; + var Di = out[D + 1]; + + // Middle values + var MAr = Ar; + var MAi = Ai; + + var tableBr = table[k]; + var tableBi = inv * table[k + 1]; + var MBr = Br * tableBr - Bi * tableBi; + var MBi = Br * tableBi + Bi * tableBr; + + var tableCr = table[2 * k]; + var tableCi = inv * table[2 * k + 1]; + var MCr = Cr * tableCr - Ci * tableCi; + var MCi = Cr * tableCi + Ci * tableCr; + + var tableDr = table[3 * k]; + var tableDi = inv * table[3 * k + 1]; + var MDr = Dr * tableDr - Di * tableDi; + var MDi = Dr * tableDi + Di * tableDr; + + // Pre-Final values + var T0r = MAr + MCr; + var T0i = MAi + MCi; + var T1r = MAr - MCr; + var T1i = MAi - MCi; + var T2r = MBr + MDr; + var T2i = MBi + MDi; + var T3r = inv * (MBr - MDr); + var T3i = inv * (MBi - MDi); + + // Final values + var FAr = T0r + T2r; + var FAi = T0i + T2i; + + var FBr = T1r + T3i; + var FBi = T1i - T3r; + + out[A] = FAr; + out[A + 1] = FAi; + out[B] = FBr; + out[B + 1] = FBi; + + // Output final middle point + if (i === 0) { + var FCr = T0r - T2r; + var FCi = T0i - T2i; + out[C] = FCr; + out[C + 1] = FCi; + continue; + } + + // Do not overwrite ourselves + if (i === hquarterLen) + continue; + + // In the flipped case: + // MAi = -MAi + // MBr=-MBi, MBi=-MBr + // MCr=-MCr + // MDr=MDi, MDi=MDr + var ST0r = T1r; + var ST0i = -T1i; + var ST1r = T0r; + var ST1i = -T0i; + var ST2r = -inv * T3i; + var ST2i = -inv * T3r; + var ST3r = -inv * T2i; + var ST3i = -inv * T2r; + + var SFAr = ST0r + ST2r; + var SFAi = ST0i + ST2i; + + var SFBr = ST1r + ST3i; + var SFBi = ST1i - ST3r; + + var SA = outOff + quarterLen - i; + var SB = outOff + halfLen - i; + + out[SA] = SFAr; + out[SA + 1] = SFAi; + out[SB] = SFBr; + out[SB + 1] = SFBi; + } + } + } + } + // radix-2 implementation + // + // NOTE: Only called for len=4 + _singleRealTransform2(outOff, + off, + step) { + const out = this._out; + const data = this._data; + + const evenR = data[off]; + const oddR = data[off + step]; + + const leftR = evenR + oddR; + const rightR = evenR - oddR; + + out[outOff] = leftR; + out[outOff + 1] = 0; + out[outOff + 2] = rightR; + out[outOff + 3] = 0; + } + // radix-4 + // + // NOTE: Only called for len=8 + _singleRealTransform4(outOff, + off, + step) { + const out = this._out; + const data = this._data; + const inv = this._inv ? -1 : 1; + const step2 = step * 2; + const step3 = step * 3; + + // Original values + const Ar = data[off]; + const Br = data[off + step]; + const Cr = data[off + step2]; + const Dr = data[off + step3]; + + // Pre-Final values + const T0r = Ar + Cr; + const T1r = Ar - Cr; + const T2r = Br + Dr; + const T3r = inv * (Br - Dr); + + // Final values + const FAr = T0r + T2r; + + const FBr = T1r; + const FBi = -T3r; + + const FCr = T0r - T2r; + + const FDr = T1r; + const FDi = T3r; + + out[outOff] = FAr; + out[outOff + 1] = 0; + out[outOff + 2] = FBr; + out[outOff + 3] = FBi; + out[outOff + 4] = FCr; + out[outOff + 5] = 0; + out[outOff + 6] = FDr; + out[outOff + 7] = FDi; + } +} + + + + + + + + + + + + + + diff --git a/packages/superdough/ola-processor.js b/packages/superdough/ola-processor.js index 36ed95bd..ae7deca2 100644 --- a/packages/superdough/ola-processor.js +++ b/packages/superdough/ola-processor.js @@ -1,12 +1,13 @@ "use strict"; +// sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file const WEBAUDIO_BLOCK_SIZE = 128; /** Overlap-Add Node */ class OLAProcessor extends AudioWorkletProcessor { constructor(options) { super(options); - + this.started = false; this.nbInputs = options.numberOfInputs; this.nbOutputs = options.numberOfOutputs; @@ -157,6 +158,12 @@ class OLAProcessor extends AudioWorkletProcessor { } process(inputs, outputs, params) { + const input = inputs[0]; + const hasInput = !(input[0] === undefined); + if (this.started && !hasInput) { + return false; + } + this.started = hasInput; this.reallocateChannelsIfNeeded(inputs, outputs); this.readInputs(inputs); diff --git a/packages/superdough/package.json b/packages/superdough/package.json index 685ab9c5..11cd562b 100644 --- a/packages/superdough/package.json +++ b/packages/superdough/package.json @@ -35,6 +35,7 @@ "vite": "^5.0.10" }, "dependencies": { + "fft.js": "^4.0.4", "nanostores": "^0.9.5" } } diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index f0475d77..46c56e56 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -371,6 +371,7 @@ export const superdough = async (value, t, hapDuration) => { // coarse, crush, + stretch, shape, shapevol = getDefaultValue('shapevol'), distort, @@ -438,6 +439,8 @@ export const superdough = async (value, t, hapDuration) => { } const chain = []; // audio nodes that will be connected to each other sequentially chain.push(sourceNode); + stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch })); + // gain stage chain.push(gainNode(gain)); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 3ecc987c..61078634 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -2,8 +2,8 @@ // LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE // TOFIX: THIS FILE DOES NOT SUPPORT IMPORTS ON DEPOLYMENT -import OLAProcessor from './ola-processor.js'; -import FFT from 'fft.js'; +import OLAProcessor from "./ola-processor" +import FFT from './fft.js'; const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const _mod = (n, m) => ((n % m) + m) % m; @@ -471,6 +471,8 @@ registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor); + + const BUFFERED_BLOCK_SIZE = 2048; function genHannWindow(length) { @@ -499,7 +501,6 @@ class PhaseVocoderProcessor extends OLAProcessor { this.timeCursor = 0; this.hannWindow = genHannWindow(this.blockSize); - // prepare FFT and pre-allocate buffers this.fft = new FFT(this.fftSize); this.freqComplexBuffer = this.fft.createComplexArray(); @@ -641,4 +642,8 @@ class PhaseVocoderProcessor extends OLAProcessor { } } -registerProcessor("phase-vocoder-processor", PhaseVocoderProcessor); \ No newline at end of file +registerProcessor("phase-vocoder-processor", PhaseVocoderProcessor); + + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a07618b8..9f06a6a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,9 @@ importers: events: specifier: ^3.3.0 version: 3.3.0 + fft-js: + specifier: ^0.0.12 + version: 0.0.12 jsdoc: specifier: ^4.0.2 version: 4.0.2 @@ -430,6 +433,9 @@ importers: packages/superdough: dependencies: + fft.js: + specifier: ^4.0.4 + version: 4.0.4 nanostores: specifier: ^0.9.5 version: 0.9.5 @@ -5879,6 +5885,10 @@ packages: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} + /bit-twiddle@1.0.2: + resolution: {integrity: sha512-B9UhK0DKFZhoTFcfvAzhqsjStvGJp9vYWf3+6SNTtdSQnvIgfkHbgHrg/e4+TH71N2GDu8tpmCVoyfrL1d7ntA==} + dev: true + /bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} dependencies: @@ -6436,6 +6446,13 @@ packages: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} dev: true + /commander@2.7.1: + resolution: {integrity: sha512-5qK/Wsc2fnRCiizV1JlHavWrSGAXQI7AusK423F8zJLwIGq8lmtO5GmO8PVMrtDUJMwTXOFBzSN6OCRD8CEMWw==} + engines: {node: '>= 0.6.x'} + dependencies: + graceful-readlink: 1.0.1 + dev: true + /commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -7683,6 +7700,18 @@ packages: resolution: {integrity: sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==} dev: true + /fft-js@0.0.12: + resolution: {integrity: sha512-nLOa0/SYYnN2NPcLrI81UNSPxyg3q0sGiltfe9G1okg0nxs5CqAwtmaqPQdGcOryeGURaCoQx8Y4AUkhGTh7IQ==} + engines: {node: '>=0.12.0'} + dependencies: + bit-twiddle: 1.0.2 + commander: 2.7.1 + dev: true + + /fft.js@4.0.4: + resolution: {integrity: sha512-f9c00hphOgeQTlDyavwTtu6RiK8AIFjD6+jvXkNkpeQ7rirK3uFWVpalkoS4LAwbdX7mfZ8aoBfFVQX1Re/8aw==} + dev: false + /fftjs@0.0.4: resolution: {integrity: sha512-nIWxQyth1LVD6NH8a+YZUv+McjzbOY6dMe4wv6Pq5cGfP+c8Rd1T8Dsd50DCWlNgzSqA3y9lOkpD6dZD3qHa1A==} dependencies: @@ -8251,6 +8280,10 @@ packages: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} dev: true + /graceful-readlink@1.0.1: + resolution: {integrity: sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==} + dev: true + /graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} From b72e71b0bdadcfd775018e501496a5d7eb6886c0 Mon Sep 17 00:00:00 2001 From: Matthew Kaney Date: Tue, 11 Jun 2024 10:02:57 -0400 Subject: [PATCH 044/547] Update base path build setting for core and repl packages --- packages/core/vite.config.js | 1 + packages/repl/vite.config.js | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/core/vite.config.js b/packages/core/vite.config.js index 5df3edc1..718798fb 100644 --- a/packages/core/vite.config.js +++ b/packages/core/vite.config.js @@ -4,6 +4,7 @@ import { resolve } from 'path'; // https://vitejs.dev/config/ export default defineConfig({ + base: './', plugins: [], build: { lib: { diff --git a/packages/repl/vite.config.js b/packages/repl/vite.config.js index ca209b4e..84781891 100644 --- a/packages/repl/vite.config.js +++ b/packages/repl/vite.config.js @@ -6,6 +6,7 @@ import replace from '@rollup/plugin-replace'; // https://vitejs.dev/config/ export default defineConfig({ + base: './', plugins: [], build: { lib: { From 9f958abb085afe2195de2968e55cacdcccbc6514 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 12 Jun 2024 01:11:58 -0400 Subject: [PATCH 045/547] fixed offset time --- package.json | 1 - packages/core/controls.mjs | 5 +- packages/core/pattern.mjs | 2 + packages/superdough/fft.js | 946 +++++++++++++-------------- packages/superdough/ola-processor.js | 302 ++++----- packages/superdough/package.json | 1 - packages/superdough/superdough.mjs | 17 +- packages/superdough/worklets.mjs | 317 ++++----- pnpm-lock.yaml | 18 - 9 files changed, 784 insertions(+), 825 deletions(-) diff --git a/package.json b/package.json index e8bf591a..8aefa102 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,6 @@ "eslint": "^8.56.0", "eslint-plugin-import": "^2.29.1", "events": "^3.3.0", - "fft-js": "^0.0.12", "jsdoc": "^4.0.2", "jsdoc-json": "^2.0.2", "jsdoc-to-markdown": "^8.0.0", diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index e0f29eaf..9f3b1520 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1390,9 +1390,7 @@ export const { speed } = registerControl('speed'); * @name stretch * @param {number | Pattern} factor -inf to inf, negative numbers play the sample backwards. * @example - * s("bd*6").speed("1 2 4 1 -2 -4") - * @example - * speed("1 1.5*2 [2 1.1]").s("piano").clip(1) + * s("gm_flute").stretch("1 2 .5") * */ export const { stretch } = registerControl('stretch'); @@ -1407,7 +1405,6 @@ export const { stretch } = registerControl('stretch'); * */ - export const { unit } = registerControl('unit'); /** * Made by Calum Gunn. Reminiscent of some weird mixture of filter, ring-modulator and pitch-shifter. The SuperCollider manual defines Squiz as: diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index a8a4f7da..516c5815 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -21,6 +21,7 @@ import { numeralArgs, parseNumeral, pairs, + clamp, } from './util.mjs'; import drawLine from './drawLine.mjs'; import { logger } from './logger.mjs'; @@ -1970,6 +1971,7 @@ export const late = register( true, ); + /** * Plays a portion of a pattern, specified by the beginning and end of a time span. The new resulting pattern is played over the time period of the original pattern: * diff --git a/packages/superdough/fft.js b/packages/superdough/fft.js index 0c478693..38294952 100644 --- a/packages/superdough/fft.js +++ b/packages/superdough/fft.js @@ -8,509 +8,481 @@ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. export default class FFT { - constructor(size) { - this.size = size | 0; - if (this.size <= 1 || (this.size & (this.size - 1)) !== 0) - throw new Error('FFT size must be a power of two and bigger than 1'); + constructor(size) { + this.size = size | 0; + if (this.size <= 1 || (this.size & (this.size - 1)) !== 0) + throw new Error('FFT size must be a power of two and bigger than 1'); - this._csize = size << 1; + this._csize = size << 1; - // NOTE: Use of `var` is intentional for old V8 versions - var table = new Array(this.size * 2); - for (var i = 0; i < table.length; i += 2) { - const angle = Math.PI * i / this.size; - table[i] = Math.cos(angle); - table[i + 1] = -Math.sin(angle); + // NOTE: Use of `var` is intentional for old V8 versions + var table = new Array(this.size * 2); + for (var i = 0; i < table.length; i += 2) { + const angle = (Math.PI * i) / this.size; + table[i] = Math.cos(angle); + table[i + 1] = -Math.sin(angle); + } + this.table = table; + + // Find size's power of two + var power = 0; + for (var t = 1; this.size > t; t <<= 1) power++; + + // Calculate initial step's width: + // * If we are full radix-4 - it is 2x smaller to give inital len=8 + // * Otherwise it is the same as `power` to give len=4 + this._width = power % 2 === 0 ? power - 1 : power; + + // Pre-compute bit-reversal patterns + this._bitrev = new Array(1 << this._width); + for (var j = 0; j < this._bitrev.length; j++) { + this._bitrev[j] = 0; + for (var shift = 0; shift < this._width; shift += 2) { + var revShift = this._width - shift - 2; + this._bitrev[j] |= ((j >>> shift) & 3) << revShift; + } + } + + this._out = null; + this._data = null; + this._inv = 0; + } + fromComplexArray(complex, storage) { + var res = storage || new Array(complex.length >>> 1); + for (var i = 0; i < complex.length; i += 2) res[i >>> 1] = complex[i]; + return res; + } + createComplexArray() { + const res = new Array(this._csize); + for (var i = 0; i < res.length; i++) res[i] = 0; + return res; + } + toComplexArray(input, storage) { + var res = storage || this.createComplexArray(); + for (var i = 0; i < res.length; i += 2) { + res[i] = input[i >>> 1]; + res[i + 1] = 0; + } + return res; + } + completeSpectrum(spectrum) { + var size = this._csize; + var half = size >>> 1; + for (var i = 2; i < half; i += 2) { + spectrum[size - i] = spectrum[i]; + spectrum[size - i + 1] = -spectrum[i + 1]; + } + } + transform(out, data) { + if (out === data) throw new Error('Input and output buffers must be different'); + + this._out = out; + this._data = data; + this._inv = 0; + this._transform4(); + this._out = null; + this._data = null; + } + realTransform(out, data) { + if (out === data) throw new Error('Input and output buffers must be different'); + + this._out = out; + this._data = data; + this._inv = 0; + this._realTransform4(); + this._out = null; + this._data = null; + } + inverseTransform(out, data) { + if (out === data) throw new Error('Input and output buffers must be different'); + + this._out = out; + this._data = data; + this._inv = 1; + this._transform4(); + for (var i = 0; i < out.length; i++) out[i] /= this.size; + this._out = null; + this._data = null; + } + // radix-4 implementation + // + // NOTE: Uses of `var` are intentional for older V8 version that do not + // support both `let compound assignments` and `const phi` + _transform4() { + var out = this._out; + var size = this._csize; + + // Initial step (permute and transform) + var width = this._width; + var step = 1 << width; + var len = (size / step) << 1; + + var outOff; + var t; + var bitrev = this._bitrev; + if (len === 4) { + for (outOff = 0, t = 0; outOff < size; outOff += len, t++) { + const off = bitrev[t]; + this._singleTransform2(outOff, off, step); + } + } else { + // len === 8 + for (outOff = 0, t = 0; outOff < size; outOff += len, t++) { + const off = bitrev[t]; + this._singleTransform4(outOff, off, step); + } + } + + // Loop through steps in decreasing order + var inv = this._inv ? -1 : 1; + var table = this.table; + for (step >>= 2; step >= 2; step >>= 2) { + len = (size / step) << 1; + var quarterLen = len >>> 2; + + // Loop through offsets in the data + for (outOff = 0; outOff < size; outOff += len) { + // Full case + var limit = outOff + quarterLen; + for (var i = outOff, k = 0; i < limit; i += 2, k += step) { + const A = i; + const B = A + quarterLen; + const C = B + quarterLen; + const D = C + quarterLen; + + // Original values + const Ar = out[A]; + const Ai = out[A + 1]; + const Br = out[B]; + const Bi = out[B + 1]; + const Cr = out[C]; + const Ci = out[C + 1]; + const Dr = out[D]; + const Di = out[D + 1]; + + // Middle values + const MAr = Ar; + const MAi = Ai; + + const tableBr = table[k]; + const tableBi = inv * table[k + 1]; + const MBr = Br * tableBr - Bi * tableBi; + const MBi = Br * tableBi + Bi * tableBr; + + const tableCr = table[2 * k]; + const tableCi = inv * table[2 * k + 1]; + const MCr = Cr * tableCr - Ci * tableCi; + const MCi = Cr * tableCi + Ci * tableCr; + + const tableDr = table[3 * k]; + const tableDi = inv * table[3 * k + 1]; + const MDr = Dr * tableDr - Di * tableDi; + const MDi = Dr * tableDi + Di * tableDr; + + // Pre-Final values + const T0r = MAr + MCr; + const T0i = MAi + MCi; + const T1r = MAr - MCr; + const T1i = MAi - MCi; + const T2r = MBr + MDr; + const T2i = MBi + MDi; + const T3r = inv * (MBr - MDr); + const T3i = inv * (MBi - MDi); + + // Final values + const FAr = T0r + T2r; + const FAi = T0i + T2i; + + const FCr = T0r - T2r; + const FCi = T0i - T2i; + + const FBr = T1r + T3i; + const FBi = T1i - T3r; + + const FDr = T1r - T3i; + const FDi = T1i + T3r; + + out[A] = FAr; + out[A + 1] = FAi; + out[B] = FBr; + out[B + 1] = FBi; + out[C] = FCr; + out[C + 1] = FCi; + out[D] = FDr; + out[D + 1] = FDi; } - this.table = table; + } + } + } + // radix-2 implementation + // + // NOTE: Only called for len=4 + _singleTransform2(outOff, off, step) { + const out = this._out; + const data = this._data; - // Find size's power of two - var power = 0; - for (var t = 1; this.size > t; t <<= 1) - power++; + const evenR = data[off]; + const evenI = data[off + 1]; + const oddR = data[off + step]; + const oddI = data[off + step + 1]; - // Calculate initial step's width: - // * If we are full radix-4 - it is 2x smaller to give inital len=8 - // * Otherwise it is the same as `power` to give len=4 - this._width = power % 2 === 0 ? power - 1 : power; + const leftR = evenR + oddR; + const leftI = evenI + oddI; + const rightR = evenR - oddR; + const rightI = evenI - oddI; - // Pre-compute bit-reversal patterns - this._bitrev = new Array(1 << this._width); - for (var j = 0; j < this._bitrev.length; j++) { - this._bitrev[j] = 0; - for (var shift = 0; shift < this._width; shift += 2) { - var revShift = this._width - shift - 2; - this._bitrev[j] |= ((j >>> shift) & 3) << revShift; - } + out[outOff] = leftR; + out[outOff + 1] = leftI; + out[outOff + 2] = rightR; + out[outOff + 3] = rightI; + } + // radix-4 + // + // NOTE: Only called for len=8 + _singleTransform4(outOff, off, step) { + const out = this._out; + const data = this._data; + const inv = this._inv ? -1 : 1; + const step2 = step * 2; + const step3 = step * 3; + + // Original values + const Ar = data[off]; + const Ai = data[off + 1]; + const Br = data[off + step]; + const Bi = data[off + step + 1]; + const Cr = data[off + step2]; + const Ci = data[off + step2 + 1]; + const Dr = data[off + step3]; + const Di = data[off + step3 + 1]; + + // Pre-Final values + const T0r = Ar + Cr; + const T0i = Ai + Ci; + const T1r = Ar - Cr; + const T1i = Ai - Ci; + const T2r = Br + Dr; + const T2i = Bi + Di; + const T3r = inv * (Br - Dr); + const T3i = inv * (Bi - Di); + + // Final values + const FAr = T0r + T2r; + const FAi = T0i + T2i; + + const FBr = T1r + T3i; + const FBi = T1i - T3r; + + const FCr = T0r - T2r; + const FCi = T0i - T2i; + + const FDr = T1r - T3i; + const FDi = T1i + T3r; + + out[outOff] = FAr; + out[outOff + 1] = FAi; + out[outOff + 2] = FBr; + out[outOff + 3] = FBi; + out[outOff + 4] = FCr; + out[outOff + 5] = FCi; + out[outOff + 6] = FDr; + out[outOff + 7] = FDi; + } + // Real input radix-4 implementation + _realTransform4() { + var out = this._out; + var size = this._csize; + + // Initial step (permute and transform) + var width = this._width; + var step = 1 << width; + var len = (size / step) << 1; + + var outOff; + var t; + var bitrev = this._bitrev; + if (len === 4) { + for (outOff = 0, t = 0; outOff < size; outOff += len, t++) { + const off = bitrev[t]; + this._singleRealTransform2(outOff, off >>> 1, step >>> 1); + } + } else { + // len === 8 + for (outOff = 0, t = 0; outOff < size; outOff += len, t++) { + const off = bitrev[t]; + this._singleRealTransform4(outOff, off >>> 1, step >>> 1); + } + } + + // Loop through steps in decreasing order + var inv = this._inv ? -1 : 1; + var table = this.table; + for (step >>= 2; step >= 2; step >>= 2) { + len = (size / step) << 1; + var halfLen = len >>> 1; + var quarterLen = halfLen >>> 1; + var hquarterLen = quarterLen >>> 1; + + // Loop through offsets in the data + for (outOff = 0; outOff < size; outOff += len) { + for (var i = 0, k = 0; i <= hquarterLen; i += 2, k += step) { + var A = outOff + i; + var B = A + quarterLen; + var C = B + quarterLen; + var D = C + quarterLen; + + // Original values + var Ar = out[A]; + var Ai = out[A + 1]; + var Br = out[B]; + var Bi = out[B + 1]; + var Cr = out[C]; + var Ci = out[C + 1]; + var Dr = out[D]; + var Di = out[D + 1]; + + // Middle values + var MAr = Ar; + var MAi = Ai; + + var tableBr = table[k]; + var tableBi = inv * table[k + 1]; + var MBr = Br * tableBr - Bi * tableBi; + var MBi = Br * tableBi + Bi * tableBr; + + var tableCr = table[2 * k]; + var tableCi = inv * table[2 * k + 1]; + var MCr = Cr * tableCr - Ci * tableCi; + var MCi = Cr * tableCi + Ci * tableCr; + + var tableDr = table[3 * k]; + var tableDi = inv * table[3 * k + 1]; + var MDr = Dr * tableDr - Di * tableDi; + var MDi = Dr * tableDi + Di * tableDr; + + // Pre-Final values + var T0r = MAr + MCr; + var T0i = MAi + MCi; + var T1r = MAr - MCr; + var T1i = MAi - MCi; + var T2r = MBr + MDr; + var T2i = MBi + MDi; + var T3r = inv * (MBr - MDr); + var T3i = inv * (MBi - MDi); + + // Final values + var FAr = T0r + T2r; + var FAi = T0i + T2i; + + var FBr = T1r + T3i; + var FBi = T1i - T3r; + + out[A] = FAr; + out[A + 1] = FAi; + out[B] = FBr; + out[B + 1] = FBi; + + // Output final middle point + if (i === 0) { + var FCr = T0r - T2r; + var FCi = T0i - T2i; + out[C] = FCr; + out[C + 1] = FCi; + continue; + } + + // Do not overwrite ourselves + if (i === hquarterLen) continue; + + // In the flipped case: + // MAi = -MAi + // MBr=-MBi, MBi=-MBr + // MCr=-MCr + // MDr=MDi, MDi=MDr + var ST0r = T1r; + var ST0i = -T1i; + var ST1r = T0r; + var ST1i = -T0i; + var ST2r = -inv * T3i; + var ST2i = -inv * T3r; + var ST3r = -inv * T2i; + var ST3i = -inv * T2r; + + var SFAr = ST0r + ST2r; + var SFAi = ST0i + ST2i; + + var SFBr = ST1r + ST3i; + var SFBi = ST1i - ST3r; + + var SA = outOff + quarterLen - i; + var SB = outOff + halfLen - i; + + out[SA] = SFAr; + out[SA + 1] = SFAi; + out[SB] = SFBr; + out[SB + 1] = SFBi; } - - this._out = null; - this._data = null; - this._inv = 0; + } } - fromComplexArray(complex, storage) { - var res = storage || new Array(complex.length >>> 1); - for (var i = 0; i < complex.length; i += 2) - res[i >>> 1] = complex[i]; - return res; - } - createComplexArray() { - const res = new Array(this._csize); - for (var i = 0; i < res.length; i++) - res[i] = 0; - return res; - } - toComplexArray(input, storage) { - var res = storage || this.createComplexArray(); - for (var i = 0; i < res.length; i += 2) { - res[i] = input[i >>> 1]; - res[i + 1] = 0; - } - return res; - } - completeSpectrum(spectrum) { - var size = this._csize; - var half = size >>> 1; - for (var i = 2; i < half; i += 2) { - spectrum[size - i] = spectrum[i]; - spectrum[size - i + 1] = -spectrum[i + 1]; - } - } - transform(out, data) { - if (out === data) - throw new Error('Input and output buffers must be different'); + } + // radix-2 implementation + // + // NOTE: Only called for len=4 + _singleRealTransform2(outOff, off, step) { + const out = this._out; + const data = this._data; - this._out = out; - this._data = data; - this._inv = 0; - this._transform4(); - this._out = null; - this._data = null; - } - realTransform(out, data) { - if (out === data) - throw new Error('Input and output buffers must be different'); + const evenR = data[off]; + const oddR = data[off + step]; - this._out = out; - this._data = data; - this._inv = 0; - this._realTransform4(); - this._out = null; - this._data = null; - } - inverseTransform(out, data) { - if (out === data) - throw new Error('Input and output buffers must be different'); + const leftR = evenR + oddR; + const rightR = evenR - oddR; - this._out = out; - this._data = data; - this._inv = 1; - this._transform4(); - for (var i = 0; i < out.length; i++) - out[i] /= this.size; - this._out = null; - this._data = null; - } - // radix-4 implementation - // - // NOTE: Uses of `var` are intentional for older V8 version that do not - // support both `let compound assignments` and `const phi` - _transform4() { - var out = this._out; - var size = this._csize; + out[outOff] = leftR; + out[outOff + 1] = 0; + out[outOff + 2] = rightR; + out[outOff + 3] = 0; + } + // radix-4 + // + // NOTE: Only called for len=8 + _singleRealTransform4(outOff, off, step) { + const out = this._out; + const data = this._data; + const inv = this._inv ? -1 : 1; + const step2 = step * 2; + const step3 = step * 3; - // Initial step (permute and transform) - var width = this._width; - var step = 1 << width; - var len = (size / step) << 1; + // Original values + const Ar = data[off]; + const Br = data[off + step]; + const Cr = data[off + step2]; + const Dr = data[off + step3]; - var outOff; - var t; - var bitrev = this._bitrev; - if (len === 4) { - for (outOff = 0, t = 0; outOff < size; outOff += len, t++) { - const off = bitrev[t]; - this._singleTransform2(outOff, off, step); - } - } else { - // len === 8 - for (outOff = 0, t = 0; outOff < size; outOff += len, t++) { - const off = bitrev[t]; - this._singleTransform4(outOff, off, step); - } - } + // Pre-Final values + const T0r = Ar + Cr; + const T1r = Ar - Cr; + const T2r = Br + Dr; + const T3r = inv * (Br - Dr); - // Loop through steps in decreasing order - var inv = this._inv ? -1 : 1; - var table = this.table; - for (step >>= 2; step >= 2; step >>= 2) { - len = (size / step) << 1; - var quarterLen = len >>> 2; + // Final values + const FAr = T0r + T2r; - // Loop through offsets in the data - for (outOff = 0; outOff < size; outOff += len) { - // Full case - var limit = outOff + quarterLen; - for (var i = outOff, k = 0; i < limit; i += 2, k += step) { - const A = i; - const B = A + quarterLen; - const C = B + quarterLen; - const D = C + quarterLen; + const FBr = T1r; + const FBi = -T3r; - // Original values - const Ar = out[A]; - const Ai = out[A + 1]; - const Br = out[B]; - const Bi = out[B + 1]; - const Cr = out[C]; - const Ci = out[C + 1]; - const Dr = out[D]; - const Di = out[D + 1]; + const FCr = T0r - T2r; - // Middle values - const MAr = Ar; - const MAi = Ai; + const FDr = T1r; + const FDi = T3r; - const tableBr = table[k]; - const tableBi = inv * table[k + 1]; - const MBr = Br * tableBr - Bi * tableBi; - const MBi = Br * tableBi + Bi * tableBr; - - const tableCr = table[2 * k]; - const tableCi = inv * table[2 * k + 1]; - const MCr = Cr * tableCr - Ci * tableCi; - const MCi = Cr * tableCi + Ci * tableCr; - - const tableDr = table[3 * k]; - const tableDi = inv * table[3 * k + 1]; - const MDr = Dr * tableDr - Di * tableDi; - const MDi = Dr * tableDi + Di * tableDr; - - // Pre-Final values - const T0r = MAr + MCr; - const T0i = MAi + MCi; - const T1r = MAr - MCr; - const T1i = MAi - MCi; - const T2r = MBr + MDr; - const T2i = MBi + MDi; - const T3r = inv * (MBr - MDr); - const T3i = inv * (MBi - MDi); - - // Final values - const FAr = T0r + T2r; - const FAi = T0i + T2i; - - const FCr = T0r - T2r; - const FCi = T0i - T2i; - - const FBr = T1r + T3i; - const FBi = T1i - T3r; - - const FDr = T1r - T3i; - const FDi = T1i + T3r; - - out[A] = FAr; - out[A + 1] = FAi; - out[B] = FBr; - out[B + 1] = FBi; - out[C] = FCr; - out[C + 1] = FCi; - out[D] = FDr; - out[D + 1] = FDi; - } - } - } - } - // radix-2 implementation - // - // NOTE: Only called for len=4 - _singleTransform2(outOff, off, - step) { - const out = this._out; - const data = this._data; - - const evenR = data[off]; - const evenI = data[off + 1]; - const oddR = data[off + step]; - const oddI = data[off + step + 1]; - - const leftR = evenR + oddR; - const leftI = evenI + oddI; - const rightR = evenR - oddR; - const rightI = evenI - oddI; - - out[outOff] = leftR; - out[outOff + 1] = leftI; - out[outOff + 2] = rightR; - out[outOff + 3] = rightI; - } - // radix-4 - // - // NOTE: Only called for len=8 - _singleTransform4(outOff, off, - step) { - const out = this._out; - const data = this._data; - const inv = this._inv ? -1 : 1; - const step2 = step * 2; - const step3 = step * 3; - - // Original values - const Ar = data[off]; - const Ai = data[off + 1]; - const Br = data[off + step]; - const Bi = data[off + step + 1]; - const Cr = data[off + step2]; - const Ci = data[off + step2 + 1]; - const Dr = data[off + step3]; - const Di = data[off + step3 + 1]; - - // Pre-Final values - const T0r = Ar + Cr; - const T0i = Ai + Ci; - const T1r = Ar - Cr; - const T1i = Ai - Ci; - const T2r = Br + Dr; - const T2i = Bi + Di; - const T3r = inv * (Br - Dr); - const T3i = inv * (Bi - Di); - - // Final values - const FAr = T0r + T2r; - const FAi = T0i + T2i; - - const FBr = T1r + T3i; - const FBi = T1i - T3r; - - const FCr = T0r - T2r; - const FCi = T0i - T2i; - - const FDr = T1r - T3i; - const FDi = T1i + T3r; - - out[outOff] = FAr; - out[outOff + 1] = FAi; - out[outOff + 2] = FBr; - out[outOff + 3] = FBi; - out[outOff + 4] = FCr; - out[outOff + 5] = FCi; - out[outOff + 6] = FDr; - out[outOff + 7] = FDi; - } - // Real input radix-4 implementation - _realTransform4() { - var out = this._out; - var size = this._csize; - - // Initial step (permute and transform) - var width = this._width; - var step = 1 << width; - var len = (size / step) << 1; - - var outOff; - var t; - var bitrev = this._bitrev; - if (len === 4) { - for (outOff = 0, t = 0; outOff < size; outOff += len, t++) { - const off = bitrev[t]; - this._singleRealTransform2(outOff, off >>> 1, step >>> 1); - } - } else { - // len === 8 - for (outOff = 0, t = 0; outOff < size; outOff += len, t++) { - const off = bitrev[t]; - this._singleRealTransform4(outOff, off >>> 1, step >>> 1); - } - } - - // Loop through steps in decreasing order - var inv = this._inv ? -1 : 1; - var table = this.table; - for (step >>= 2; step >= 2; step >>= 2) { - len = (size / step) << 1; - var halfLen = len >>> 1; - var quarterLen = halfLen >>> 1; - var hquarterLen = quarterLen >>> 1; - - // Loop through offsets in the data - for (outOff = 0; outOff < size; outOff += len) { - for (var i = 0, k = 0; i <= hquarterLen; i += 2, k += step) { - var A = outOff + i; - var B = A + quarterLen; - var C = B + quarterLen; - var D = C + quarterLen; - - // Original values - var Ar = out[A]; - var Ai = out[A + 1]; - var Br = out[B]; - var Bi = out[B + 1]; - var Cr = out[C]; - var Ci = out[C + 1]; - var Dr = out[D]; - var Di = out[D + 1]; - - // Middle values - var MAr = Ar; - var MAi = Ai; - - var tableBr = table[k]; - var tableBi = inv * table[k + 1]; - var MBr = Br * tableBr - Bi * tableBi; - var MBi = Br * tableBi + Bi * tableBr; - - var tableCr = table[2 * k]; - var tableCi = inv * table[2 * k + 1]; - var MCr = Cr * tableCr - Ci * tableCi; - var MCi = Cr * tableCi + Ci * tableCr; - - var tableDr = table[3 * k]; - var tableDi = inv * table[3 * k + 1]; - var MDr = Dr * tableDr - Di * tableDi; - var MDi = Dr * tableDi + Di * tableDr; - - // Pre-Final values - var T0r = MAr + MCr; - var T0i = MAi + MCi; - var T1r = MAr - MCr; - var T1i = MAi - MCi; - var T2r = MBr + MDr; - var T2i = MBi + MDi; - var T3r = inv * (MBr - MDr); - var T3i = inv * (MBi - MDi); - - // Final values - var FAr = T0r + T2r; - var FAi = T0i + T2i; - - var FBr = T1r + T3i; - var FBi = T1i - T3r; - - out[A] = FAr; - out[A + 1] = FAi; - out[B] = FBr; - out[B + 1] = FBi; - - // Output final middle point - if (i === 0) { - var FCr = T0r - T2r; - var FCi = T0i - T2i; - out[C] = FCr; - out[C + 1] = FCi; - continue; - } - - // Do not overwrite ourselves - if (i === hquarterLen) - continue; - - // In the flipped case: - // MAi = -MAi - // MBr=-MBi, MBi=-MBr - // MCr=-MCr - // MDr=MDi, MDi=MDr - var ST0r = T1r; - var ST0i = -T1i; - var ST1r = T0r; - var ST1i = -T0i; - var ST2r = -inv * T3i; - var ST2i = -inv * T3r; - var ST3r = -inv * T2i; - var ST3i = -inv * T2r; - - var SFAr = ST0r + ST2r; - var SFAi = ST0i + ST2i; - - var SFBr = ST1r + ST3i; - var SFBi = ST1i - ST3r; - - var SA = outOff + quarterLen - i; - var SB = outOff + halfLen - i; - - out[SA] = SFAr; - out[SA + 1] = SFAi; - out[SB] = SFBr; - out[SB + 1] = SFBi; - } - } - } - } - // radix-2 implementation - // - // NOTE: Only called for len=4 - _singleRealTransform2(outOff, - off, - step) { - const out = this._out; - const data = this._data; - - const evenR = data[off]; - const oddR = data[off + step]; - - const leftR = evenR + oddR; - const rightR = evenR - oddR; - - out[outOff] = leftR; - out[outOff + 1] = 0; - out[outOff + 2] = rightR; - out[outOff + 3] = 0; - } - // radix-4 - // - // NOTE: Only called for len=8 - _singleRealTransform4(outOff, - off, - step) { - const out = this._out; - const data = this._data; - const inv = this._inv ? -1 : 1; - const step2 = step * 2; - const step3 = step * 3; - - // Original values - const Ar = data[off]; - const Br = data[off + step]; - const Cr = data[off + step2]; - const Dr = data[off + step3]; - - // Pre-Final values - const T0r = Ar + Cr; - const T1r = Ar - Cr; - const T2r = Br + Dr; - const T3r = inv * (Br - Dr); - - // Final values - const FAr = T0r + T2r; - - const FBr = T1r; - const FBi = -T3r; - - const FCr = T0r - T2r; - - const FDr = T1r; - const FDi = T3r; - - out[outOff] = FAr; - out[outOff + 1] = 0; - out[outOff + 2] = FBr; - out[outOff + 3] = FBi; - out[outOff + 4] = FCr; - out[outOff + 5] = 0; - out[outOff + 6] = FDr; - out[outOff + 7] = FDi; - } + out[outOff] = FAr; + out[outOff + 1] = 0; + out[outOff + 2] = FBr; + out[outOff + 3] = FBi; + out[outOff + 4] = FCr; + out[outOff + 5] = 0; + out[outOff + 6] = FDr; + out[outOff + 7] = FDi; + } } - - - - - - - - - - - - - - diff --git a/packages/superdough/ola-processor.js b/packages/superdough/ola-processor.js index ae7deca2..38d45a25 100644 --- a/packages/superdough/ola-processor.js +++ b/packages/superdough/ola-processor.js @@ -1,185 +1,185 @@ -"use strict"; +'use strict'; // sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file const WEBAUDIO_BLOCK_SIZE = 128; /** Overlap-Add Node */ class OLAProcessor extends AudioWorkletProcessor { - constructor(options) { - super(options); - this.started = false; - this.nbInputs = options.numberOfInputs; - this.nbOutputs = options.numberOfOutputs; + constructor(options) { + super(options); + this.started = false; + this.nbInputs = options.numberOfInputs; + this.nbOutputs = options.numberOfOutputs; - this.blockSize = options.processorOptions.blockSize; - // TODO for now, the only support hop size is the size of a web audio block - this.hopSize = WEBAUDIO_BLOCK_SIZE; + this.blockSize = options.processorOptions.blockSize; + // TODO for now, the only support hop size is the size of a web audio block + this.hopSize = WEBAUDIO_BLOCK_SIZE; - this.nbOverlaps = this.blockSize / this.hopSize; + this.nbOverlaps = this.blockSize / this.hopSize; - // pre-allocate input buffers (will be reallocated if needed) - this.inputBuffers = new Array(this.nbInputs); - this.inputBuffersHead = new Array(this.nbInputs); - this.inputBuffersToSend = new Array(this.nbInputs); - // default to 1 channel per input until we know more - for (let i = 0; i < this.nbInputs; i++) { - this.allocateInputChannels(i, 1); - } - // pre-allocate input buffers (will be reallocated if needed) - this.outputBuffers = new Array(this.nbOutputs); - this.outputBuffersToRetrieve = new Array(this.nbOutputs); - // default to 1 channel per output until we know more - for (let i = 0; i < this.nbOutputs; i++) { - this.allocateOutputChannels(i, 1); - } + // pre-allocate input buffers (will be reallocated if needed) + this.inputBuffers = new Array(this.nbInputs); + this.inputBuffersHead = new Array(this.nbInputs); + this.inputBuffersToSend = new Array(this.nbInputs); + // default to 1 channel per input until we know more + for (let i = 0; i < this.nbInputs; i++) { + this.allocateInputChannels(i, 1); } + // pre-allocate input buffers (will be reallocated if needed) + this.outputBuffers = new Array(this.nbOutputs); + this.outputBuffersToRetrieve = new Array(this.nbOutputs); + // default to 1 channel per output until we know more + for (let i = 0; i < this.nbOutputs; i++) { + this.allocateOutputChannels(i, 1); + } + } - /** Handles dynamic reallocation of input/output channels buffer + /** Handles dynamic reallocation of input/output channels buffer (channel numbers may lety during lifecycle) **/ - reallocateChannelsIfNeeded(inputs, outputs) { - for (let i = 0; i < this.nbInputs; i++) { - let nbChannels = inputs[i].length; - if (nbChannels != this.inputBuffers[i].length) { - this.allocateInputChannels(i, nbChannels); - } - } - - for (let i = 0; i < this.nbOutputs; i++) { - let nbChannels = outputs[i].length; - if (nbChannels != this.outputBuffers[i].length) { - this.allocateOutputChannels(i, nbChannels); - } - } + reallocateChannelsIfNeeded(inputs, outputs) { + for (let i = 0; i < this.nbInputs; i++) { + let nbChannels = inputs[i].length; + if (nbChannels != this.inputBuffers[i].length) { + this.allocateInputChannels(i, nbChannels); + } } - allocateInputChannels(inputIndex, nbChannels) { - // allocate input buffers + for (let i = 0; i < this.nbOutputs; i++) { + let nbChannels = outputs[i].length; + if (nbChannels != this.outputBuffers[i].length) { + this.allocateOutputChannels(i, nbChannels); + } + } + } - this.inputBuffers[inputIndex] = new Array(nbChannels); - for (let i = 0; i < nbChannels; i++) { - this.inputBuffers[inputIndex][i] = new Float32Array(this.blockSize + WEBAUDIO_BLOCK_SIZE); - this.inputBuffers[inputIndex][i].fill(0); - } + allocateInputChannels(inputIndex, nbChannels) { + // allocate input buffers - // allocate input buffers to send and head pointers to copy from - // (cannot directly send a pointer/subarray because input may be modified) - this.inputBuffersHead[inputIndex] = new Array(nbChannels); - this.inputBuffersToSend[inputIndex] = new Array(nbChannels); - for (let i = 0; i < nbChannels; i++) { - this.inputBuffersHead[inputIndex][i] = this.inputBuffers[inputIndex][i] .subarray(0, this.blockSize); - this.inputBuffersToSend[inputIndex][i] = new Float32Array(this.blockSize); - } + this.inputBuffers[inputIndex] = new Array(nbChannels); + for (let i = 0; i < nbChannels; i++) { + this.inputBuffers[inputIndex][i] = new Float32Array(this.blockSize + WEBAUDIO_BLOCK_SIZE); + this.inputBuffers[inputIndex][i].fill(0); } - allocateOutputChannels(outputIndex, nbChannels) { - // allocate output buffers - this.outputBuffers[outputIndex] = new Array(nbChannels); - for (let i = 0; i < nbChannels; i++) { - this.outputBuffers[outputIndex][i] = new Float32Array(this.blockSize); - this.outputBuffers[outputIndex][i].fill(0); - } + // allocate input buffers to send and head pointers to copy from + // (cannot directly send a pointer/subarray because input may be modified) + this.inputBuffersHead[inputIndex] = new Array(nbChannels); + this.inputBuffersToSend[inputIndex] = new Array(nbChannels); + for (let i = 0; i < nbChannels; i++) { + this.inputBuffersHead[inputIndex][i] = this.inputBuffers[inputIndex][i].subarray(0, this.blockSize); + this.inputBuffersToSend[inputIndex][i] = new Float32Array(this.blockSize); + } + } - // allocate output buffers to retrieve - // (cannot send a pointer/subarray because new output has to be add to exising output) - this.outputBuffersToRetrieve[outputIndex] = new Array(nbChannels); - for (let i = 0; i < nbChannels; i++) { - this.outputBuffersToRetrieve[outputIndex][i] = new Float32Array(this.blockSize); - this.outputBuffersToRetrieve[outputIndex][i].fill(0); - } + allocateOutputChannels(outputIndex, nbChannels) { + // allocate output buffers + this.outputBuffers[outputIndex] = new Array(nbChannels); + for (let i = 0; i < nbChannels; i++) { + this.outputBuffers[outputIndex][i] = new Float32Array(this.blockSize); + this.outputBuffers[outputIndex][i].fill(0); } - /** Read next web audio block to input buffers **/ - readInputs(inputs) { - // when playback is paused, we may stop receiving new samples - if (inputs[0].length && inputs[0][0].length == 0) { - for (let i = 0; i < this.nbInputs; i++) { - for (let j = 0; j < this.inputBuffers[i].length; j++) { - this.inputBuffers[i][j].fill(0, this.blockSize); - } - } - return; - } + // allocate output buffers to retrieve + // (cannot send a pointer/subarray because new output has to be add to exising output) + this.outputBuffersToRetrieve[outputIndex] = new Array(nbChannels); + for (let i = 0; i < nbChannels; i++) { + this.outputBuffersToRetrieve[outputIndex][i] = new Float32Array(this.blockSize); + this.outputBuffersToRetrieve[outputIndex][i].fill(0); + } + } - for (let i = 0; i < this.nbInputs; i++) { - for (let j = 0; j < this.inputBuffers[i].length; j++) { - let webAudioBlock = inputs[i][j]; - this.inputBuffers[i][j].set(webAudioBlock, this.blockSize); - } + /** Read next web audio block to input buffers **/ + readInputs(inputs) { + // when playback is paused, we may stop receiving new samples + if (inputs[0].length && inputs[0][0].length == 0) { + for (let i = 0; i < this.nbInputs; i++) { + for (let j = 0; j < this.inputBuffers[i].length; j++) { + this.inputBuffers[i][j].fill(0, this.blockSize); } + } + return; } - /** Write next web audio block from output buffers **/ - writeOutputs(outputs) { - for (let i = 0; i < this.nbInputs; i++) { - for (let j = 0; j < this.inputBuffers[i].length; j++) { - let webAudioBlock = this.outputBuffers[i][j].subarray(0, WEBAUDIO_BLOCK_SIZE); - outputs[i][j].set(webAudioBlock); - } + for (let i = 0; i < this.nbInputs; i++) { + for (let j = 0; j < this.inputBuffers[i].length; j++) { + let webAudioBlock = inputs[i][j]; + this.inputBuffers[i][j].set(webAudioBlock, this.blockSize); + } + } + } + + /** Write next web audio block from output buffers **/ + writeOutputs(outputs) { + for (let i = 0; i < this.nbInputs; i++) { + for (let j = 0; j < this.inputBuffers[i].length; j++) { + let webAudioBlock = this.outputBuffers[i][j].subarray(0, WEBAUDIO_BLOCK_SIZE); + outputs[i][j].set(webAudioBlock); + } + } + } + + /** Shift left content of input buffers to receive new web audio block **/ + shiftInputBuffers() { + for (let i = 0; i < this.nbInputs; i++) { + for (let j = 0; j < this.inputBuffers[i].length; j++) { + this.inputBuffers[i][j].copyWithin(0, WEBAUDIO_BLOCK_SIZE); + } + } + } + + /** Shift left content of output buffers to receive new web audio block **/ + shiftOutputBuffers() { + for (let i = 0; i < this.nbOutputs; i++) { + for (let j = 0; j < this.outputBuffers[i].length; j++) { + this.outputBuffers[i][j].copyWithin(0, WEBAUDIO_BLOCK_SIZE); + this.outputBuffers[i][j].subarray(this.blockSize - WEBAUDIO_BLOCK_SIZE).fill(0); + } + } + } + + /** Copy contents of input buffers to buffer actually sent to process **/ + prepareInputBuffersToSend() { + for (let i = 0; i < this.nbInputs; i++) { + for (let j = 0; j < this.inputBuffers[i].length; j++) { + this.inputBuffersToSend[i][j].set(this.inputBuffersHead[i][j]); + } + } + } + + /** Add contents of output buffers just processed to output buffers **/ + handleOutputBuffersToRetrieve() { + for (let i = 0; i < this.nbOutputs; i++) { + for (let j = 0; j < this.outputBuffers[i].length; j++) { + for (let k = 0; k < this.blockSize; k++) { + this.outputBuffers[i][j][k] += this.outputBuffersToRetrieve[i][j][k] / this.nbOverlaps; } + } } + } - /** Shift left content of input buffers to receive new web audio block **/ - shiftInputBuffers() { - for (let i = 0; i < this.nbInputs; i++) { - for (let j = 0; j < this.inputBuffers[i].length; j++) { - this.inputBuffers[i][j].copyWithin(0, WEBAUDIO_BLOCK_SIZE); - } - } + process(inputs, outputs, params) { + const input = inputs[0]; + const hasInput = !(input[0] === undefined); + if (this.started && !hasInput) { + return false; } + this.started = hasInput; + this.reallocateChannelsIfNeeded(inputs, outputs); - /** Shift left content of output buffers to receive new web audio block **/ - shiftOutputBuffers() { - for (let i = 0; i < this.nbOutputs; i++) { - for (let j = 0; j < this.outputBuffers[i].length; j++) { - this.outputBuffers[i][j].copyWithin(0, WEBAUDIO_BLOCK_SIZE); - this.outputBuffers[i][j].subarray(this.blockSize - WEBAUDIO_BLOCK_SIZE).fill(0); - } - } - } + this.readInputs(inputs); + this.shiftInputBuffers(); + this.prepareInputBuffersToSend(); + this.processOLA(this.inputBuffersToSend, this.outputBuffersToRetrieve, params); + this.handleOutputBuffersToRetrieve(); + this.writeOutputs(outputs); + this.shiftOutputBuffers(); - /** Copy contents of input buffers to buffer actually sent to process **/ - prepareInputBuffersToSend() { - for (let i = 0; i < this.nbInputs; i++) { - for (let j = 0; j < this.inputBuffers[i].length; j++) { - this.inputBuffersToSend[i][j].set(this.inputBuffersHead[i][j]); - } - } - } + return true; + } - /** Add contents of output buffers just processed to output buffers **/ - handleOutputBuffersToRetrieve() { - for (let i = 0; i < this.nbOutputs; i++) { - for (let j = 0; j < this.outputBuffers[i].length; j++) { - for (let k = 0; k < this.blockSize; k++) { - this.outputBuffers[i][j][k] += this.outputBuffersToRetrieve[i][j][k] / this.nbOverlaps; - } - } - } - } - - process(inputs, outputs, params) { - const input = inputs[0]; - const hasInput = !(input[0] === undefined); - if (this.started && !hasInput) { - return false; - } - this.started = hasInput; - this.reallocateChannelsIfNeeded(inputs, outputs); - - this.readInputs(inputs); - this.shiftInputBuffers(); - this.prepareInputBuffersToSend() - this.processOLA(this.inputBuffersToSend, this.outputBuffersToRetrieve, params); - this.handleOutputBuffersToRetrieve(); - this.writeOutputs(outputs); - this.shiftOutputBuffers(); - - return true; - } - - processOLA(inputs, outputs, params) { - console.assert(false, "Not overriden"); - } + processOLA(inputs, outputs, params) { + console.assert(false, 'Not overriden'); + } } -export default OLAProcessor; \ No newline at end of file +export default OLAProcessor; diff --git a/packages/superdough/package.json b/packages/superdough/package.json index 11cd562b..685ab9c5 100644 --- a/packages/superdough/package.json +++ b/packages/superdough/package.json @@ -35,7 +35,6 @@ "vite": "^5.0.10" }, "dependencies": { - "fft.js": "^4.0.4", "nanostores": "^0.9.5" } } diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 46c56e56..01569788 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -8,7 +8,7 @@ import './feedbackdelay.mjs'; import './reverb.mjs'; import './vowel.mjs'; import { clamp, nanFallback, _mod } from './util.mjs'; -import workletsUrl from './worklets.mjs?url'; +import workletsUrl from './worklets.mjs?worker&url'; import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs'; import { map } from 'nanostores'; import { logger } from './logger.mjs'; @@ -309,6 +309,13 @@ export function resetGlobalEffects() { } export const superdough = async (value, t, hapDuration) => { + t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t; + let { stretch } = value; + if (stretch != null) { + //account for phase vocoder latency + const latency = 0.04; + t = t - latency; + } const ac = getAudioContext(); if (typeof value !== 'object') { throw new Error( @@ -320,7 +327,7 @@ export const superdough = async (value, t, hapDuration) => { // duration is passed as value too.. value.duration = hapDuration; // calculate absolute time - t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t; + if (t < ac.currentTime) { console.warn( `[superdough]: cannot schedule sounds in the past (target: ${t.toFixed(2)}, now: ${ac.currentTime.toFixed(2)})`, @@ -371,7 +378,6 @@ export const superdough = async (value, t, hapDuration) => { // coarse, crush, - stretch, shape, shapevol = getDefaultValue('shapevol'), distort, @@ -441,7 +447,6 @@ export const superdough = async (value, t, hapDuration) => { chain.push(sourceNode); stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch })); - // gain stage chain.push(gainNode(gain)); @@ -587,4 +592,6 @@ export const superdough = async (value, t, hapDuration) => { toDisconnect = chain.concat([delaySend, reverbSend, analyserSend]); }; -export const superdoughTrigger = (t, hap, ct, cps) => superdough(hap, t - ct, hap.duration / cps, cps); +export const superdoughTrigger = (t, hap, ct, cps) => { + superdough(hap, t - ct, hap.duration / cps, cps); +}; diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 61078634..a2b7828c 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -2,8 +2,8 @@ // LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE // TOFIX: THIS FILE DOES NOT SUPPORT IMPORTS ON DEPOLYMENT -import OLAProcessor from "./ola-processor" -import FFT from './fft.js'; +import OLAProcessor from './ola-processor'; +import FFT from './fft.js'; const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const _mod = (n, m) => ((n % m) + m) % m; @@ -469,181 +469,182 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor); - - - - +// Phase Vocoder sourced from // sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file const BUFFERED_BLOCK_SIZE = 2048; function genHannWindow(length) { - let win = new Float32Array(length); - for (var i = 0; i < length; i++) { - win[i] = 0.5 * (1 - Math.cos(2 * Math.PI * i / length)); - } - return win; + let win = new Float32Array(length); + for (var i = 0; i < length; i++) { + win[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / length)); + } + return win; } class PhaseVocoderProcessor extends OLAProcessor { - static get parameterDescriptors() { - return [{ - name: 'pitchFactor', - defaultValue: 1.0 - }]; + static get parameterDescriptors() { + return [ + { + name: 'pitchFactor', + defaultValue: 1.0, + }, + ]; + } + + constructor(options) { + options.processorOptions = { + blockSize: BUFFERED_BLOCK_SIZE, + }; + super(options); + + this.fftSize = this.blockSize; + this.timeCursor = 0; + + this.hannWindow = genHannWindow(this.blockSize); + // prepare FFT and pre-allocate buffers + this.fft = new FFT(this.fftSize); + this.freqComplexBuffer = this.fft.createComplexArray(); + this.freqComplexBufferShifted = this.fft.createComplexArray(); + this.timeComplexBuffer = this.fft.createComplexArray(); + this.magnitudes = new Float32Array(this.fftSize / 2 + 1); + this.peakIndexes = new Int32Array(this.magnitudes.length); + this.nbPeaks = 0; + } + + processOLA(inputs, outputs, parameters) { + // no automation, take last value + + let pitchFactor = parameters.pitchFactor[parameters.pitchFactor.length - 1]; + + if (pitchFactor < 0) { + pitchFactor = pitchFactor * 0.25; + } + pitchFactor = Math.max(0, pitchFactor + 1); + + for (var i = 0; i < this.nbInputs; i++) { + for (var j = 0; j < inputs[i].length; j++) { + // big assumption here: output is symetric to input + var input = inputs[i][j]; + var output = outputs[i][j]; + + this.applyHannWindow(input); + + this.fft.realTransform(this.freqComplexBuffer, input); + + this.computeMagnitudes(); + this.findPeaks(); + this.shiftPeaks(pitchFactor); + + this.fft.completeSpectrum(this.freqComplexBufferShifted); + this.fft.inverseTransform(this.timeComplexBuffer, this.freqComplexBufferShifted); + this.fft.fromComplexArray(this.timeComplexBuffer, output); + this.applyHannWindow(output); + } } - constructor(options) { - options.processorOptions = { - blockSize: BUFFERED_BLOCK_SIZE, - }; - super(options); + this.timeCursor += this.hopSize; + } - this.fftSize = this.blockSize; - this.timeCursor = 0; - - this.hannWindow = genHannWindow(this.blockSize); - // prepare FFT and pre-allocate buffers - this.fft = new FFT(this.fftSize); - this.freqComplexBuffer = this.fft.createComplexArray(); - this.freqComplexBufferShifted = this.fft.createComplexArray(); - this.timeComplexBuffer = this.fft.createComplexArray(); - this.magnitudes = new Float32Array(this.fftSize / 2 + 1); - this.peakIndexes = new Int32Array(this.magnitudes.length); - this.nbPeaks = 0; + /** Apply Hann window in-place */ + applyHannWindow(input) { + for (var i = 0; i < this.blockSize; i++) { + input[i] = input[i] * this.hannWindow[i] * 1.62; } + } - processOLA(inputs, outputs, parameters) { - // no automation, take last value - const pitchFactor = parameters.pitchFactor[parameters.pitchFactor.length - 1]; + /** Compute squared magnitudes for peak finding **/ + computeMagnitudes() { + var i = 0, + j = 0; + while (i < this.magnitudes.length) { + let real = this.freqComplexBuffer[j]; + let imag = this.freqComplexBuffer[j + 1]; + // no need to sqrt for peak finding + this.magnitudes[i] = real ** 2 + imag ** 2; + i += 1; + j += 2; + } + } - for (var i = 0; i < this.nbInputs; i++) { - for (var j = 0; j < inputs[i].length; j++) { - // big assumption here: output is symetric to input - var input = inputs[i][j]; - var output = outputs[i][j]; + /** Find peaks in spectrum magnitudes **/ + findPeaks() { + this.nbPeaks = 0; + var i = 2; + let end = this.magnitudes.length - 2; - this.applyHannWindow(input); + while (i < end) { + let mag = this.magnitudes[i]; - this.fft.realTransform(this.freqComplexBuffer, input); + if (this.magnitudes[i - 1] >= mag || this.magnitudes[i - 2] >= mag) { + i++; + continue; + } + if (this.magnitudes[i + 1] >= mag || this.magnitudes[i + 2] >= mag) { + i++; + continue; + } - this.computeMagnitudes(); - this.findPeaks(); - this.shiftPeaks(pitchFactor); + this.peakIndexes[this.nbPeaks] = i; + this.nbPeaks++; + i += 2; + } + } - this.fft.completeSpectrum(this.freqComplexBufferShifted); - this.fft.inverseTransform(this.timeComplexBuffer, this.freqComplexBufferShifted); - this.fft.fromComplexArray(this.timeComplexBuffer, output); + /** Shift peaks and regions of influence by pitchFactor into new specturm */ + shiftPeaks(pitchFactor) { + // zero-fill new spectrum + this.freqComplexBufferShifted.fill(0); - this.applyHannWindow(output); - } + for (var i = 0; i < this.nbPeaks; i++) { + let peakIndex = this.peakIndexes[i]; + let peakIndexShifted = Math.round(peakIndex * pitchFactor); + + if (peakIndexShifted > this.magnitudes.length) { + break; + } + + // find region of influence + var startIndex = 0; + var endIndex = this.fftSize; + if (i > 0) { + let peakIndexBefore = this.peakIndexes[i - 1]; + startIndex = peakIndex - Math.floor((peakIndex - peakIndexBefore) / 2); + } + if (i < this.nbPeaks - 1) { + let peakIndexAfter = this.peakIndexes[i + 1]; + endIndex = peakIndex + Math.ceil((peakIndexAfter - peakIndex) / 2); + } + + // shift whole region of influence around peak to shifted peak + let startOffset = startIndex - peakIndex; + let endOffset = endIndex - peakIndex; + for (var j = startOffset; j < endOffset; j++) { + let binIndex = peakIndex + j; + let binIndexShifted = peakIndexShifted + j; + + if (binIndexShifted >= this.magnitudes.length) { + break; } - this.timeCursor += this.hopSize; - } - - /** Apply Hann window in-place */ - applyHannWindow(input) { - for (var i = 0; i < this.blockSize; i++) { - input[i] = input[i] * this.hannWindow[i]; - } - } - - /** Compute squared magnitudes for peak finding **/ - computeMagnitudes() { - var i = 0, j = 0; - while (i < this.magnitudes.length) { - let real = this.freqComplexBuffer[j]; - let imag = this.freqComplexBuffer[j + 1]; - // no need to sqrt for peak finding - this.magnitudes[i] = real ** 2 + imag ** 2; - i+=1; - j+=2; - } - } - - /** Find peaks in spectrum magnitudes **/ - findPeaks() { - this.nbPeaks = 0; - var i = 2; - let end = this.magnitudes.length - 2; - - while (i < end) { - let mag = this.magnitudes[i]; - - if (this.magnitudes[i - 1] >= mag || this.magnitudes[i - 2] >= mag) { - i++; - continue; - } - if (this.magnitudes[i + 1] >= mag || this.magnitudes[i + 2] >= mag) { - i++; - continue; - } - - this.peakIndexes[this.nbPeaks] = i; - this.nbPeaks++; - i += 2; - } - } - - /** Shift peaks and regions of influence by pitchFactor into new specturm */ - shiftPeaks(pitchFactor) { - // zero-fill new spectrum - this.freqComplexBufferShifted.fill(0); - - for (var i = 0; i < this.nbPeaks; i++) { - let peakIndex = this.peakIndexes[i]; - let peakIndexShifted = Math.round(peakIndex * pitchFactor); - - if (peakIndexShifted > this.magnitudes.length) { - break; - } - - // find region of influence - var startIndex = 0; - var endIndex = this.fftSize; - if (i > 0) { - let peakIndexBefore = this.peakIndexes[i - 1]; - startIndex = peakIndex - Math.floor((peakIndex - peakIndexBefore) / 2); - } - if (i < this.nbPeaks - 1) { - let peakIndexAfter = this.peakIndexes[i + 1]; - endIndex = peakIndex + Math.ceil((peakIndexAfter - peakIndex) / 2); - } - - // shift whole region of influence around peak to shifted peak - let startOffset = startIndex - peakIndex; - let endOffset = endIndex - peakIndex; - for (var j = startOffset; j < endOffset; j++) { - let binIndex = peakIndex + j; - let binIndexShifted = peakIndexShifted + j; - - if (binIndexShifted >= this.magnitudes.length) { - break; - } - - // apply phase correction - let omegaDelta = 2 * Math.PI * (binIndexShifted - binIndex) / this.fftSize; - let phaseShiftReal = Math.cos(omegaDelta * this.timeCursor); - let phaseShiftImag = Math.sin(omegaDelta * this.timeCursor); - - let indexReal = binIndex * 2; - let indexImag = indexReal + 1; - let valueReal = this.freqComplexBuffer[indexReal]; - let valueImag = this.freqComplexBuffer[indexImag]; - - let valueShiftedReal = valueReal * phaseShiftReal - valueImag * phaseShiftImag; - let valueShiftedImag = valueReal * phaseShiftImag + valueImag * phaseShiftReal; - - let indexShiftedReal = binIndexShifted * 2; - let indexShiftedImag = indexShiftedReal + 1; - this.freqComplexBufferShifted[indexShiftedReal] += valueShiftedReal; - this.freqComplexBufferShifted[indexShiftedImag] += valueShiftedImag; - } - } + // apply phase correction + let omegaDelta = (2 * Math.PI * (binIndexShifted - binIndex)) / this.fftSize; + let phaseShiftReal = Math.cos(omegaDelta * this.timeCursor); + let phaseShiftImag = Math.sin(omegaDelta * this.timeCursor); + + let indexReal = binIndex * 2; + let indexImag = indexReal + 1; + let valueReal = this.freqComplexBuffer[indexReal]; + let valueImag = this.freqComplexBuffer[indexImag]; + + let valueShiftedReal = valueReal * phaseShiftReal - valueImag * phaseShiftImag; + let valueShiftedImag = valueReal * phaseShiftImag + valueImag * phaseShiftReal; + + let indexShiftedReal = binIndexShifted * 2; + let indexShiftedImag = indexShiftedReal + 1; + this.freqComplexBufferShifted[indexShiftedReal] += valueShiftedReal; + this.freqComplexBufferShifted[indexShiftedImag] += valueShiftedImag; + } } + } } -registerProcessor("phase-vocoder-processor", PhaseVocoderProcessor); - - - - +registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f06a6a9..0082b522 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,9 +48,6 @@ importers: events: specifier: ^3.3.0 version: 3.3.0 - fft-js: - specifier: ^0.0.12 - version: 0.0.12 jsdoc: specifier: ^4.0.2 version: 4.0.2 @@ -433,9 +430,6 @@ importers: packages/superdough: dependencies: - fft.js: - specifier: ^4.0.4 - version: 4.0.4 nanostores: specifier: ^0.9.5 version: 0.9.5 @@ -7700,18 +7694,6 @@ packages: resolution: {integrity: sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==} dev: true - /fft-js@0.0.12: - resolution: {integrity: sha512-nLOa0/SYYnN2NPcLrI81UNSPxyg3q0sGiltfe9G1okg0nxs5CqAwtmaqPQdGcOryeGURaCoQx8Y4AUkhGTh7IQ==} - engines: {node: '>=0.12.0'} - dependencies: - bit-twiddle: 1.0.2 - commander: 2.7.1 - dev: true - - /fft.js@4.0.4: - resolution: {integrity: sha512-f9c00hphOgeQTlDyavwTtu6RiK8AIFjD6+jvXkNkpeQ7rirK3uFWVpalkoS4LAwbdX7mfZ8aoBfFVQX1Re/8aw==} - dev: false - /fftjs@0.0.4: resolution: {integrity: sha512-nIWxQyth1LVD6NH8a+YZUv+McjzbOY6dMe4wv6Pq5cGfP+c8Rd1T8Dsd50DCWlNgzSqA3y9lOkpD6dZD3qHa1A==} dependencies: From 11102adb75404f75b6126dc8ccd5b71ffe165131 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 12 Jun 2024 01:14:20 -0400 Subject: [PATCH 046/547] unessecary import --- packages/core/pattern.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 516c5815..cf726eed 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -21,7 +21,6 @@ import { numeralArgs, parseNumeral, pairs, - clamp, } from './util.mjs'; import drawLine from './drawLine.mjs'; import { logger } from './logger.mjs'; From f3fa1483d748e170c2424b2980b4f05bc9c34d94 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 12 Jun 2024 01:14:59 -0400 Subject: [PATCH 047/547] fix pnpm lock --- pnpm-lock.yaml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0082b522..a07618b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5879,10 +5879,6 @@ packages: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} - /bit-twiddle@1.0.2: - resolution: {integrity: sha512-B9UhK0DKFZhoTFcfvAzhqsjStvGJp9vYWf3+6SNTtdSQnvIgfkHbgHrg/e4+TH71N2GDu8tpmCVoyfrL1d7ntA==} - dev: true - /bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} dependencies: @@ -6440,13 +6436,6 @@ packages: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} dev: true - /commander@2.7.1: - resolution: {integrity: sha512-5qK/Wsc2fnRCiizV1JlHavWrSGAXQI7AusK423F8zJLwIGq8lmtO5GmO8PVMrtDUJMwTXOFBzSN6OCRD8CEMWw==} - engines: {node: '>= 0.6.x'} - dependencies: - graceful-readlink: 1.0.1 - dev: true - /commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -8262,10 +8251,6 @@ packages: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} dev: true - /graceful-readlink@1.0.1: - resolution: {integrity: sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==} - dev: true - /graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} From 248f67d1abd5f42076d74e7e0520e5cbbeeacc71 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 12 Jun 2024 10:28:12 -0400 Subject: [PATCH 048/547] prettier --- packages/core/pattern.mjs | 1 - test/__snapshots__/examples.test.mjs.snap | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index cf726eed..a8a4f7da 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1970,7 +1970,6 @@ export const late = register( true, ); - /** * Plays a portion of a pattern, specified by the beginning and end of a time span. The new resulting pattern is played over the time period of the original pattern: * diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 3c1e39f8..41aa8d84 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -7486,6 +7486,23 @@ exports[`runs examples > example "steps" example index 0 1`] = ` ] `; +exports[`runs examples > example "stretch" example index 0 1`] = ` +[ + "[ (0/1 → 1/3) ⇝ 1/1 | s:gm_flute stretch:1 ]", + "[ 0/1 ⇜ (1/3 → 2/3) ⇝ 1/1 | s:gm_flute stretch:2 ]", + "[ 0/1 ⇜ (2/3 → 1/1) | s:gm_flute stretch:0.5 ]", + "[ (1/1 → 4/3) ⇝ 2/1 | s:gm_flute stretch:1 ]", + "[ 1/1 ⇜ (4/3 → 5/3) ⇝ 2/1 | s:gm_flute stretch:2 ]", + "[ 1/1 ⇜ (5/3 → 2/1) | s:gm_flute stretch:0.5 ]", + "[ (2/1 → 7/3) ⇝ 3/1 | s:gm_flute stretch:1 ]", + "[ 2/1 ⇜ (7/3 → 8/3) ⇝ 3/1 | s:gm_flute stretch:2 ]", + "[ 2/1 ⇜ (8/3 → 3/1) | s:gm_flute stretch:0.5 ]", + "[ (3/1 → 10/3) ⇝ 4/1 | s:gm_flute stretch:1 ]", + "[ 3/1 ⇜ (10/3 → 11/3) ⇝ 4/1 | s:gm_flute stretch:2 ]", + "[ 3/1 ⇜ (11/3 → 4/1) | s:gm_flute stretch:0.5 ]", +] +`; + exports[`runs examples > example "striate" example index 0 1`] = ` [ "[ 0/1 → 1/6 | s:numbers n:0 begin:0 end:0.16666666666666666 ]", From 6c9e0aaa1aa9bacc2793cc0cdfd2669350465e46 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 14 Jun 2024 01:24:08 -0400 Subject: [PATCH 049/547] added udels editor and header --- website/src/components/Udels/Udels.jsx | 7 ++- website/src/components/Udels/UdelsEditor.jsx | 34 ++++++++++ website/src/components/Udels/UdelsHeader.tsx | 25 ++++++++ website/src/repl/Repl.jsx | 10 ++- website/src/repl/components/BigPlayButton.jsx | 22 +++++++ website/src/repl/components/Code.jsx | 23 +++++++ .../components/UserFacingErrorMessage.jsx | 8 +++ website/src/repl/panel/NumberInput.jsx | 62 +++++++++++++++++++ website/src/settings.mjs | 6 +- 9 files changed, 190 insertions(+), 7 deletions(-) create mode 100644 website/src/components/Udels/UdelsEditor.jsx create mode 100644 website/src/components/Udels/UdelsHeader.tsx create mode 100644 website/src/repl/components/BigPlayButton.jsx create mode 100644 website/src/repl/components/Code.jsx create mode 100644 website/src/repl/components/UserFacingErrorMessage.jsx create mode 100644 website/src/repl/panel/NumberInput.jsx diff --git a/website/src/components/Udels/Udels.jsx b/website/src/components/Udels/Udels.jsx index b2d81ea9..72d983e0 100644 --- a/website/src/components/Udels/Udels.jsx +++ b/website/src/components/Udels/Udels.jsx @@ -2,6 +2,7 @@ import { code2hash } from '@strudel/core'; import { UdelFrame } from './UdelFrame'; import { useState } from 'react'; +import UdelsHeader from './UdelsHeader'; function NumberInput({ value, onChange, label = '', min, max }) { const [localState, setLocalState] = useState(value); @@ -68,7 +69,6 @@ export function Udels() { return (
-
+ {/*
-
+
*/}
, +// editorRef: React.MutableRefObject, +// error: Error +// init: () => void +// } + +export default function UdelsEditor(Props) { + const { context, containerRef, editorRef, error, init } = Props; + const { pending, started, handleTogglePlay } = context; + return ( + +
+ + {/*
*/} + +
+ +
+ + +
+
+ ); +} diff --git a/website/src/components/Udels/UdelsHeader.tsx b/website/src/components/Udels/UdelsHeader.tsx new file mode 100644 index 00000000..7555e182 --- /dev/null +++ b/website/src/components/Udels/UdelsHeader.tsx @@ -0,0 +1,25 @@ +import NumberInput from '@src/repl/panel/NumberInput'; + +export default function UdelsHeader(Props) { + const { numWindows, setNumWindows } = Props; + + return ( + + ); +} diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index a4b7ba73..87ff653c 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -39,6 +39,7 @@ import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon'; import './Repl.css'; import { setInterval, clearInterval } from 'worker-timers'; import { getMetadata } from '../metadata_parser'; +import UdelsEditor from '@components/Udels/UdelsEditor'; const { latestCode } = settingsMap.get(); @@ -234,7 +235,14 @@ export function Repl({ embedded = false }) { handleEvaluate, }; - const showPanel = !isEmbedded || isUdels(); + if (isUdels()) { + return ( + + ); + } + + const showPanel = !isEmbedded; + return (
diff --git a/website/src/repl/components/BigPlayButton.jsx b/website/src/repl/components/BigPlayButton.jsx new file mode 100644 index 00000000..80a4d345 --- /dev/null +++ b/website/src/repl/components/BigPlayButton.jsx @@ -0,0 +1,22 @@ +import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon'; + +// type Props = { +// started: boolean; +// handleTogglePlay: () => void; +// }; +export default function BigPlayButton(Props) { + const { started, handleTogglePlay } = Props; + if (started) { + return; + } + + return ( + + ); +} diff --git a/website/src/repl/components/Code.jsx b/website/src/repl/components/Code.jsx new file mode 100644 index 00000000..5afc53eb --- /dev/null +++ b/website/src/repl/components/Code.jsx @@ -0,0 +1,23 @@ + + +// type Props = { +// containerRef: React.MutableRefObject, +// editorRef: React.MutableRefObject, +// init: () => void +// } +export function Code(Props) { +const {editorRef, containerRef, init} = Props; + + return ( +
{ + containerRef.current = el; + if (!editorRef.current) { + init(); + } + }} + >
+ ); +} diff --git a/website/src/repl/components/UserFacingErrorMessage.jsx b/website/src/repl/components/UserFacingErrorMessage.jsx new file mode 100644 index 00000000..ec8d8be3 --- /dev/null +++ b/website/src/repl/components/UserFacingErrorMessage.jsx @@ -0,0 +1,8 @@ +// type Props = { error: Error | null }; +export default function UserFacingErrorMessage(Props) { + const { error } = Props; + if (error == null) { + return; + } + return
{error.message || 'Unknown Error :-/'}
; +} diff --git a/website/src/repl/panel/NumberInput.jsx b/website/src/repl/panel/NumberInput.jsx new file mode 100644 index 00000000..d6683587 --- /dev/null +++ b/website/src/repl/panel/NumberInput.jsx @@ -0,0 +1,62 @@ +function Button(Props) { + const { children, onClick } = Props; + + return ( + + ); +} +export default function NumberInput(Props) { + const { value = 0, setValue, max, min} = Props; + + return ( +
+ + setValue(e.target.value)} + /> + +
+ ); + + +} diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 3f1ca051..c7958912 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -1,7 +1,7 @@ import { persistentMap } from '@nanostores/persistent'; import { useStore } from '@nanostores/react'; import { register } from '@strudel/core'; -import { isUdels } from './repl/util.mjs'; +import { isUdels, } from './repl/util.mjs'; export const defaultAudioDeviceName = 'System Standard'; @@ -25,7 +25,7 @@ export const defaultSettings = { isZen: false, soundsFilter: 'all', patternFilter: 'community', - panelPosition: 'right', + panelPosition: 'right', userPatterns: '{}', audioDeviceName: defaultAudioDeviceName, }; @@ -61,7 +61,7 @@ export function useSettings() { isFlashEnabled: parseBoolean(state.isFlashEnabled), isSyncEnabled: isUdels() ? true : parseBoolean(state.isSyncEnabled), fontSize: Number(state.fontSize), - panelPosition: state.activeFooter !== '' ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is! + panelPosition: state.activeFooter !== '' && !isUdels() ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is! userPatterns: userPatterns, }; } From 2a2ddf205abf51c8d4a0edc450ee63474c585980 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 14 Jun 2024 14:26:23 -0400 Subject: [PATCH 050/547] need to fix window message --- website/src/components/Oven/Oven.jsx | 2 +- website/src/components/Udels/UdelFrame.jsx | 3 +- website/src/components/Udels/Udels.jsx | 41 +----------- website/src/components/Udels/UdelsEditor.jsx | 4 +- website/src/components/Udels/UdelsHeader.tsx | 2 +- website/src/repl/Repl.jsx | 50 ++++----------- website/src/repl/{ => components}/Header.jsx | 4 +- website/src/repl/{ => components}/Loader.jsx | 0 website/src/repl/components/NumberInput.jsx | 54 ++++++++++++++++ website/src/repl/components/ReplEditor.jsx | 39 ++++++++++++ .../panel/AudioDeviceSelector.jsx | 2 +- .../{ => components}/panel/ConsoleTab.jsx | 0 .../repl/{ => components}/panel/FilesTab.jsx | 2 +- .../src/repl/{ => components}/panel/Forms.jsx | 0 .../panel/ImportSoundsButton.jsx | 2 +- .../src/repl/{ => components}/panel/Panel.jsx | 2 +- .../{ => components}/panel/PatternsTab.jsx | 10 +-- .../repl/{ => components}/panel/Reference.jsx | 2 +- .../{ => components}/panel/SelectInput.jsx | 0 .../{ => components}/panel/SettingsTab.jsx | 4 +- .../repl/{ => components}/panel/SoundsTab.jsx | 2 +- .../{ => components}/panel/WelcomeTab.jsx | 0 website/src/repl/panel/NumberInput.jsx | 62 ------------------- 23 files changed, 126 insertions(+), 161 deletions(-) rename website/src/repl/{ => components}/Header.jsx (98%) rename website/src/repl/{ => components}/Loader.jsx (100%) create mode 100644 website/src/repl/components/NumberInput.jsx create mode 100644 website/src/repl/components/ReplEditor.jsx rename website/src/repl/{ => components}/panel/AudioDeviceSelector.jsx (94%) rename website/src/repl/{ => components}/panel/ConsoleTab.jsx (100%) rename website/src/repl/{ => components}/panel/FilesTab.jsx (97%) rename website/src/repl/{ => components}/panel/Forms.jsx (100%) rename website/src/repl/{ => components}/panel/ImportSoundsButton.jsx (97%) rename website/src/repl/{ => components}/panel/Panel.jsx (98%) rename website/src/repl/{ => components}/panel/PatternsTab.jsx (95%) rename website/src/repl/{ => components}/panel/Reference.jsx (98%) rename website/src/repl/{ => components}/panel/SelectInput.jsx (100%) rename website/src/repl/{ => components}/panel/SettingsTab.jsx (99%) rename website/src/repl/{ => components}/panel/SoundsTab.jsx (98%) rename website/src/repl/{ => components}/panel/WelcomeTab.jsx (100%) delete mode 100644 website/src/repl/panel/NumberInput.jsx diff --git a/website/src/components/Oven/Oven.jsx b/website/src/components/Oven/Oven.jsx index a4199b5e..eb3c8692 100644 --- a/website/src/components/Oven/Oven.jsx +++ b/website/src/components/Oven/Oven.jsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { loadFeaturedPatterns, loadPublicPatterns } from '@src/user_pattern_utils.mjs'; import { MiniRepl } from '@src/docs/MiniRepl'; -import { PatternLabel } from '@src/repl/panel/PatternsTab'; +import { PatternLabel } from '@src/repl/components/panel/PatternsTab'; function PatternList({ patterns }) { return ( diff --git a/website/src/components/Udels/UdelFrame.jsx b/website/src/components/Udels/UdelFrame.jsx index 78d28944..856aa831 100644 --- a/website/src/components/Udels/UdelFrame.jsx +++ b/website/src/components/Udels/UdelFrame.jsx @@ -2,8 +2,9 @@ import { useRef } from 'react'; export function UdelFrame({ onEvaluate, hash, instance }) { const ref = useRef(); - + console.info('frame') window.addEventListener('message', (message) => { + console.info(message, 'message') const childWindow = ref?.current?.contentWindow; if (message == null || message.source !== childWindow) { return; // Skip message in this event listener diff --git a/website/src/components/Udels/Udels.jsx b/website/src/components/Udels/Udels.jsx index 72d983e0..c3062419 100644 --- a/website/src/components/Udels/Udels.jsx +++ b/website/src/components/Udels/Udels.jsx @@ -4,36 +4,7 @@ import { UdelFrame } from './UdelFrame'; import { useState } from 'react'; import UdelsHeader from './UdelsHeader'; -function NumberInput({ value, onChange, label = '', min, max }) { - const [localState, setLocalState] = useState(value); - const [isFocused, setIsFocused] = useState(false); - return ( - - ); -} const defaultHash = 'c3RhY2soCiAgCik%3D'; const getHashesFromUrl = () => { @@ -60,6 +31,7 @@ export function Udels() { const onEvaluate = (key, code) => { const hashes = getHashesFromUrl(); hashes[key] = code2hash(code); + updateURLHashes(hashes); }; // useEffect(() => { @@ -78,17 +50,6 @@ export function Udels() { }} > - {/*
- -
*/}
-
- -
- {isEmbedded && !started && ( - - )} -
-
{ - containerRef.current = el; - if (!editorRef.current) { - init(); - } - }} - >
- {panelPosition === 'right' && showPanel && } -
- {error && ( -
{error.message || 'Unknown Error :-/'}
- )} - {panelPosition === 'bottom' && showPanel && } -
- + ); } diff --git a/website/src/repl/Header.jsx b/website/src/repl/components/Header.jsx similarity index 98% rename from website/src/repl/Header.jsx rename to website/src/repl/components/Header.jsx index 748b84e4..288822dd 100644 --- a/website/src/repl/Header.jsx +++ b/website/src/repl/components/Header.jsx @@ -5,9 +5,9 @@ import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon'; import SparklesIcon from '@heroicons/react/20/solid/SparklesIcon'; import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon'; import cx from '@src/cx.mjs'; -import { useSettings, setIsZen } from '../settings.mjs'; +import { useSettings, setIsZen } from '../../settings.mjs'; // import { ReplContext } from './Repl'; -import './Repl.css'; +import '../Repl.css'; const { BASE_URL } = import.meta.env; const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; diff --git a/website/src/repl/Loader.jsx b/website/src/repl/components/Loader.jsx similarity index 100% rename from website/src/repl/Loader.jsx rename to website/src/repl/components/Loader.jsx diff --git a/website/src/repl/components/NumberInput.jsx b/website/src/repl/components/NumberInput.jsx new file mode 100644 index 00000000..0c3e7e53 --- /dev/null +++ b/website/src/repl/components/NumberInput.jsx @@ -0,0 +1,54 @@ +function Button(Props) { + const { children, onClick } = Props; + + return ( + + ); +} +export default function NumberInput(Props) { + const { value = 0, setValue, max, min } = Props; + + return ( +
+ + setValue(e.target.value)} + /> + +
+ ); +} diff --git a/website/src/repl/components/ReplEditor.jsx b/website/src/repl/components/ReplEditor.jsx new file mode 100644 index 00000000..57eabb71 --- /dev/null +++ b/website/src/repl/components/ReplEditor.jsx @@ -0,0 +1,39 @@ +import { ReplContext } from '@src/repl/util.mjs'; + +import Loader from '@src/repl/components/Loader'; +import { Panel } from '@src/repl/components/panel/Panel'; +import { Code } from '@src/repl/components/Code'; +import BigPlayButton from '@src/repl/components/BigPlayButton'; +import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage'; +import { Header } from './Header'; + + +// type Props = { +// context: replcontext, +// containerRef: React.MutableRefObject, +// editorRef: React.MutableRefObject, +// error: Error +// init: () => void +// isEmbedded: boolean +// } + +export default function ReplEditor(Props) { + const { context, containerRef, editorRef, error, init, panelPosition } = Props; + const { pending, started, handleTogglePlay, isEmbedded } = context; + const showPanel = !isEmbedded; + return ( + +
+ +
+ {isEmbedded && } +
+ + {panelPosition === 'right' && showPanel && } +
+ + {panelPosition === 'bottom' && showPanel && } +
+
+ ); +} diff --git a/website/src/repl/panel/AudioDeviceSelector.jsx b/website/src/repl/components/panel/AudioDeviceSelector.jsx similarity index 94% rename from website/src/repl/panel/AudioDeviceSelector.jsx rename to website/src/repl/components/panel/AudioDeviceSelector.jsx index 969bf387..d1f13c22 100644 --- a/website/src/repl/panel/AudioDeviceSelector.jsx +++ b/website/src/repl/components/panel/AudioDeviceSelector.jsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { getAudioDevices, setAudioDevice } from '../util.mjs'; +import { getAudioDevices, setAudioDevice } from '../../util.mjs'; import { SelectInput } from './SelectInput'; const initdevices = new Map(); diff --git a/website/src/repl/panel/ConsoleTab.jsx b/website/src/repl/components/panel/ConsoleTab.jsx similarity index 100% rename from website/src/repl/panel/ConsoleTab.jsx rename to website/src/repl/components/panel/ConsoleTab.jsx diff --git a/website/src/repl/panel/FilesTab.jsx b/website/src/repl/components/panel/FilesTab.jsx similarity index 97% rename from website/src/repl/panel/FilesTab.jsx rename to website/src/repl/components/panel/FilesTab.jsx index d78ec1ec..2e121fb5 100644 --- a/website/src/repl/panel/FilesTab.jsx +++ b/website/src/repl/components/panel/FilesTab.jsx @@ -1,6 +1,6 @@ import { Fragment, useEffect } from 'react'; import React, { useMemo, useState } from 'react'; -import { isAudioFile, readDir, dir, playFile } from '../files.mjs'; +import { isAudioFile, readDir, dir, playFile } from '../../files.mjs'; export function FilesTab() { const [path, setPath] = useState([]); diff --git a/website/src/repl/panel/Forms.jsx b/website/src/repl/components/panel/Forms.jsx similarity index 100% rename from website/src/repl/panel/Forms.jsx rename to website/src/repl/components/panel/Forms.jsx diff --git a/website/src/repl/panel/ImportSoundsButton.jsx b/website/src/repl/components/panel/ImportSoundsButton.jsx similarity index 97% rename from website/src/repl/panel/ImportSoundsButton.jsx rename to website/src/repl/components/panel/ImportSoundsButton.jsx index da45705f..7862b766 100644 --- a/website/src/repl/panel/ImportSoundsButton.jsx +++ b/website/src/repl/components/panel/ImportSoundsButton.jsx @@ -1,5 +1,5 @@ import React, { useCallback, useState } from 'react'; -import { registerSamplesFromDB, uploadSamplesToDB, userSamplesDBConfig } from '../idbutils.mjs'; +import { registerSamplesFromDB, uploadSamplesToDB, userSamplesDBConfig } from '../../idbutils.mjs'; //choose a directory to locally import samples export default function ImportSoundsButton({ onComplete }) { diff --git a/website/src/repl/panel/Panel.jsx b/website/src/repl/components/panel/Panel.jsx similarity index 98% rename from website/src/repl/panel/Panel.jsx rename to website/src/repl/components/panel/Panel.jsx index 7d35f0e4..a7bb6a83 100644 --- a/website/src/repl/panel/Panel.jsx +++ b/website/src/repl/components/panel/Panel.jsx @@ -4,7 +4,7 @@ import useEvent from '@src/useEvent.mjs'; import cx from '@src/cx.mjs'; import { nanoid } from 'nanoid'; import { useCallback, useLayoutEffect, useEffect, useRef, useState } from 'react'; -import { setActiveFooter, useSettings } from '../../settings.mjs'; +import { setActiveFooter, useSettings } from '../../../settings.mjs'; import { ConsoleTab } from './ConsoleTab'; import { FilesTab } from './FilesTab'; import { Reference } from './Reference'; diff --git a/website/src/repl/panel/PatternsTab.jsx b/website/src/repl/components/panel/PatternsTab.jsx similarity index 95% rename from website/src/repl/panel/PatternsTab.jsx rename to website/src/repl/components/panel/PatternsTab.jsx index cacbe897..5e9119b1 100644 --- a/website/src/repl/panel/PatternsTab.jsx +++ b/website/src/repl/components/panel/PatternsTab.jsx @@ -5,13 +5,13 @@ import { useActivePattern, useViewingPatternData, userPattern, -} from '../../user_pattern_utils.mjs'; +} from '../../../user_pattern_utils.mjs'; import { useMemo } from 'react'; -import { getMetadata } from '../../metadata_parser'; -import { useExamplePatterns } from '../useExamplePatterns'; -import { parseJSON, isUdels } from '../util.mjs'; +import { getMetadata } from '../../../metadata_parser.js'; +import { useExamplePatterns } from '../../useExamplePatterns.jsx'; +import { parseJSON, isUdels } from '../../util.mjs'; import { ButtonGroup } from './Forms.jsx'; -import { settingsMap, useSettings } from '../../settings.mjs'; +import { settingsMap, useSettings } from '../../../settings.mjs'; function classNames(...classes) { return classes.filter(Boolean).join(' '); diff --git a/website/src/repl/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx similarity index 98% rename from website/src/repl/panel/Reference.jsx rename to website/src/repl/components/panel/Reference.jsx index 9483e9c5..04297c0b 100644 --- a/website/src/repl/panel/Reference.jsx +++ b/website/src/repl/components/panel/Reference.jsx @@ -1,4 +1,4 @@ -import jsdocJson from '../../../../doc.json'; +import jsdocJson from '../../../../../doc.json'; const visibleFunctions = jsdocJson.docs .filter(({ name, description }) => name && !name.startsWith('_') && !!description) .sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name)); diff --git a/website/src/repl/panel/SelectInput.jsx b/website/src/repl/components/panel/SelectInput.jsx similarity index 100% rename from website/src/repl/panel/SelectInput.jsx rename to website/src/repl/components/panel/SelectInput.jsx diff --git a/website/src/repl/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx similarity index 99% rename from website/src/repl/panel/SettingsTab.jsx rename to website/src/repl/components/panel/SettingsTab.jsx index 36ec12bd..e1d047ea 100644 --- a/website/src/repl/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -1,6 +1,6 @@ -import { defaultSettings, settingsMap, useSettings } from '../../settings.mjs'; +import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs'; import { themes } from '@strudel/codemirror'; -import { isUdels } from '../util.mjs'; +import { isUdels } from '../../util.mjs'; import { ButtonGroup } from './Forms.jsx'; import { AudioDeviceSelector } from './AudioDeviceSelector.jsx'; diff --git a/website/src/repl/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx similarity index 98% rename from website/src/repl/panel/SoundsTab.jsx rename to website/src/repl/components/panel/SoundsTab.jsx index 9b35d04b..8b7b36a8 100644 --- a/website/src/repl/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -2,7 +2,7 @@ import useEvent from '@src/useEvent.mjs'; import { useStore } from '@nanostores/react'; import { getAudioContext, soundMap, connectToDestination } from '@strudel/webaudio'; import React, { useMemo, useRef } from 'react'; -import { settingsMap, useSettings } from '../../settings.mjs'; +import { settingsMap, useSettings } from '../../../settings.mjs'; import { ButtonGroup } from './Forms.jsx'; import ImportSoundsButton from './ImportSoundsButton.jsx'; diff --git a/website/src/repl/panel/WelcomeTab.jsx b/website/src/repl/components/panel/WelcomeTab.jsx similarity index 100% rename from website/src/repl/panel/WelcomeTab.jsx rename to website/src/repl/components/panel/WelcomeTab.jsx diff --git a/website/src/repl/panel/NumberInput.jsx b/website/src/repl/panel/NumberInput.jsx deleted file mode 100644 index d6683587..00000000 --- a/website/src/repl/panel/NumberInput.jsx +++ /dev/null @@ -1,62 +0,0 @@ -function Button(Props) { - const { children, onClick } = Props; - - return ( - - ); -} -export default function NumberInput(Props) { - const { value = 0, setValue, max, min} = Props; - - return ( -
- - setValue(e.target.value)} - /> - -
- ); - - -} From 1d95da7a3a0aa62ec9168311fdb718e6bd7cc7c1 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 14 Jun 2024 15:28:24 -0400 Subject: [PATCH 051/547] fixed build window issue: --- packages/core/evaluate.mjs | 2 ++ website/src/components/Udels/UdelFrame.jsx | 2 -- website/src/settings.mjs | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/core/evaluate.mjs b/packages/core/evaluate.mjs index 23cd44c1..af02a9a9 100644 --- a/packages/core/evaluate.mjs +++ b/packages/core/evaluate.mjs @@ -39,6 +39,8 @@ function safeEval(str, options = {}) { export const evaluate = async (code, transpiler, transpilerOptions) => { let meta = {}; + //post to iframe parent (like Udels) if it exists... + window.parent?.postMessage(code); if (transpiler) { // transform syntactically correct js code to semantically usable code const transpiled = transpiler(code, transpilerOptions); diff --git a/website/src/components/Udels/UdelFrame.jsx b/website/src/components/Udels/UdelFrame.jsx index 856aa831..8bbbe14e 100644 --- a/website/src/components/Udels/UdelFrame.jsx +++ b/website/src/components/Udels/UdelFrame.jsx @@ -2,9 +2,7 @@ import { useRef } from 'react'; export function UdelFrame({ onEvaluate, hash, instance }) { const ref = useRef(); - console.info('frame') window.addEventListener('message', (message) => { - console.info(message, 'message') const childWindow = ref?.current?.contentWindow; if (message == null || message.source !== childWindow) { return; // Skip message in this event listener diff --git a/website/src/settings.mjs b/website/src/settings.mjs index c7958912..1405fbf1 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -29,9 +29,13 @@ export const defaultSettings = { userPatterns: '{}', audioDeviceName: defaultAudioDeviceName, }; -const search = new URLSearchParams(window.location.search); + +let search = null; +if (typeof window !== 'undefined') { + search = new URLSearchParams(window.location.search); +} // if running multiple instance in one window, it will use the settings for that instance. else default to normal -const instance = parseInt(search.get('instance') ?? '0'); +const instance = parseInt(search?.get('instance') ?? '0'); const settings_key = `strudel-settings${instance > 0 ? instance : ''}`; export const settingsMap = persistentMap(settings_key, defaultSettings); From f3b83b41f5da6f1562d8ed43865ec8816b55bae3 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 14 Jun 2024 15:40:37 -0400 Subject: [PATCH 052/547] prettier --- website/src/components/Udels/Udels.jsx | 3 +-- website/src/components/Udels/UdelsHeader.tsx | 13 ++++--------- website/src/repl/components/Code.jsx | 4 +--- website/src/repl/components/ReplEditor.jsx | 3 +-- website/src/settings.mjs | 6 +++--- 5 files changed, 10 insertions(+), 19 deletions(-) diff --git a/website/src/components/Udels/Udels.jsx b/website/src/components/Udels/Udels.jsx index c3062419..0de70050 100644 --- a/website/src/components/Udels/Udels.jsx +++ b/website/src/components/Udels/Udels.jsx @@ -4,7 +4,6 @@ import { UdelFrame } from './UdelFrame'; import { useState } from 'react'; import UdelsHeader from './UdelsHeader'; - const defaultHash = 'c3RhY2soCiAgCik%3D'; const getHashesFromUrl = () => { @@ -31,7 +30,7 @@ export function Udels() { const onEvaluate = (key, code) => { const hashes = getHashesFromUrl(); hashes[key] = code2hash(code); - + updateURLHashes(hashes); }; // useEffect(() => { diff --git a/website/src/components/Udels/UdelsHeader.tsx b/website/src/components/Udels/UdelsHeader.tsx index 7049e781..d56f3a1d 100644 --- a/website/src/components/Udels/UdelsHeader.tsx +++ b/website/src/components/Udels/UdelsHeader.tsx @@ -7,16 +7,11 @@ export default function UdelsHeader(Props) {