diff --git a/ac-strudel/README.md b/ac-strudel/README.md new file mode 100644 index 0000000000..dc3a4d72ec --- /dev/null +++ b/ac-strudel/README.md @@ -0,0 +1,119 @@ +# Notepat for Strudel + +[Open the demo](https://pat.aesthetic.computer), then press Play. +All four tracks enter on the first beat: tonic melody, a fifth above it, +a chord, and bass. Random rests only occur after that opening. +The same address evaluates a new seed every ten minutes: key, notes, and rests +change when you open it or press Update. During playback, Perlin noise and +different signal periods evolve timbre, filters, stereo position, and levels. +The key does not abruptly change at a clock boundary during an existing run. +The source includes annotated, independently commentable tracks and a solo lab +for Will. Replace `Date.now()` with a fixed timestamp to keep a variation. +To use the instrument in another pattern: + +```js +await import('https://pat.aesthetic.computer/s') +note("c3 eb3 g3 bb3").s('ac_glass').n("<0.2 0.8>") +``` + +Marimbaba arrangements: + +- [Marimbaba](https://pat.aesthetic.computer/marimbaba): the written 24-bar + melody at 56 BPM in 3/4, with a reduced marimba/bass/chord arrangement. +- [Marimbaba Orbit](https://pat.aesthetic.computer/marimbaba-orbit): the same + melody at 84 BPM, with a feedback-FM double and moving stereo/delay. + +Append `/source` to either URL for annotated import-based source, or `/paste` +for the self-contained version. These adapt `pop/marimba/marimbaba.np`, not +the released master and its later renderer additions. `build-tracks.mjs` +translates every score token and checks that the five sections total 72 beats. +Attribution: **@jeffrey / aesthetic.computer**. + +[Stone Wattajetta](https://pat.aesthetic.computer/stone) is a compact instrumental +reinterpretation at 132 BPM: twelve active parts using ten AC voices. The +editable score is 49 lines / 2,528 UTF-8 bytes, down from 88 / 4,714, while the +number of distinct sounds doubles from five to ten. Strudel's `scale`, +`palindrome`, `struct`, and `euclidRot` replace the JavaScript score generator. +The E/G/A/D roots retain their 18-bar cycle; a rephrased 17-note stone melody +crosses a seven-note Orbit counterline, marimba answers, Bloom chords, and +Notepat fifth stabs. Sub/kick/bells enter immediately; all parts enter in bar one. +Shakers grow through 5/7/9/11 hits over 72 bars and rotate five steps per bar; +independent signals evolve brightness, ring, stereo, and chord level. + +Annotated [source](https://pat.aesthetic.computer/stone/source) and self-contained +[paste](https://pat.aesthetic.computer/stone/paste) remain available for Will. +The paste embeds the synth library and is consequently larger than the editable +import-based score. `stone-score.strudel` preserves the earlier cursor-based +adaptation locally. Neither arrangement reproduces the current 144-bar club +master, accelerando, FEM bodies, vocals, or found effects. +Pattern reference: [Strudel time modifiers](https://strudel.cc/learn/time-modifiers/). + +| Sound | Recipe | +| --- | --- | +| `ac_stone` | Designed inharmonic bar, modes 1:2.756:5.404; `.n()` changes brightness/ring; an approximation, not FEM | +| `ac_water` | Rising sine chirp, 0.55× to target pitch; `.n()` changes droplet length | +| `ac_kick` | Falling sine, roughly 126→43 Hz; `.n()` changes pitch attack and decay | +| `ac_marimba` | Rosewood modes 1:4:9.2, independently decaying; `.n()` controls brightness and ring | +| `ac_orbit` | Delayed feedback FM, four interlocking pulses, moving stereo and modulation depth | +| `ac_glass` | Metallic FM, triangle fifth, inharmonic partial, decaying brightness | +| `ac_swarm` | Detuned saws, triangle fifth, sine sub, drifting pitch | +| `ac_bloom` | Triangle/sine layers, gentle FM, moving stereo position and filter | +| `ac_triangle_fifth` | Triangle plus a 1.5× fifth, levels 0.42 + 0.12 | +| `ac_triangle` | Single triangle | +| `ac_sine` | Single sine | +| `ac_major` | Sine chord, semitones 0, 4, 7 | +| `ac_minor` | Sine chord, semitones 0, 3, 7 | +| `ac_sus2` | Sine chord, semitones 0, 2, 7 | + +The original six recipes come from `fedac/native/pieces/notepat.mjs`: the touchscreen +triangle Shift alternate and keyboard Control/Alt chords. Web Audio uses +band-limited oscillators; native mixer saturation, echo, and speaker coloration +are not included. Chords are level-scaled for headroom. Glass, swarm, bloom, and +orbit are new extensions of those sounds. On these four, `.n(0)` through `.n(1)` +changes FM depth, detuning, and brightness; it can be patterned. + +FM, feedback, detuning, envelopes, LFOs, and seeded composition are established +techniques. This package's identity is the particular Notepat-derived recipes, +their tuning/layer balance, and their evolving arrangement, not a novel +synthesis algorithm. Filters, reverb, delay, and pattern signals in the example +come from Strudel. Custom voices do not implement every built-in synth control; +use `.n()` for their internal modulation rather than Strudel's `.fm()`. +The marimba is a modal reduction of `pop/marimba/synths/marimba.mjs`; it omits +that renderer's mallet convolution and resonator-tube simulation. + +Use `note()` or `freq()` for pitch and Strudel's `attack`, `decay`, `sustain`, +`release`, `gain`, `pan`, filter, delay, and reverb controls. The original voices +default to a 20 ms attack, full sustain, and 100 ms release; the new voices +have their own envelopes. Water and kick use `.n()` for their percussive decay; +they accept attack/release but ignore sustain/decay. The kick has a fixed +43 Hz destination regardless of the pattern's note. No external code dependencies +are downloaded by the module; it registers with the running Strudel audio API. + +For a copy-and-paste version, paste all of [pat.aesthetic.computer/paste](https://pat.aesthetic.computer/paste) +(`notepat-paste.strudel`) into Strudel. +It contains the same voices and example, with no hosted import. Regenerate it +and the two Strudel links with `node ac-strudel/build.mjs`. + +`example.strudel` is the complete example, also served at `/source`. +`node ac-strudel/build.mjs && node ac-strudel/deploy.mjs` publishes the dedicated +`ac-strudel-notepat` Cloudflare Worker on AC's account. The Worker bundles the +source, permits cross-origin imports, and redirects `/` to Strudel's example +URL. It stores no user data and makes no upstream requests. The original +`assets.aesthetic.computer/strudel/` files remain a compatibility mirror. + +API reference: https://strudel.cc/technical-manual/sounds/ + +Validation: `node ac-strudel/verify.mjs` opens the actual Strudel editor in +headless Chrome with a local module response; `LIVE=1 node ac-strudel/verify.mjs` +checks the published URL. Both exercise the import and paste versions. Offline +audio checks cover non-silent output, triangle/fifth spectral levels, bounded +peaks, silent tails, timbre changes at both macro extremes, and exactly-once +cleanup after normal and early stops, including every modulation oscillator. +The scheduler check also verifies simultaneous first-beat onsets from all +four demo tracks, and treats Strudel's logged `getTrigger` errors as failures. +`node ac-strudel/verify-tracks.mjs` checks both Marimbaba arrangements and their +standalone versions against all 50 pitched events in the original score; +`LIVE=1` checks the published synth. All active tracks must trigger together. +`node ac-strudel/verify-stone.mjs` checks import/paste playback, immediate +entrances from all twelve parts, ten distinct voices, valid pitches across +72 bars, uninterrupted kick/gallop, changing shaker density, and timbre motion. diff --git a/ac-strudel/build-tracks.mjs b/ac-strudel/build-tracks.mjs new file mode 100644 index 0000000000..2cb940f1d6 --- /dev/null +++ b/ac-strudel/build-tracks.mjs @@ -0,0 +1,73 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import assert from 'node:assert/strict'; +const here = new URL('./', import.meta.url); +const np = await readFile(new URL('../pop/marimba/marimbaba.np', here), 'utf8'); +const sections = []; +for (const line of np.split('\n')) { + const marker = line.match(/^# (hush|twinkle|wow|baba|sleep) (\d+) /); + if (marker) sections.push({name: marker[1], bars: Number(marker[2]), tokens: [], events: []}); + if (!line.trim() || line.startsWith('#') || !sections.length) continue; + const section = sections.at(-1); + for (const token of line.trim().split(/\s+/)) { + const match = token.match(/^(.*):_\*([\d.]+)$/); + assert(match, `Unrecognized score token: ${token}`); + const pitches = match[1].toLowerCase().split('+').filter(Boolean); + const beats = Number(match[2]); + const pitch = pitches.length > 1 ? '[' + pitches.join(',') + ']' : pitches[0] || '~'; + section.tokens.push(pitch + '@' + beats); + section.events.push({pitches, beats}); + } +} +assert.equal(sections.length, 5); +assert.equal(sections.reduce((n, s) => n + s.bars, 0), 24); +for (const s of sections) assert.equal(s.events.reduce((n,e) => n + e.beats, 0), s.bars * 3); +let beat = 0; +const events = sections.flatMap(s => s.events.map(e => { const event={...e, at:beat}; beat+=e.beats; return event; })); +await writeFile(new URL('marimbaba-score.json', here), JSON.stringify({bars:24, beats:72, bpm:56, events}, null, 2)+'\n'); +const notation = sections.map(s => ` // ${s.name}: ${s.bars} bars\n '${s.tokens.join(' ')}',`).join('\n'); +const bass = ''; +const pad = '<[f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [bb2,d3,f3] [bb2,d3,f3] [bb2,d3,f3] [g3,bb3,d4] [g3,bb3,d4] [g3,bb3,d4] [g3,bb3,d4] [f3,a3,c4] [c3,e3,g3] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4]>'; +for (const remix of [false, true]) { + const name = remix ? 'marimbaba-orbit' : 'marimbaba'; + const source = `// MARIMBABA${remix ? ' / ORBIT REMIX' : ''} — @jeffrey / aesthetic.computer +// https://pat.aesthetic.computer/${name} +// Lead: pop/marimba/marimbaba.np — all 24 bars, original pitches and durations. +// Accompaniment: a reduced arrangement informed by bin/render-marimbaba.mjs. +// This is an editable score adaptation, not playback of the released master. +await import('https://pat.aesthetic.computer/s') + +// TEMPO — one cycle = one 3/4 bar. ${remix ? '84 BPM remix; set 56/3 for the lullaby.' : 'Original 56 BPM.'} +setcpm(${remix ? 84 : 56}/3) + +// MELODY — @ weights are beats. mini(...) parses the assembled strings. +// Five sections: hush, twinkle, wow, ba-ba, sleep. Total: 72 beats / 24 bars. +const melody = mini([ +${notation} +].join(' ')).note().slow(24) + +// SINGER — ac_marimba uses the source rosewood ratios 1:4:9.2. +// .n(0..1) changes mallet brightness and ring; change .s(...) to recast it. +$: melody.s('ac_marimba').n(${remix ? 'perlin.slow(13).range(.35,.9)' : '.45'}) + .attack(.001).release(.5).gain(.8).room(.3)._pianoroll() + +// ROCKING BASS — starts on beat one. Roots follow the source bass figure. +$: note("${bass}").s('${remix ? 'ac_swarm' : 'ac_sine'}') + .n(.15).attack(.008).decay(.25).sustain(.25).release(.3).gain(${remix ? '.3' : '.38'}).lpf(450) + +// HARMONY — F / Bb / Gm / F / C / F, following the renderer's chord map. +// Rearticulated once per bar; intentionally reduced from the finished mix. +$: note("${pad}").s('ac_bloom') + .n(.08).attack(.06).release(.65).gain(.16).lpf(1800).room(.4) +${remix ? ` +// ORBIT DOUBLE — exact same melody, different spectral rhythm and stereo motion. +// Comment out this block to hear just the marimba-led arrangement. +$: melody.s('ac_orbit').n(perlin.slow(17).range(.15,.85)) + .gain(sine.slow(23).range(.1,.22)).pan(sine.slow(19)) + .attack(.004).release(.4).delay(.2).delaytime(.357).room(.3) +` : ''} +// All active blocks play together. Comment out a whole $: block to mute it. +// Voices/controls: https://pat.aesthetic.computer/source +`; + await writeFile(new URL(name+'.strudel',here),source); +} +console.log('Built two Marimbaba arrangements from the 72-beat source score.'); diff --git a/ac-strudel/build.mjs b/ac-strudel/build.mjs new file mode 100644 index 0000000000..5c4cbbc9ff --- /dev/null +++ b/ac-strudel/build.mjs @@ -0,0 +1,23 @@ +import { readFile, writeFile } from "node:fs/promises"; + +const here = new URL("./", import.meta.url); +await import('./build-tracks.mjs'); +const module = await readFile(new URL("notepat.mjs", here), "utf8"); +const example = await readFile(new URL("example.strudel", here), "utf8"); +const standalone = "(() => {\n" + module.replace(/^export /gm, "") + + "\n})()\n\n" + example.replace(/^await import\([^\n]+\)\n/m, ""); +await writeFile(new URL("notepat-paste.strudel", here), standalone); +for (const [name, source] of [["example", example], ["standalone", standalone]]) { + const url = "https://strudel.cc/#" + Buffer.from(source).toString("base64"); + await writeFile(new URL(`${name}.url`, here), url + "\n"); +} +console.log("Built standalone source and Strudel example links."); +for (const name of ['marimbaba', 'marimbaba-orbit', 'stone']) { + const source = await readFile(new URL(name + '.strudel', here), 'utf8'); + const paste = '(() => {\n' + module.replace(/^export /gm, '') + + '\n})()\n\n' + source.replace(/^await import\([^\n]+\)\n/m, ''); + await writeFile(new URL(name + '-paste.strudel', here), paste); + for (const [suffix, code] of [['', source], ['-paste', paste]]) { + await writeFile(new URL(name + suffix + '.url', here), 'https://strudel.cc/#' + Buffer.from(code).toString('base64') + '\n'); + } +} diff --git a/ac-strudel/deploy.mjs b/ac-strudel/deploy.mjs new file mode 100644 index 0000000000..16a2788d04 --- /dev/null +++ b/ac-strudel/deploy.mjs @@ -0,0 +1,43 @@ +import { readFile } from 'node:fs/promises'; +import { credentials, zone, records } from '../toolchain/domains/cloudflare.mjs'; + +const service = 'ac-strudel-notepat'; +const hostname = 'pat.aesthetic.computer'; +const z = await zone('aesthetic.computer'); +const { email, apiKey } = credentials(); +const auth = { 'X-Auth-Email': email, 'X-Auth-Key': apiKey }; +const base = `https://api.cloudflare.com/client/v4/accounts/${z.account.id}/workers`; +async function call(path, options = {}) { + const response = await fetch(base + path, { ...options, headers: { ...auth, ...options.headers } }); + const body = await response.json(); + if (!body.success) throw new Error(JSON.stringify(body.errors)); + return body.result; +} +const domains = await call('/domains'); +const existing = domains.find(d => d.hostname === hostname); +if (existing && existing.service !== service) throw new Error('Host belongs to another Worker'); +if (!existing && (await records(z.id)).some(r => r.name === hostname)) { + throw new Error('Host already has DNS records; refusing to replace them'); +} +const form = new FormData(); +form.set('metadata', JSON.stringify({ main_module: 'worker.mjs', compatibility_date: '2026-10-03' })); +for (const [part, file, type] of [ + ['worker.mjs', 'worker.mjs', 'application/javascript+module'], + ['synth.txt', 'notepat.mjs', 'text/plain'], + ['example.txt', 'example.strudel', 'text/plain'], + ['paste.txt', 'notepat-paste.strudel', 'text/plain'], + ['stone.txt', 'stone.strudel', 'text/plain'], + ['stone-paste.txt', 'stone-paste.strudel', 'text/plain'], + ['marimbaba.txt', 'marimbaba.strudel', 'text/plain'], + ['marimbaba-paste.txt', 'marimbaba-paste.strudel', 'text/plain'], + ['marimbaba-orbit.txt', 'marimbaba-orbit.strudel', 'text/plain'], + ['marimbaba-orbit-paste.txt', 'marimbaba-orbit-paste.strudel', 'text/plain'], +]) { + form.set(part, new Blob([await readFile(new URL(file, import.meta.url))], { type }), part); +} +await call(`/scripts/${service}`, { method: 'PUT', body: form }); +if (!existing) await call('/domains', { + method: 'PUT', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ hostname, service, zone_id: z.id, zone_name: z.name }), +}); +console.log(`Published https://${hostname}`); diff --git a/ac-strudel/example.strudel b/ac-strudel/example.strudel new file mode 100644 index 0000000000..1cf93c925e --- /dev/null +++ b/ac-strudel/example.strudel @@ -0,0 +1,76 @@ +// PAT / for Will — https://pat.aesthetic.computer +await import('https://pat.aesthetic.computer/s') + +// VOICES — change .s(...) in any block below. +// ac_orbit: feedback FM, interlocking pulses, moving stereo +// ac_glass: metallic FM, triangle fifth, inharmonic overtones +// ac_swarm: detuned saws, triangle fifth, sine sub +// ac_bloom: softer FM, triangle/sine layers, stereo drift +// ac_marimba: rosewood modal mallet; .n() changes brightness and ring +// ac_stone: inharmonic bar; ac_water: rising drip; ac_kick: falling sine drum +// Stone Wattajetta arrangement: https://pat.aesthetic.computer/stone +// ac_triangle_fifth / ac_triangle / ac_sine: original simple voices +// ac_major / ac_minor / ac_sus2: one root triggers a three-note sine chord +// These are custom recipes using established synthesis techniques. +// The original six derive from AC OS Notepat; the four rich voices extend them. +// Track arrangements: https://pat.aesthetic.computer/marimbaba +// Faster remix: https://pat.aesthetic.computer/marimbaba-orbit + +// TIME — new notes/key every 10 minutes when you open or re-evaluate. +// Keep a version by replacing Date.now() with a fixed timestamp. +// While playing, signals keep changing timbre, space, and layer levels. +// Generated strings need mini(...); quoted pattern literals use double quotes. +const epoch = Math.floor(Date.now() / 600000) +let seed = epoch >>> 0 +const dice = () => ((seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0) / 4294967296) +const root = [45, 48, 50, 53][epoch % 4] +const scale = [0, 2, 3, 5, 7, 9, 10] +const phrase = (count, opening) => Array.from({length: count}, (_, i) => + i === 0 ? String(root + 24 + opening) : + dice() < .16 ? '~' : String(root + 24 + scale[Math.floor(dice() * scale.length)]) +).join(' ') +const melody = phrase(13, 0) +const sparks = phrase(7, 7) +const chord = (offset, intervals) => '[' + intervals.map(n => root + offset + n).join(',') + ']' +const harmony = '<' + [chord(0, [0,3,7,10]), chord(-5, [0,3,7]), chord(-4, [0,4,7]), chord(-2, [0,4,7])].join(' ') + '>' +setcpm(24) + +// ORBIT — .n(0..1) is the timbre macro: feedback/FM depth and brightness. +// sine/perlin are Strudel signals; different periods avoid a short shared loop. +// Each $: block is independent. Select a whole block and toggle // to mute it. +// All four enter on beat one: tonic lead, fifth accent, chord, and bass. +$: note(mini(melody)).s('ac_orbit').slow(2) + .n(perlin.slow(11).range(.12,.95)) + .attack(.008).decay(.2).sustain(.55).release(.6) + .gain(sine.slow(17).range(.32,.5)) + .pan(sine.slow(19)).lpf(sine.slow(23).range(600,6000)) + .delay(.3).delaytime(.375).delayfeedback(.35).room(.35) + ._pianoroll() + +// GLASS — seven-note phrase against orbit's thirteen; notes/rests vary per epoch. +$: note(mini(sparks)).s('ac_glass').slow(3) + .n(sine.slow(13).range(.1,.9)) + .attack(.004).gain(sine.slow(29).range(.16,.3)).release(.8) + .pan(sine.slow(31)).delay(.25).delaytime(.5).room(.45) + +// BLOOM — extended chords; attack/decay/sustain/release are seconds, except +// sustain (0..1). .slow(4) stretches the harmonic progression. +$: note(mini(harmony)).s('ac_bloom').slow(4) + .n(perlin.slow(37).range(.15,.8)) + .attack(.06).decay(.4).sustain(.65).release(1.8) + .gain(.2).lpf(sine.slow(41).range(650,3200)).room(.5) + +// SWARM — bass with changing tone and level. Filters/delay/reverb/pan are +// Strudel effects; FM, detune, and internal movement belong to these voices. +$: note(mini('<' + [root-12, root-17, root-16, root-14].join(' ') + '> ~ ~ [~ ' + root + ']')) + .s('ac_swarm').slow(2).n(sine.slow(7).range(.15,.8)) + .attack(.006).release(.25).gain(.4).lpf(850) + +// SOLO LAB — comment out the four active blocks above and uncomment this. +// note(...) accepts note names or MIDI numbers; freq(...) accepts hertz. +// All voices: .attack .decay .sustain .release .gain .pan .lpf .hpf .delay .room +// Rich voices: .n(0..1). Synth-specific Strudel controls such as .fm() are +// not wired into these custom voices; use .n() for their internal modulation. +// $: note("c3 eb3 g3 bb3").s('ac_orbit').n("<0 .25 .5 .75 1>") +// .attack(.02).release(.7).gain(.4).room(.3) +// $: freq("110 165 220").s('ac_triangle_fifth').gain(.4) diff --git a/ac-strudel/example.url b/ac-strudel/example.url new file mode 100644 index 0000000000..f993fccaf6 --- /dev/null +++ b/ac-strudel/example.url @@ -0,0 +1 @@ +https://strudel.cc/#Ly8gUEFUIC8gZm9yIFdpbGwg4oCUIGh0dHBzOi8vcGF0LmFlc3RoZXRpYy5jb21wdXRlcgphd2FpdCBpbXBvcnQoJ2h0dHBzOi8vcGF0LmFlc3RoZXRpYy5jb21wdXRlci9zJykKCi8vIFZPSUNFUyDigJQgY2hhbmdlIC5zKC4uLikgaW4gYW55IGJsb2NrIGJlbG93LgovLyBhY19vcmJpdDogZmVlZGJhY2sgRk0sIGludGVybG9ja2luZyBwdWxzZXMsIG1vdmluZyBzdGVyZW8KLy8gYWNfZ2xhc3M6IG1ldGFsbGljIEZNLCB0cmlhbmdsZSBmaWZ0aCwgaW5oYXJtb25pYyBvdmVydG9uZXMKLy8gYWNfc3dhcm06IGRldHVuZWQgc2F3cywgdHJpYW5nbGUgZmlmdGgsIHNpbmUgc3ViCi8vIGFjX2Jsb29tOiBzb2Z0ZXIgRk0sIHRyaWFuZ2xlL3NpbmUgbGF5ZXJzLCBzdGVyZW8gZHJpZnQKLy8gYWNfbWFyaW1iYTogcm9zZXdvb2QgbW9kYWwgbWFsbGV0OyAubigpIGNoYW5nZXMgYnJpZ2h0bmVzcyBhbmQgcmluZwovLyBhY19zdG9uZTogaW5oYXJtb25pYyBiYXI7IGFjX3dhdGVyOiByaXNpbmcgZHJpcDsgYWNfa2ljazogZmFsbGluZyBzaW5lIGRydW0KLy8gU3RvbmUgV2F0dGFqZXR0YSBhcnJhbmdlbWVudDogaHR0cHM6Ly9wYXQuYWVzdGhldGljLmNvbXB1dGVyL3N0b25lCi8vIGFjX3RyaWFuZ2xlX2ZpZnRoIC8gYWNfdHJpYW5nbGUgLyBhY19zaW5lOiBvcmlnaW5hbCBzaW1wbGUgdm9pY2VzCi8vIGFjX21ham9yIC8gYWNfbWlub3IgLyBhY19zdXMyOiBvbmUgcm9vdCB0cmlnZ2VycyBhIHRocmVlLW5vdGUgc2luZSBjaG9yZAovLyBUaGVzZSBhcmUgY3VzdG9tIHJlY2lwZXMgdXNpbmcgZXN0YWJsaXNoZWQgc3ludGhlc2lzIHRlY2huaXF1ZXMuCi8vIFRoZSBvcmlnaW5hbCBzaXggZGVyaXZlIGZyb20gQUMgT1MgTm90ZXBhdDsgdGhlIGZvdXIgcmljaCB2b2ljZXMgZXh0ZW5kIHRoZW0uCi8vIFRyYWNrIGFycmFuZ2VtZW50czogaHR0cHM6Ly9wYXQuYWVzdGhldGljLmNvbXB1dGVyL21hcmltYmFiYQovLyBGYXN0ZXIgcmVtaXg6IGh0dHBzOi8vcGF0LmFlc3RoZXRpYy5jb21wdXRlci9tYXJpbWJhYmEtb3JiaXQKCi8vIFRJTUUg4oCUIG5ldyBub3Rlcy9rZXkgZXZlcnkgMTAgbWludXRlcyB3aGVuIHlvdSBvcGVuIG9yIHJlLWV2YWx1YXRlLgovLyBLZWVwIGEgdmVyc2lvbiBieSByZXBsYWNpbmcgRGF0ZS5ub3coKSB3aXRoIGEgZml4ZWQgdGltZXN0YW1wLgovLyBXaGlsZSBwbGF5aW5nLCBzaWduYWxzIGtlZXAgY2hhbmdpbmcgdGltYnJlLCBzcGFjZSwgYW5kIGxheWVyIGxldmVscy4KLy8gR2VuZXJhdGVkIHN0cmluZ3MgbmVlZCBtaW5pKC4uLik7IHF1b3RlZCBwYXR0ZXJuIGxpdGVyYWxzIHVzZSBkb3VibGUgcXVvdGVzLgpjb25zdCBlcG9jaCA9IE1hdGguZmxvb3IoRGF0ZS5ub3coKSAvIDYwMDAwMCkKbGV0IHNlZWQgPSBlcG9jaCA+Pj4gMApjb25zdCBkaWNlID0gKCkgPT4gKChzZWVkID0gKE1hdGguaW11bChzZWVkLCAxNjY0NTI1KSArIDEwMTM5MDQyMjMpID4+PiAwKSAvIDQyOTQ5NjcyOTYpCmNvbnN0IHJvb3QgPSBbNDUsIDQ4LCA1MCwgNTNdW2Vwb2NoICUgNF0KY29uc3Qgc2NhbGUgPSBbMCwgMiwgMywgNSwgNywgOSwgMTBdCmNvbnN0IHBocmFzZSA9IChjb3VudCwgb3BlbmluZykgPT4gQXJyYXkuZnJvbSh7bGVuZ3RoOiBjb3VudH0sIChfLCBpKSA9PgogIGkgPT09IDAgPyBTdHJpbmcocm9vdCArIDI0ICsgb3BlbmluZykgOgogICAgZGljZSgpIDwgLjE2ID8gJ34nIDogU3RyaW5nKHJvb3QgKyAyNCArIHNjYWxlW01hdGguZmxvb3IoZGljZSgpICogc2NhbGUubGVuZ3RoKV0pCikuam9pbignICcpCmNvbnN0IG1lbG9keSA9IHBocmFzZSgxMywgMCkKY29uc3Qgc3BhcmtzID0gcGhyYXNlKDcsIDcpCmNvbnN0IGNob3JkID0gKG9mZnNldCwgaW50ZXJ2YWxzKSA9PiAnWycgKyBpbnRlcnZhbHMubWFwKG4gPT4gcm9vdCArIG9mZnNldCArIG4pLmpvaW4oJywnKSArICddJwpjb25zdCBoYXJtb255ID0gJzwnICsgW2Nob3JkKDAsIFswLDMsNywxMF0pLCBjaG9yZCgtNSwgWzAsMyw3XSksIGNob3JkKC00LCBbMCw0LDddKSwgY2hvcmQoLTIsIFswLDQsN10pXS5qb2luKCcgJykgKyAnPicKc2V0Y3BtKDI0KQoKLy8gT1JCSVQg4oCUIC5uKDAuLjEpIGlzIHRoZSB0aW1icmUgbWFjcm86IGZlZWRiYWNrL0ZNIGRlcHRoIGFuZCBicmlnaHRuZXNzLgovLyBzaW5lL3BlcmxpbiBhcmUgU3RydWRlbCBzaWduYWxzOyBkaWZmZXJlbnQgcGVyaW9kcyBhdm9pZCBhIHNob3J0IHNoYXJlZCBsb29wLgovLyBFYWNoICQ6IGJsb2NrIGlzIGluZGVwZW5kZW50LiBTZWxlY3QgYSB3aG9sZSBibG9jayBhbmQgdG9nZ2xlIC8vIHRvIG11dGUgaXQuCi8vIEFsbCBmb3VyIGVudGVyIG9uIGJlYXQgb25lOiB0b25pYyBsZWFkLCBmaWZ0aCBhY2NlbnQsIGNob3JkLCBhbmQgYmFzcy4KJDogbm90ZShtaW5pKG1lbG9keSkpLnMoJ2FjX29yYml0Jykuc2xvdygyKQogIC5uKHBlcmxpbi5zbG93KDExKS5yYW5nZSguMTIsLjk1KSkKICAuYXR0YWNrKC4wMDgpLmRlY2F5KC4yKS5zdXN0YWluKC41NSkucmVsZWFzZSguNikKICAuZ2FpbihzaW5lLnNsb3coMTcpLnJhbmdlKC4zMiwuNSkpCiAgLnBhbihzaW5lLnNsb3coMTkpKS5scGYoc2luZS5zbG93KDIzKS5yYW5nZSg2MDAsNjAwMCkpCiAgLmRlbGF5KC4zKS5kZWxheXRpbWUoLjM3NSkuZGVsYXlmZWVkYmFjayguMzUpLnJvb20oLjM1KQogIC5fcGlhbm9yb2xsKCkKCi8vIEdMQVNTIOKAlCBzZXZlbi1ub3RlIHBocmFzZSBhZ2FpbnN0IG9yYml0J3MgdGhpcnRlZW47IG5vdGVzL3Jlc3RzIHZhcnkgcGVyIGVwb2NoLgokOiBub3RlKG1pbmkoc3BhcmtzKSkucygnYWNfZ2xhc3MnKS5zbG93KDMpCiAgLm4oc2luZS5zbG93KDEzKS5yYW5nZSguMSwuOSkpCiAgLmF0dGFjayguMDA0KS5nYWluKHNpbmUuc2xvdygyOSkucmFuZ2UoLjE2LC4zKSkucmVsZWFzZSguOCkKICAucGFuKHNpbmUuc2xvdygzMSkpLmRlbGF5KC4yNSkuZGVsYXl0aW1lKC41KS5yb29tKC40NSkKCi8vIEJMT09NIOKAlCBleHRlbmRlZCBjaG9yZHM7IGF0dGFjay9kZWNheS9zdXN0YWluL3JlbGVhc2UgYXJlIHNlY29uZHMsIGV4Y2VwdAovLyBzdXN0YWluICgwLi4xKS4gLnNsb3coNCkgc3RyZXRjaGVzIHRoZSBoYXJtb25pYyBwcm9ncmVzc2lvbi4KJDogbm90ZShtaW5pKGhhcm1vbnkpKS5zKCdhY19ibG9vbScpLnNsb3coNCkKICAubihwZXJsaW4uc2xvdygzNykucmFuZ2UoLjE1LC44KSkKICAuYXR0YWNrKC4wNikuZGVjYXkoLjQpLnN1c3RhaW4oLjY1KS5yZWxlYXNlKDEuOCkKICAuZ2FpbiguMikubHBmKHNpbmUuc2xvdyg0MSkucmFuZ2UoNjUwLDMyMDApKS5yb29tKC41KQoKLy8gU1dBUk0g4oCUIGJhc3Mgd2l0aCBjaGFuZ2luZyB0b25lIGFuZCBsZXZlbC4gRmlsdGVycy9kZWxheS9yZXZlcmIvcGFuIGFyZQovLyBTdHJ1ZGVsIGVmZmVjdHM7IEZNLCBkZXR1bmUsIGFuZCBpbnRlcm5hbCBtb3ZlbWVudCBiZWxvbmcgdG8gdGhlc2Ugdm9pY2VzLgokOiBub3RlKG1pbmkoJzwnICsgW3Jvb3QtMTIsIHJvb3QtMTcsIHJvb3QtMTYsIHJvb3QtMTRdLmpvaW4oJyAnKSArICc+IH4gfiBbfiAnICsgcm9vdCArICddJykpCiAgLnMoJ2FjX3N3YXJtJykuc2xvdygyKS5uKHNpbmUuc2xvdyg3KS5yYW5nZSguMTUsLjgpKQogIC5hdHRhY2soLjAwNikucmVsZWFzZSguMjUpLmdhaW4oLjQpLmxwZig4NTApCgovLyBTT0xPIExBQiDigJQgY29tbWVudCBvdXQgdGhlIGZvdXIgYWN0aXZlIGJsb2NrcyBhYm92ZSBhbmQgdW5jb21tZW50IHRoaXMuCi8vIG5vdGUoLi4uKSBhY2NlcHRzIG5vdGUgbmFtZXMgb3IgTUlESSBudW1iZXJzOyBmcmVxKC4uLikgYWNjZXB0cyBoZXJ0ei4KLy8gQWxsIHZvaWNlczogLmF0dGFjayAuZGVjYXkgLnN1c3RhaW4gLnJlbGVhc2UgLmdhaW4gLnBhbiAubHBmIC5ocGYgLmRlbGF5IC5yb29tCi8vIFJpY2ggdm9pY2VzOiAubigwLi4xKS4gU3ludGgtc3BlY2lmaWMgU3RydWRlbCBjb250cm9scyBzdWNoIGFzIC5mbSgpIGFyZQovLyBub3Qgd2lyZWQgaW50byB0aGVzZSBjdXN0b20gdm9pY2VzOyB1c2UgLm4oKSBmb3IgdGhlaXIgaW50ZXJuYWwgbW9kdWxhdGlvbi4KLy8gJDogbm90ZSgiYzMgZWIzIGczIGJiMyIpLnMoJ2FjX29yYml0JykubigiPDAgLjI1IC41IC43NSAxPiIpCi8vICAgLmF0dGFjayguMDIpLnJlbGVhc2UoLjcpLmdhaW4oLjQpLnJvb20oLjMpCi8vICQ6IGZyZXEoIjExMCAxNjUgMjIwIikucygnYWNfdHJpYW5nbGVfZmlmdGgnKS5nYWluKC40KQo= diff --git a/ac-strudel/marimbaba-orbit-paste.strudel b/ac-strudel/marimbaba-orbit-paste.strudel new file mode 100644 index 0000000000..de754924c9 --- /dev/null +++ b/ac-strudel/marimbaba-orbit-paste.strudel @@ -0,0 +1,331 @@ +(() => { +// AC OS Notepat voices for Strudel. https://aesthetic.computer +// Recipes: fedac/native/pieces/notepat.mjs (triangle Shift alternate and chords). +// Uses Web Audio's band-limited oscillators; this is not a recording of the +// native mixer, its echo, or its output saturation. +// Glass, swarm, bloom, and orbit extend those recipes as new Strudel sounds. + +const voices = Object.freeze({ + ac_triangle: { wave: 'triangle', ratios: [1], levels: [0.5] }, + ac_triangle_fifth: { wave: 'triangle', ratios: [1, 1.5], levels: [0.42, 0.12] }, + ac_sine: { wave: 'sine', ratios: [1], levels: [0.5] }, + ac_major: { wave: 'sine', ratios: [1, 2 ** (4 / 12), 2 ** (7 / 12)] }, + ac_minor: { wave: 'sine', ratios: [1, 2 ** (3 / 12), 2 ** (7 / 12)] }, + ac_sus2: { wave: 'sine', ratios: [1, 2 ** (2 / 12), 2 ** (7 / 12)] }, + ac_glass: { rich: 'glass' }, + ac_swarm: { rich: 'swarm' }, + ac_bloom: { rich: 'bloom' }, + ac_orbit: { rich: 'orbit' }, + ac_marimba: { modal: true }, + ac_stone: { modal: 'stone' }, + ac_water: { chirp: 'water' }, + ac_kick: { chirp: 'kick' }, +}); + +const bounded = (value, fallback, min, max) => { + const n = Number(value ?? fallback); + return Number.isFinite(n) ? Math.max(min, Math.min(max, n)) : fallback; +}; + +function marimbaVoice(ctx, time, value, frequency, onended, stone = false) { + // Modal reduction of pop/marimba/synths/marimba.mjs, rosewood preset. + // Preserves its 1:4:9.2 ratios and T60 decay relationships; omits the + // source renderer's mallet convolution and resonator tube model. + const color = bounded(value.n, 0.5, 0, 1); + const duration = bounded(value.duration, 0.5, 0.001, 3600); + const attack = Math.min(bounded(value.attack, 0.001, 0, 60), duration); + const decay = Math.min(bounded(value.decay, 0, 0, 60), duration - attack); + const sustain = bounded(value.sustain, 1, 0, 1); + const release = bounded(value.release, 0.25, 0, 60); + const end = time + duration + release + 0.005; + const node = ctx.createGain(); + node.gain.setValueAtTime(attack ? 0 : 1, time); + if (attack) node.gain.linearRampToValueAtTime(1, time + attack); + node.gain.linearRampToValueAtTime(sustain, time + attack + decay); + node.gain.setValueAtTime(sustain, time + duration); + node.gain.linearRampToValueAtTime(0, end - 0.005); + // Stone is a designed inharmonic bar surrogate, not the offline FEM model. + const ratios = stone ? [1, 2.756, 5.404] : [1, 4, 9.2]; + const t60 = stone ? [2.8, 1.1, 0.38] : [1.6, 0.32, 0.09]; + const levels = [1, 0.32 * (0.3 + color * 1.4), 0.1 * (0.3 + color * 1.4)]; + const scale = 0.48 / levels.reduce((a, b) => a + b, 0); + const oscillators = [], gains = []; + let remaining = 3, finished = false; + ratios.forEach((ratio, i) => { + const o = ctx.createOscillator(), g = ctx.createGain(); + o.frequency.value = Math.min(frequency * ratio, ctx.sampleRate * 0.45); + const amplitude = levels[i] * scale; + const ring = t60[i] * (0.8 + color * 0.8); + g.gain.setValueAtTime(amplitude, time); + g.gain.exponentialRampToValueAtTime(amplitude * 0.001, time + ring); + g.gain.exponentialRampToValueAtTime(0.0000001, time + ring * 2); + o.connect(g).connect(node); + o.onended = () => { + if (--remaining === 0 && !finished) { + finished = true; + [...oscillators, ...gains, node].forEach(n => n.disconnect()); + onended?.(); + } + }; + oscillators.push(o); gains.push(g); + o.start(time); o.stop(end); + }); + return { node, stop(at) { + if (finished) return; + const stopAt = Math.max(ctx.currentTime, Math.min(at, end)); + node.gain.cancelAndHoldAtTime(stopAt); + node.gain.linearRampToValueAtTime(0, stopAt + 0.005); + oscillators.forEach(o => o.stop(Math.min(end, stopAt + 0.005))); + } }; +} + +function chirpVoice(ctx, time, value, frequency, kind, onended) { + const color = bounded(value.n, 0.5, 0, 1); + const kick = kind === 'kick'; + const duration = bounded(value.duration, 0.2, 0.001, 3600); + const release = bounded(value.release, 0.08, 0, 60); + const end = time + duration + release + 0.005; + const node = ctx.createGain(), oscillator = ctx.createOscillator(); + const attack = Math.min(bounded(value.attack, kick ? 0.007 : 0.003, 0, 60), duration); + const ring = kick ? 0.12 + color * 0.2 : 0.055 + color * 0.1; + node.gain.setValueAtTime(attack ? 0 : 0.5, time); + if (attack) node.gain.linearRampToValueAtTime(0.5, time + attack); + node.gain.exponentialRampToValueAtTime(0.00001, Math.min(end - 0.005, time + attack + ring)); + node.gain.setValueAtTime(0, end); + oscillator.frequency.setValueAtTime(kick ? 126 + color * 20 : frequency * 0.55, time); + oscillator.frequency.exponentialRampToValueAtTime(kick ? 43 : frequency, + time + Math.min(duration, kick ? 0.055 + color * 0.04 : 0.07)); + oscillator.connect(node); + let finished = false; + oscillator.onended = () => { + if (finished) return; + finished = true; oscillator.disconnect(); node.disconnect(); onended?.(); + }; + oscillator.start(time); oscillator.stop(end); + return { node, stop(at) { + if (finished) return; + const stopAt = Math.max(ctx.currentTime, Math.min(at, end)); + node.gain.cancelAndHoldAtTime(stopAt); + node.gain.linearRampToValueAtTime(0, stopAt + 0.005); + oscillator.stop(Math.min(end, stopAt + 0.005)); + } }; +} + +function richVoice(ctx, time, value, frequency, kind, onended, cps = 0.5) { + // n is a 0..1 timbre macro: FM depth, detune, and filter motion. + const color = bounded(value.n, 0.5, 0, 1); + const duration = bounded(value.duration, 0.5, 0.001, 3600); + const attack = Math.min(bounded(value.attack, kind === 'bloom' ? 0.18 : 0.006, 0, 60), duration); + const decay = Math.min(bounded(value.decay, kind === 'glass' ? 0.3 : 0.1, 0, 60), duration - attack); + const sustain = bounded(value.sustain, kind === 'glass' ? 0.24 : 0.8, 0, 1); + const release = bounded(value.release, kind === 'glass' ? 0.35 : 0.6, 0, 60); + const holdEnd = time + duration; + const end = holdEnd + release + 0.005; + const node = ctx.createGain(); + const allNodes = [node], oscillators = []; + const keep = object => { allNodes.push(object); return object; }; + const gain = level => { + const g = keep(ctx.createGain()); g.gain.value = level; return g; + }; + const osc = (type, hz) => { + const o = keep(ctx.createOscillator()); + o.type = type; + o.frequency.value = Math.max(0.05, Math.min(hz, ctx.sampleRate * 0.4)); + oscillators.push(o); return o; + }; + const filter = keep(ctx.createBiquadFilter()); + filter.type = 'lowpass'; + filter.Q.value = 0.5; + const cutoff = Math.min(ctx.sampleRate * 0.4, frequency * (5 + color * 15)); + filter.frequency.setValueAtTime(cutoff, time); + filter.frequency.exponentialRampToValueAtTime( + Math.max(60, cutoff * (kind === 'glass' ? 0.24 : 0.55)), holdEnd, + ); + filter.connect(node); + node.gain.setValueAtTime(attack ? 0 : 1, time); + if (attack) node.gain.linearRampToValueAtTime(1, time + attack); + node.gain.linearRampToValueAtTime(sustain, time + attack + decay); + node.gain.setValueAtTime(sustain, holdEnd); + node.gain.linearRampToValueAtTime(0, holdEnd + release); + + const layers = kind === 'glass' + ? [[1, 0.24, 'sine', 0], [1.5, 0.10, 'triangle', 0], [2.756, 0.07, 'sine', 0]] + : kind === 'swarm' + ? [[1, 0.12, 'sawtooth', -1], [1, 0.12, 'sawtooth', 1], [1.5, 0.09, 'triangle', 0.4], [0.5, 0.10, 'sine', 0]] + : [[1, 0.15, 'triangle', -1], [1, 0.12, 'triangle', 1], [1.5, 0.09, 'sine', -0.5], [2, 0.06, 'sine', 0.5]]; + layers.forEach(([ratio, level, wave, spread], i) => { + const carrier = osc(wave, frequency * ratio); + carrier.detune.value = spread * (4 + color * 18); + const pan = keep(ctx.createStereoPanner()); + pan.pan.value = spread * 0.45; + const pulse = gain(kind === 'orbit' ? 0.72 : 1); + carrier.connect(gain(level)).connect(pulse).connect(pan).connect(filter); + if (kind === 'orbit') { + // Amplitude pulses locked to the scheduler's cycle rate. + osc('sine', bounded(cps, 0.5, 0.01, 20) * (i + 2)) + .connect(gain(0.28)).connect(pulse.gain); + } + if (kind !== 'swarm') { + const mod = osc('sine', frequency * ratio * (kind === 'glass' ? 2.756 : 1.5)); + const depth = gain(frequency * ratio * (0.08 + color * (kind === 'glass' ? 2.4 : 0.6))); + mod.connect(depth).connect(carrier.frequency); + depth.gain.setValueAtTime(depth.gain.value, time); + depth.gain.exponentialRampToValueAtTime( + Math.max(0.001, depth.gain.value * 0.08), time + Math.min(duration, 0.3 + color), + ); + if (kind === 'orbit') { + // Carrier -> delay -> modulator pitch -> carrier pitch. The delay + // makes the feedback cycle valid, while sine outputs bound its level. + const feedback = keep(ctx.createDelay(0.05)); + feedback.delayTime.value = 0.004 + i * 0.002; + carrier.connect(feedback).connect(gain(frequency * (0.15 + color * 1.4))) + .connect(mod.frequency); + const shape = osc('sine', 0.13 + i * 0.07); + shape.connect(gain(frequency * ratio * color * 0.24)).connect(depth.gain); + } + } + const drift = osc('sine', 0.19 + i * 0.11 + color * 0.35); + drift.connect(gain(2 + color * 6)).connect(carrier.detune); + if (kind === 'bloom' || kind === 'orbit') drift.connect(gain(0.3)).connect(pan.pan); + }); + let remaining = oscillators.length, finished = false; + for (const oscillator of oscillators) { + oscillator.onended = () => { + if (--remaining === 0 && !finished) { + finished = true; + allNodes.forEach(n => n.disconnect()); + onended?.(); + } + }; + oscillator.start(time); + oscillator.stop(end); + } + return { + node, + stop(at) { + if (finished) return; + const stopAt = Math.max(ctx.currentTime, Math.min(at, end)); + node.gain.cancelAndHoldAtTime(stopAt); + node.gain.linearRampToValueAtTime(0, stopAt + 0.005); + for (const o of oscillators) o.stop(Math.min(end, stopAt + 0.005)); + }, + }; +} + +function registerNotepat(api = globalThis) { + const { registerSound, getAudioContext, getFrequencyFromValue } = api; + if (![registerSound, getAudioContext, getFrequencyFromValue].every(fn => typeof fn === 'function')) { + throw new Error('Import Notepat inside Strudel, or pass its audio API to registerNotepat().'); + } + for (const [name, recipe] of Object.entries(voices)) { + registerSound(name, (time, value, onended, cps) => { + const ctx = getAudioContext(); + const frequency = bounded(getFrequencyFromValue(value), 220, 1, ctx.sampleRate / 2 - 1); + if (recipe.modal) return marimbaVoice(ctx, time, value, frequency, onended, recipe.modal === 'stone'); + if (recipe.chirp) return chirpVoice(ctx, time, value, frequency, recipe.chirp, onended); + if (recipe.rich) return richVoice(ctx, time, value, frequency, recipe.rich, onended, cps); + const duration = bounded(value.duration, 0.5, 0.001, 3600); + const attack = Math.min(bounded(value.attack, 0.02, 0, 60), duration); + const decay = Math.min(bounded(value.decay, 0, 0, 60), duration - attack); + const sustain = bounded(value.sustain, 1, 0, 1); + const release = bounded(value.release, 0.1, 0, 60); + const holdEnd = time + duration; + const end = holdEnd + release + 0.005; + const node = ctx.createGain(); + node.gain.setValueAtTime(attack ? 0 : 1, time); + if (attack) node.gain.linearRampToValueAtTime(1, time + attack); + node.gain.linearRampToValueAtTime(sustain, time + attack + decay); + node.gain.setValueAtTime(sustain, holdEnd); + node.gain.linearRampToValueAtTime(0, holdEnd + release); + + const oscillators = []; + let remaining = recipe.ratios.length; + let finished = false; + for (let i = 0; i < recipe.ratios.length; i++) { + const oscillator = ctx.createOscillator(); + oscillator.type = recipe.wave; + oscillator.frequency.value = Math.min(frequency * recipe.ratios[i], ctx.sampleRate / 2 - 1); + const gain = ctx.createGain(); + // Preserve the native triangle layer balance. Chords share headroom. + gain.gain.value = (recipe.levels?.[i] ?? 0.5 / recipe.ratios.length) + * (recipe.wave === 'triangle' ? -1 : 1); + oscillator.connect(gain).connect(node); + oscillator.onended = () => { + oscillator.disconnect(); + gain.disconnect(); + if (--remaining === 0 && !finished) { + finished = true; + node.disconnect(); + onended?.(); + } + }; + oscillator.start(time); + oscillator.stop(end); + oscillators.push(oscillator); + } + return { + node, + // Strudel calls this when retiring a voice, including polyphony steals. + stop(at) { + if (finished) return; + const stopAt = Math.max(ctx.currentTime, Math.min(at, end)); + node.gain.cancelAndHoldAtTime(stopAt); + node.gain.linearRampToValueAtTime(0, stopAt + 0.005); + for (const oscillator of oscillators) oscillator.stop(Math.min(end, stopAt + 0.005)); + }, + }; + }, { type: 'synth' }); + } + return Object.keys(voices); +} + +registerNotepat(); + +})() + +// MARIMBABA / ORBIT REMIX — @jeffrey / aesthetic.computer +// https://pat.aesthetic.computer/marimbaba-orbit +// Lead: pop/marimba/marimbaba.np — all 24 bars, original pitches and durations. +// Accompaniment: a reduced arrangement informed by bin/render-marimbaba.mjs. +// This is an editable score adaptation, not playback of the released master. + +// TEMPO — one cycle = one 3/4 bar. 84 BPM remix; set 56/3 for the lullaby. +setcpm(84/3) + +// MELODY — @ weights are beats. mini(...) parses the assembled strings. +// Five sections: hush, twinkle, wow, ba-ba, sleep. Total: 72 beats / 24 bars. +const melody = mini([ + // hush: 4 bars + 'c5@1 a4@1 f4@1 f4@3 a4@1.5 g4@1.5 f4@3', + // twinkle: 6 bars + 'f5@1 a5@1 c6@0.5 a5@0.5 g5@3 f5@1 g5@1 a5@0.5 g5@0.5 f5@3 bb5@1 d6@1 bb5@1 c6@1.5 a5@1.5', + // wow: 4 bars + '[g5,bb5]@6 a5@1 g5@1 a5@1 f5@3', + // baba: 4 bars + 'a5@0.5 g5@0.5 a5@1 f5@1 c6@0.5 bb5@0.5 c6@1 a5@1 g5@1 f5@1 e5@1 f5@3', + // sleep: 6 bars + 'c5@1.5 a4@1.5 g4@1 f4@1 f4@1 a4@1 g4@1 f4@1 f4@3 f3@3 ~@3', +].join(' ')).note().slow(24) + +// SINGER — ac_marimba uses the source rosewood ratios 1:4:9.2. +// .n(0..1) changes mallet brightness and ring; change .s(...) to recast it. +$: melody.s('ac_marimba').n(perlin.slow(13).range(.35,.9)) + .attack(.001).release(.5).gain(.8).room(.3)._pianoroll() + +// ROCKING BASS — starts on beat one. Roots follow the source bass figure. +$: note("").s('ac_swarm') + .n(.15).attack(.008).decay(.25).sustain(.25).release(.3).gain(.3).lpf(450) + +// HARMONY — F / Bb / Gm / F / C / F, following the renderer's chord map. +// Rearticulated once per bar; intentionally reduced from the finished mix. +$: note("<[f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [bb2,d3,f3] [bb2,d3,f3] [bb2,d3,f3] [g3,bb3,d4] [g3,bb3,d4] [g3,bb3,d4] [g3,bb3,d4] [f3,a3,c4] [c3,e3,g3] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4]>").s('ac_bloom') + .n(.08).attack(.06).release(.65).gain(.16).lpf(1800).room(.4) + +// ORBIT DOUBLE — exact same melody, different spectral rhythm and stereo motion. +// Comment out this block to hear just the marimba-led arrangement. +$: melody.s('ac_orbit').n(perlin.slow(17).range(.15,.85)) + .gain(sine.slow(23).range(.1,.22)).pan(sine.slow(19)) + .attack(.004).release(.4).delay(.2).delaytime(.357).room(.3) + +// All active blocks play together. Comment out a whole $: block to mute it. +// Voices/controls: https://pat.aesthetic.computer/source diff --git a/ac-strudel/marimbaba-orbit-paste.url b/ac-strudel/marimbaba-orbit-paste.url new file mode 100644 index 0000000000..a0c45df830 --- /dev/null +++ b/ac-strudel/marimbaba-orbit-paste.url @@ -0,0 +1 @@ +https://strudel.cc/#KCgpID0+IHsKLy8gQUMgT1MgTm90ZXBhdCB2b2ljZXMgZm9yIFN0cnVkZWwuIGh0dHBzOi8vYWVzdGhldGljLmNvbXB1dGVyCi8vIFJlY2lwZXM6IGZlZGFjL25hdGl2ZS9waWVjZXMvbm90ZXBhdC5tanMgKHRyaWFuZ2xlIFNoaWZ0IGFsdGVybmF0ZSBhbmQgY2hvcmRzKS4KLy8gVXNlcyBXZWIgQXVkaW8ncyBiYW5kLWxpbWl0ZWQgb3NjaWxsYXRvcnM7IHRoaXMgaXMgbm90IGEgcmVjb3JkaW5nIG9mIHRoZQovLyBuYXRpdmUgbWl4ZXIsIGl0cyBlY2hvLCBvciBpdHMgb3V0cHV0IHNhdHVyYXRpb24uCi8vIEdsYXNzLCBzd2FybSwgYmxvb20sIGFuZCBvcmJpdCBleHRlbmQgdGhvc2UgcmVjaXBlcyBhcyBuZXcgU3RydWRlbCBzb3VuZHMuCgpjb25zdCB2b2ljZXMgPSBPYmplY3QuZnJlZXplKHsKICBhY190cmlhbmdsZTogeyB3YXZlOiAndHJpYW5nbGUnLCByYXRpb3M6IFsxXSwgbGV2ZWxzOiBbMC41XSB9LAogIGFjX3RyaWFuZ2xlX2ZpZnRoOiB7IHdhdmU6ICd0cmlhbmdsZScsIHJhdGlvczogWzEsIDEuNV0sIGxldmVsczogWzAuNDIsIDAuMTJdIH0sCiAgYWNfc2luZTogeyB3YXZlOiAnc2luZScsIHJhdGlvczogWzFdLCBsZXZlbHM6IFswLjVdIH0sCiAgYWNfbWFqb3I6IHsgd2F2ZTogJ3NpbmUnLCByYXRpb3M6IFsxLCAyICoqICg0IC8gMTIpLCAyICoqICg3IC8gMTIpXSB9LAogIGFjX21pbm9yOiB7IHdhdmU6ICdzaW5lJywgcmF0aW9zOiBbMSwgMiAqKiAoMyAvIDEyKSwgMiAqKiAoNyAvIDEyKV0gfSwKICBhY19zdXMyOiB7IHdhdmU6ICdzaW5lJywgcmF0aW9zOiBbMSwgMiAqKiAoMiAvIDEyKSwgMiAqKiAoNyAvIDEyKV0gfSwKICBhY19nbGFzczogeyByaWNoOiAnZ2xhc3MnIH0sCiAgYWNfc3dhcm06IHsgcmljaDogJ3N3YXJtJyB9LAogIGFjX2Jsb29tOiB7IHJpY2g6ICdibG9vbScgfSwKICBhY19vcmJpdDogeyByaWNoOiAnb3JiaXQnIH0sCiAgYWNfbWFyaW1iYTogeyBtb2RhbDogdHJ1ZSB9LAogIGFjX3N0b25lOiB7IG1vZGFsOiAnc3RvbmUnIH0sCiAgYWNfd2F0ZXI6IHsgY2hpcnA6ICd3YXRlcicgfSwKICBhY19raWNrOiB7IGNoaXJwOiAna2ljaycgfSwKfSk7Cgpjb25zdCBib3VuZGVkID0gKHZhbHVlLCBmYWxsYmFjaywgbWluLCBtYXgpID0+IHsKICBjb25zdCBuID0gTnVtYmVyKHZhbHVlID8/IGZhbGxiYWNrKTsKICByZXR1cm4gTnVtYmVyLmlzRmluaXRlKG4pID8gTWF0aC5tYXgobWluLCBNYXRoLm1pbihtYXgsIG4pKSA6IGZhbGxiYWNrOwp9OwoKZnVuY3Rpb24gbWFyaW1iYVZvaWNlKGN0eCwgdGltZSwgdmFsdWUsIGZyZXF1ZW5jeSwgb25lbmRlZCwgc3RvbmUgPSBmYWxzZSkgewogIC8vIE1vZGFsIHJlZHVjdGlvbiBvZiBwb3AvbWFyaW1iYS9zeW50aHMvbWFyaW1iYS5tanMsIHJvc2V3b29kIHByZXNldC4KICAvLyBQcmVzZXJ2ZXMgaXRzIDE6NDo5LjIgcmF0aW9zIGFuZCBUNjAgZGVjYXkgcmVsYXRpb25zaGlwczsgb21pdHMgdGhlCiAgLy8gc291cmNlIHJlbmRlcmVyJ3MgbWFsbGV0IGNvbnZvbHV0aW9uIGFuZCByZXNvbmF0b3IgdHViZSBtb2RlbC4KICBjb25zdCBjb2xvciA9IGJvdW5kZWQodmFsdWUubiwgMC41LCAwLCAxKTsKICBjb25zdCBkdXJhdGlvbiA9IGJvdW5kZWQodmFsdWUuZHVyYXRpb24sIDAuNSwgMC4wMDEsIDM2MDApOwogIGNvbnN0IGF0dGFjayA9IE1hdGgubWluKGJvdW5kZWQodmFsdWUuYXR0YWNrLCAwLjAwMSwgMCwgNjApLCBkdXJhdGlvbik7CiAgY29uc3QgZGVjYXkgPSBNYXRoLm1pbihib3VuZGVkKHZhbHVlLmRlY2F5LCAwLCAwLCA2MCksIGR1cmF0aW9uIC0gYXR0YWNrKTsKICBjb25zdCBzdXN0YWluID0gYm91bmRlZCh2YWx1ZS5zdXN0YWluLCAxLCAwLCAxKTsKICBjb25zdCByZWxlYXNlID0gYm91bmRlZCh2YWx1ZS5yZWxlYXNlLCAwLjI1LCAwLCA2MCk7CiAgY29uc3QgZW5kID0gdGltZSArIGR1cmF0aW9uICsgcmVsZWFzZSArIDAuMDA1OwogIGNvbnN0IG5vZGUgPSBjdHguY3JlYXRlR2FpbigpOwogIG5vZGUuZ2Fpbi5zZXRWYWx1ZUF0VGltZShhdHRhY2sgPyAwIDogMSwgdGltZSk7CiAgaWYgKGF0dGFjaykgbm9kZS5nYWluLmxpbmVhclJhbXBUb1ZhbHVlQXRUaW1lKDEsIHRpbWUgKyBhdHRhY2spOwogIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZShzdXN0YWluLCB0aW1lICsgYXR0YWNrICsgZGVjYXkpOwogIG5vZGUuZ2Fpbi5zZXRWYWx1ZUF0VGltZShzdXN0YWluLCB0aW1lICsgZHVyYXRpb24pOwogIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZSgwLCBlbmQgLSAwLjAwNSk7CiAgLy8gU3RvbmUgaXMgYSBkZXNpZ25lZCBpbmhhcm1vbmljIGJhciBzdXJyb2dhdGUsIG5vdCB0aGUgb2ZmbGluZSBGRU0gbW9kZWwuCiAgY29uc3QgcmF0aW9zID0gc3RvbmUgPyBbMSwgMi43NTYsIDUuNDA0XSA6IFsxLCA0LCA5LjJdOwogIGNvbnN0IHQ2MCA9IHN0b25lID8gWzIuOCwgMS4xLCAwLjM4XSA6IFsxLjYsIDAuMzIsIDAuMDldOwogIGNvbnN0IGxldmVscyA9IFsxLCAwLjMyICogKDAuMyArIGNvbG9yICogMS40KSwgMC4xICogKDAuMyArIGNvbG9yICogMS40KV07CiAgY29uc3Qgc2NhbGUgPSAwLjQ4IC8gbGV2ZWxzLnJlZHVjZSgoYSwgYikgPT4gYSArIGIsIDApOwogIGNvbnN0IG9zY2lsbGF0b3JzID0gW10sIGdhaW5zID0gW107CiAgbGV0IHJlbWFpbmluZyA9IDMsIGZpbmlzaGVkID0gZmFsc2U7CiAgcmF0aW9zLmZvckVhY2goKHJhdGlvLCBpKSA9PiB7CiAgICBjb25zdCBvID0gY3R4LmNyZWF0ZU9zY2lsbGF0b3IoKSwgZyA9IGN0eC5jcmVhdGVHYWluKCk7CiAgICBvLmZyZXF1ZW5jeS52YWx1ZSA9IE1hdGgubWluKGZyZXF1ZW5jeSAqIHJhdGlvLCBjdHguc2FtcGxlUmF0ZSAqIDAuNDUpOwogICAgY29uc3QgYW1wbGl0dWRlID0gbGV2ZWxzW2ldICogc2NhbGU7CiAgICBjb25zdCByaW5nID0gdDYwW2ldICogKDAuOCArIGNvbG9yICogMC44KTsKICAgIGcuZ2Fpbi5zZXRWYWx1ZUF0VGltZShhbXBsaXR1ZGUsIHRpbWUpOwogICAgZy5nYWluLmV4cG9uZW50aWFsUmFtcFRvVmFsdWVBdFRpbWUoYW1wbGl0dWRlICogMC4wMDEsIHRpbWUgKyByaW5nKTsKICAgIGcuZ2Fpbi5leHBvbmVudGlhbFJhbXBUb1ZhbHVlQXRUaW1lKDAuMDAwMDAwMSwgdGltZSArIHJpbmcgKiAyKTsKICAgIG8uY29ubmVjdChnKS5jb25uZWN0KG5vZGUpOwogICAgby5vbmVuZGVkID0gKCkgPT4gewogICAgICBpZiAoLS1yZW1haW5pbmcgPT09IDAgJiYgIWZpbmlzaGVkKSB7CiAgICAgICAgZmluaXNoZWQgPSB0cnVlOwogICAgICAgIFsuLi5vc2NpbGxhdG9ycywgLi4uZ2FpbnMsIG5vZGVdLmZvckVhY2gobiA9PiBuLmRpc2Nvbm5lY3QoKSk7CiAgICAgICAgb25lbmRlZD8uKCk7CiAgICAgIH0KICAgIH07CiAgICBvc2NpbGxhdG9ycy5wdXNoKG8pOyBnYWlucy5wdXNoKGcpOwogICAgby5zdGFydCh0aW1lKTsgby5zdG9wKGVuZCk7CiAgfSk7CiAgcmV0dXJuIHsgbm9kZSwgc3RvcChhdCkgewogICAgaWYgKGZpbmlzaGVkKSByZXR1cm47CiAgICBjb25zdCBzdG9wQXQgPSBNYXRoLm1heChjdHguY3VycmVudFRpbWUsIE1hdGgubWluKGF0LCBlbmQpKTsKICAgIG5vZGUuZ2Fpbi5jYW5jZWxBbmRIb2xkQXRUaW1lKHN0b3BBdCk7CiAgICBub2RlLmdhaW4ubGluZWFyUmFtcFRvVmFsdWVBdFRpbWUoMCwgc3RvcEF0ICsgMC4wMDUpOwogICAgb3NjaWxsYXRvcnMuZm9yRWFjaChvID0+IG8uc3RvcChNYXRoLm1pbihlbmQsIHN0b3BBdCArIDAuMDA1KSkpOwogIH0gfTsKfQoKZnVuY3Rpb24gY2hpcnBWb2ljZShjdHgsIHRpbWUsIHZhbHVlLCBmcmVxdWVuY3ksIGtpbmQsIG9uZW5kZWQpIHsKICBjb25zdCBjb2xvciA9IGJvdW5kZWQodmFsdWUubiwgMC41LCAwLCAxKTsKICBjb25zdCBraWNrID0ga2luZCA9PT0gJ2tpY2snOwogIGNvbnN0IGR1cmF0aW9uID0gYm91bmRlZCh2YWx1ZS5kdXJhdGlvbiwgMC4yLCAwLjAwMSwgMzYwMCk7CiAgY29uc3QgcmVsZWFzZSA9IGJvdW5kZWQodmFsdWUucmVsZWFzZSwgMC4wOCwgMCwgNjApOwogIGNvbnN0IGVuZCA9IHRpbWUgKyBkdXJhdGlvbiArIHJlbGVhc2UgKyAwLjAwNTsKICBjb25zdCBub2RlID0gY3R4LmNyZWF0ZUdhaW4oKSwgb3NjaWxsYXRvciA9IGN0eC5jcmVhdGVPc2NpbGxhdG9yKCk7CiAgY29uc3QgYXR0YWNrID0gTWF0aC5taW4oYm91bmRlZCh2YWx1ZS5hdHRhY2ssIGtpY2sgPyAwLjAwNyA6IDAuMDAzLCAwLCA2MCksIGR1cmF0aW9uKTsKICBjb25zdCByaW5nID0ga2ljayA/IDAuMTIgKyBjb2xvciAqIDAuMiA6IDAuMDU1ICsgY29sb3IgKiAwLjE7CiAgbm9kZS5nYWluLnNldFZhbHVlQXRUaW1lKGF0dGFjayA/IDAgOiAwLjUsIHRpbWUpOwogIGlmIChhdHRhY2spIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZSgwLjUsIHRpbWUgKyBhdHRhY2spOwogIG5vZGUuZ2Fpbi5leHBvbmVudGlhbFJhbXBUb1ZhbHVlQXRUaW1lKDAuMDAwMDEsIE1hdGgubWluKGVuZCAtIDAuMDA1LCB0aW1lICsgYXR0YWNrICsgcmluZykpOwogIG5vZGUuZ2Fpbi5zZXRWYWx1ZUF0VGltZSgwLCBlbmQpOwogIG9zY2lsbGF0b3IuZnJlcXVlbmN5LnNldFZhbHVlQXRUaW1lKGtpY2sgPyAxMjYgKyBjb2xvciAqIDIwIDogZnJlcXVlbmN5ICogMC41NSwgdGltZSk7CiAgb3NjaWxsYXRvci5mcmVxdWVuY3kuZXhwb25lbnRpYWxSYW1wVG9WYWx1ZUF0VGltZShraWNrID8gNDMgOiBmcmVxdWVuY3ksCiAgICB0aW1lICsgTWF0aC5taW4oZHVyYXRpb24sIGtpY2sgPyAwLjA1NSArIGNvbG9yICogMC4wNCA6IDAuMDcpKTsKICBvc2NpbGxhdG9yLmNvbm5lY3Qobm9kZSk7CiAgbGV0IGZpbmlzaGVkID0gZmFsc2U7CiAgb3NjaWxsYXRvci5vbmVuZGVkID0gKCkgPT4gewogICAgaWYgKGZpbmlzaGVkKSByZXR1cm47CiAgICBmaW5pc2hlZCA9IHRydWU7IG9zY2lsbGF0b3IuZGlzY29ubmVjdCgpOyBub2RlLmRpc2Nvbm5lY3QoKTsgb25lbmRlZD8uKCk7CiAgfTsKICBvc2NpbGxhdG9yLnN0YXJ0KHRpbWUpOyBvc2NpbGxhdG9yLnN0b3AoZW5kKTsKICByZXR1cm4geyBub2RlLCBzdG9wKGF0KSB7CiAgICBpZiAoZmluaXNoZWQpIHJldHVybjsKICAgIGNvbnN0IHN0b3BBdCA9IE1hdGgubWF4KGN0eC5jdXJyZW50VGltZSwgTWF0aC5taW4oYXQsIGVuZCkpOwogICAgbm9kZS5nYWluLmNhbmNlbEFuZEhvbGRBdFRpbWUoc3RvcEF0KTsKICAgIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZSgwLCBzdG9wQXQgKyAwLjAwNSk7CiAgICBvc2NpbGxhdG9yLnN0b3AoTWF0aC5taW4oZW5kLCBzdG9wQXQgKyAwLjAwNSkpOwogIH0gfTsKfQoKZnVuY3Rpb24gcmljaFZvaWNlKGN0eCwgdGltZSwgdmFsdWUsIGZyZXF1ZW5jeSwga2luZCwgb25lbmRlZCwgY3BzID0gMC41KSB7CiAgLy8gbiBpcyBhIDAuLjEgdGltYnJlIG1hY3JvOiBGTSBkZXB0aCwgZGV0dW5lLCBhbmQgZmlsdGVyIG1vdGlvbi4KICBjb25zdCBjb2xvciA9IGJvdW5kZWQodmFsdWUubiwgMC41LCAwLCAxKTsKICBjb25zdCBkdXJhdGlvbiA9IGJvdW5kZWQodmFsdWUuZHVyYXRpb24sIDAuNSwgMC4wMDEsIDM2MDApOwogIGNvbnN0IGF0dGFjayA9IE1hdGgubWluKGJvdW5kZWQodmFsdWUuYXR0YWNrLCBraW5kID09PSAnYmxvb20nID8gMC4xOCA6IDAuMDA2LCAwLCA2MCksIGR1cmF0aW9uKTsKICBjb25zdCBkZWNheSA9IE1hdGgubWluKGJvdW5kZWQodmFsdWUuZGVjYXksIGtpbmQgPT09ICdnbGFzcycgPyAwLjMgOiAwLjEsIDAsIDYwKSwgZHVyYXRpb24gLSBhdHRhY2spOwogIGNvbnN0IHN1c3RhaW4gPSBib3VuZGVkKHZhbHVlLnN1c3RhaW4sIGtpbmQgPT09ICdnbGFzcycgPyAwLjI0IDogMC44LCAwLCAxKTsKICBjb25zdCByZWxlYXNlID0gYm91bmRlZCh2YWx1ZS5yZWxlYXNlLCBraW5kID09PSAnZ2xhc3MnID8gMC4zNSA6IDAuNiwgMCwgNjApOwogIGNvbnN0IGhvbGRFbmQgPSB0aW1lICsgZHVyYXRpb247CiAgY29uc3QgZW5kID0gaG9sZEVuZCArIHJlbGVhc2UgKyAwLjAwNTsKICBjb25zdCBub2RlID0gY3R4LmNyZWF0ZUdhaW4oKTsKICBjb25zdCBhbGxOb2RlcyA9IFtub2RlXSwgb3NjaWxsYXRvcnMgPSBbXTsKICBjb25zdCBrZWVwID0gb2JqZWN0ID0+IHsgYWxsTm9kZXMucHVzaChvYmplY3QpOyByZXR1cm4gb2JqZWN0OyB9OwogIGNvbnN0IGdhaW4gPSBsZXZlbCA9PiB7CiAgICBjb25zdCBnID0ga2VlcChjdHguY3JlYXRlR2FpbigpKTsgZy5nYWluLnZhbHVlID0gbGV2ZWw7IHJldHVybiBnOwogIH07CiAgY29uc3Qgb3NjID0gKHR5cGUsIGh6KSA9PiB7CiAgICBjb25zdCBvID0ga2VlcChjdHguY3JlYXRlT3NjaWxsYXRvcigpKTsKICAgIG8udHlwZSA9IHR5cGU7CiAgICBvLmZyZXF1ZW5jeS52YWx1ZSA9IE1hdGgubWF4KDAuMDUsIE1hdGgubWluKGh6LCBjdHguc2FtcGxlUmF0ZSAqIDAuNCkpOwogICAgb3NjaWxsYXRvcnMucHVzaChvKTsgcmV0dXJuIG87CiAgfTsKICBjb25zdCBmaWx0ZXIgPSBrZWVwKGN0eC5jcmVhdGVCaXF1YWRGaWx0ZXIoKSk7CiAgZmlsdGVyLnR5cGUgPSAnbG93cGFzcyc7CiAgZmlsdGVyLlEudmFsdWUgPSAwLjU7CiAgY29uc3QgY3V0b2ZmID0gTWF0aC5taW4oY3R4LnNhbXBsZVJhdGUgKiAwLjQsIGZyZXF1ZW5jeSAqICg1ICsgY29sb3IgKiAxNSkpOwogIGZpbHRlci5mcmVxdWVuY3kuc2V0VmFsdWVBdFRpbWUoY3V0b2ZmLCB0aW1lKTsKICBmaWx0ZXIuZnJlcXVlbmN5LmV4cG9uZW50aWFsUmFtcFRvVmFsdWVBdFRpbWUoCiAgICBNYXRoLm1heCg2MCwgY3V0b2ZmICogKGtpbmQgPT09ICdnbGFzcycgPyAwLjI0IDogMC41NSkpLCBob2xkRW5kLAogICk7CiAgZmlsdGVyLmNvbm5lY3Qobm9kZSk7CiAgbm9kZS5nYWluLnNldFZhbHVlQXRUaW1lKGF0dGFjayA/IDAgOiAxLCB0aW1lKTsKICBpZiAoYXR0YWNrKSBub2RlLmdhaW4ubGluZWFyUmFtcFRvVmFsdWVBdFRpbWUoMSwgdGltZSArIGF0dGFjayk7CiAgbm9kZS5nYWluLmxpbmVhclJhbXBUb1ZhbHVlQXRUaW1lKHN1c3RhaW4sIHRpbWUgKyBhdHRhY2sgKyBkZWNheSk7CiAgbm9kZS5nYWluLnNldFZhbHVlQXRUaW1lKHN1c3RhaW4sIGhvbGRFbmQpOwogIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZSgwLCBob2xkRW5kICsgcmVsZWFzZSk7CgogIGNvbnN0IGxheWVycyA9IGtpbmQgPT09ICdnbGFzcycKICAgID8gW1sxLCAwLjI0LCAnc2luZScsIDBdLCBbMS41LCAwLjEwLCAndHJpYW5nbGUnLCAwXSwgWzIuNzU2LCAwLjA3LCAnc2luZScsIDBdXQogICAgOiBraW5kID09PSAnc3dhcm0nCiAgICAgID8gW1sxLCAwLjEyLCAnc2F3dG9vdGgnLCAtMV0sIFsxLCAwLjEyLCAnc2F3dG9vdGgnLCAxXSwgWzEuNSwgMC4wOSwgJ3RyaWFuZ2xlJywgMC40XSwgWzAuNSwgMC4xMCwgJ3NpbmUnLCAwXV0KICAgICAgOiBbWzEsIDAuMTUsICd0cmlhbmdsZScsIC0xXSwgWzEsIDAuMTIsICd0cmlhbmdsZScsIDFdLCBbMS41LCAwLjA5LCAnc2luZScsIC0wLjVdLCBbMiwgMC4wNiwgJ3NpbmUnLCAwLjVdXTsKICBsYXllcnMuZm9yRWFjaCgoW3JhdGlvLCBsZXZlbCwgd2F2ZSwgc3ByZWFkXSwgaSkgPT4gewogICAgY29uc3QgY2FycmllciA9IG9zYyh3YXZlLCBmcmVxdWVuY3kgKiByYXRpbyk7CiAgICBjYXJyaWVyLmRldHVuZS52YWx1ZSA9IHNwcmVhZCAqICg0ICsgY29sb3IgKiAxOCk7CiAgICBjb25zdCBwYW4gPSBrZWVwKGN0eC5jcmVhdGVTdGVyZW9QYW5uZXIoKSk7CiAgICBwYW4ucGFuLnZhbHVlID0gc3ByZWFkICogMC40NTsKICAgIGNvbnN0IHB1bHNlID0gZ2FpbihraW5kID09PSAnb3JiaXQnID8gMC43MiA6IDEpOwogICAgY2Fycmllci5jb25uZWN0KGdhaW4obGV2ZWwpKS5jb25uZWN0KHB1bHNlKS5jb25uZWN0KHBhbikuY29ubmVjdChmaWx0ZXIpOwogICAgaWYgKGtpbmQgPT09ICdvcmJpdCcpIHsKICAgICAgLy8gQW1wbGl0dWRlIHB1bHNlcyBsb2NrZWQgdG8gdGhlIHNjaGVkdWxlcidzIGN5Y2xlIHJhdGUuCiAgICAgIG9zYygnc2luZScsIGJvdW5kZWQoY3BzLCAwLjUsIDAuMDEsIDIwKSAqIChpICsgMikpCiAgICAgICAgLmNvbm5lY3QoZ2FpbigwLjI4KSkuY29ubmVjdChwdWxzZS5nYWluKTsKICAgIH0KICAgIGlmIChraW5kICE9PSAnc3dhcm0nKSB7CiAgICAgIGNvbnN0IG1vZCA9IG9zYygnc2luZScsIGZyZXF1ZW5jeSAqIHJhdGlvICogKGtpbmQgPT09ICdnbGFzcycgPyAyLjc1NiA6IDEuNSkpOwogICAgICBjb25zdCBkZXB0aCA9IGdhaW4oZnJlcXVlbmN5ICogcmF0aW8gKiAoMC4wOCArIGNvbG9yICogKGtpbmQgPT09ICdnbGFzcycgPyAyLjQgOiAwLjYpKSk7CiAgICAgIG1vZC5jb25uZWN0KGRlcHRoKS5jb25uZWN0KGNhcnJpZXIuZnJlcXVlbmN5KTsKICAgICAgZGVwdGguZ2Fpbi5zZXRWYWx1ZUF0VGltZShkZXB0aC5nYWluLnZhbHVlLCB0aW1lKTsKICAgICAgZGVwdGguZ2Fpbi5leHBvbmVudGlhbFJhbXBUb1ZhbHVlQXRUaW1lKAogICAgICAgIE1hdGgubWF4KDAuMDAxLCBkZXB0aC5nYWluLnZhbHVlICogMC4wOCksIHRpbWUgKyBNYXRoLm1pbihkdXJhdGlvbiwgMC4zICsgY29sb3IpLAogICAgICApOwogICAgICBpZiAoa2luZCA9PT0gJ29yYml0JykgewogICAgICAgIC8vIENhcnJpZXIgLT4gZGVsYXkgLT4gbW9kdWxhdG9yIHBpdGNoIC0+IGNhcnJpZXIgcGl0Y2guIFRoZSBkZWxheQogICAgICAgIC8vIG1ha2VzIHRoZSBmZWVkYmFjayBjeWNsZSB2YWxpZCwgd2hpbGUgc2luZSBvdXRwdXRzIGJvdW5kIGl0cyBsZXZlbC4KICAgICAgICBjb25zdCBmZWVkYmFjayA9IGtlZXAoY3R4LmNyZWF0ZURlbGF5KDAuMDUpKTsKICAgICAgICBmZWVkYmFjay5kZWxheVRpbWUudmFsdWUgPSAwLjAwNCArIGkgKiAwLjAwMjsKICAgICAgICBjYXJyaWVyLmNvbm5lY3QoZmVlZGJhY2spLmNvbm5lY3QoZ2FpbihmcmVxdWVuY3kgKiAoMC4xNSArIGNvbG9yICogMS40KSkpCiAgICAgICAgICAuY29ubmVjdChtb2QuZnJlcXVlbmN5KTsKICAgICAgICBjb25zdCBzaGFwZSA9IG9zYygnc2luZScsIDAuMTMgKyBpICogMC4wNyk7CiAgICAgICAgc2hhcGUuY29ubmVjdChnYWluKGZyZXF1ZW5jeSAqIHJhdGlvICogY29sb3IgKiAwLjI0KSkuY29ubmVjdChkZXB0aC5nYWluKTsKICAgICAgfQogICAgfQogICAgY29uc3QgZHJpZnQgPSBvc2MoJ3NpbmUnLCAwLjE5ICsgaSAqIDAuMTEgKyBjb2xvciAqIDAuMzUpOwogICAgZHJpZnQuY29ubmVjdChnYWluKDIgKyBjb2xvciAqIDYpKS5jb25uZWN0KGNhcnJpZXIuZGV0dW5lKTsKICAgIGlmIChraW5kID09PSAnYmxvb20nIHx8IGtpbmQgPT09ICdvcmJpdCcpIGRyaWZ0LmNvbm5lY3QoZ2FpbigwLjMpKS5jb25uZWN0KHBhbi5wYW4pOwogIH0pOwogIGxldCByZW1haW5pbmcgPSBvc2NpbGxhdG9ycy5sZW5ndGgsIGZpbmlzaGVkID0gZmFsc2U7CiAgZm9yIChjb25zdCBvc2NpbGxhdG9yIG9mIG9zY2lsbGF0b3JzKSB7CiAgICBvc2NpbGxhdG9yLm9uZW5kZWQgPSAoKSA9PiB7CiAgICAgIGlmICgtLXJlbWFpbmluZyA9PT0gMCAmJiAhZmluaXNoZWQpIHsKICAgICAgICBmaW5pc2hlZCA9IHRydWU7CiAgICAgICAgYWxsTm9kZXMuZm9yRWFjaChuID0+IG4uZGlzY29ubmVjdCgpKTsKICAgICAgICBvbmVuZGVkPy4oKTsKICAgICAgfQogICAgfTsKICAgIG9zY2lsbGF0b3Iuc3RhcnQodGltZSk7CiAgICBvc2NpbGxhdG9yLnN0b3AoZW5kKTsKICB9CiAgcmV0dXJuIHsKICAgIG5vZGUsCiAgICBzdG9wKGF0KSB7CiAgICAgIGlmIChmaW5pc2hlZCkgcmV0dXJuOwogICAgICBjb25zdCBzdG9wQXQgPSBNYXRoLm1heChjdHguY3VycmVudFRpbWUsIE1hdGgubWluKGF0LCBlbmQpKTsKICAgICAgbm9kZS5nYWluLmNhbmNlbEFuZEhvbGRBdFRpbWUoc3RvcEF0KTsKICAgICAgbm9kZS5nYWluLmxpbmVhclJhbXBUb1ZhbHVlQXRUaW1lKDAsIHN0b3BBdCArIDAuMDA1KTsKICAgICAgZm9yIChjb25zdCBvIG9mIG9zY2lsbGF0b3JzKSBvLnN0b3AoTWF0aC5taW4oZW5kLCBzdG9wQXQgKyAwLjAwNSkpOwogICAgfSwKICB9Owp9CgpmdW5jdGlvbiByZWdpc3Rlck5vdGVwYXQoYXBpID0gZ2xvYmFsVGhpcykgewogIGNvbnN0IHsgcmVnaXN0ZXJTb3VuZCwgZ2V0QXVkaW9Db250ZXh0LCBnZXRGcmVxdWVuY3lGcm9tVmFsdWUgfSA9IGFwaTsKICBpZiAoIVtyZWdpc3RlclNvdW5kLCBnZXRBdWRpb0NvbnRleHQsIGdldEZyZXF1ZW5jeUZyb21WYWx1ZV0uZXZlcnkoZm4gPT4gdHlwZW9mIGZuID09PSAnZnVuY3Rpb24nKSkgewogICAgdGhyb3cgbmV3IEVycm9yKCdJbXBvcnQgTm90ZXBhdCBpbnNpZGUgU3RydWRlbCwgb3IgcGFzcyBpdHMgYXVkaW8gQVBJIHRvIHJlZ2lzdGVyTm90ZXBhdCgpLicpOwogIH0KICBmb3IgKGNvbnN0IFtuYW1lLCByZWNpcGVdIG9mIE9iamVjdC5lbnRyaWVzKHZvaWNlcykpIHsKICAgIHJlZ2lzdGVyU291bmQobmFtZSwgKHRpbWUsIHZhbHVlLCBvbmVuZGVkLCBjcHMpID0+IHsKICAgICAgY29uc3QgY3R4ID0gZ2V0QXVkaW9Db250ZXh0KCk7CiAgICAgIGNvbnN0IGZyZXF1ZW5jeSA9IGJvdW5kZWQoZ2V0RnJlcXVlbmN5RnJvbVZhbHVlKHZhbHVlKSwgMjIwLCAxLCBjdHguc2FtcGxlUmF0ZSAvIDIgLSAxKTsKICAgICAgaWYgKHJlY2lwZS5tb2RhbCkgcmV0dXJuIG1hcmltYmFWb2ljZShjdHgsIHRpbWUsIHZhbHVlLCBmcmVxdWVuY3ksIG9uZW5kZWQsIHJlY2lwZS5tb2RhbCA9PT0gJ3N0b25lJyk7CiAgICAgIGlmIChyZWNpcGUuY2hpcnApIHJldHVybiBjaGlycFZvaWNlKGN0eCwgdGltZSwgdmFsdWUsIGZyZXF1ZW5jeSwgcmVjaXBlLmNoaXJwLCBvbmVuZGVkKTsKICAgICAgaWYgKHJlY2lwZS5yaWNoKSByZXR1cm4gcmljaFZvaWNlKGN0eCwgdGltZSwgdmFsdWUsIGZyZXF1ZW5jeSwgcmVjaXBlLnJpY2gsIG9uZW5kZWQsIGNwcyk7CiAgICAgIGNvbnN0IGR1cmF0aW9uID0gYm91bmRlZCh2YWx1ZS5kdXJhdGlvbiwgMC41LCAwLjAwMSwgMzYwMCk7CiAgICAgIGNvbnN0IGF0dGFjayA9IE1hdGgubWluKGJvdW5kZWQodmFsdWUuYXR0YWNrLCAwLjAyLCAwLCA2MCksIGR1cmF0aW9uKTsKICAgICAgY29uc3QgZGVjYXkgPSBNYXRoLm1pbihib3VuZGVkKHZhbHVlLmRlY2F5LCAwLCAwLCA2MCksIGR1cmF0aW9uIC0gYXR0YWNrKTsKICAgICAgY29uc3Qgc3VzdGFpbiA9IGJvdW5kZWQodmFsdWUuc3VzdGFpbiwgMSwgMCwgMSk7CiAgICAgIGNvbnN0IHJlbGVhc2UgPSBib3VuZGVkKHZhbHVlLnJlbGVhc2UsIDAuMSwgMCwgNjApOwogICAgICBjb25zdCBob2xkRW5kID0gdGltZSArIGR1cmF0aW9uOwogICAgICBjb25zdCBlbmQgPSBob2xkRW5kICsgcmVsZWFzZSArIDAuMDA1OwogICAgICBjb25zdCBub2RlID0gY3R4LmNyZWF0ZUdhaW4oKTsKICAgICAgbm9kZS5nYWluLnNldFZhbHVlQXRUaW1lKGF0dGFjayA/IDAgOiAxLCB0aW1lKTsKICAgICAgaWYgKGF0dGFjaykgbm9kZS5nYWluLmxpbmVhclJhbXBUb1ZhbHVlQXRUaW1lKDEsIHRpbWUgKyBhdHRhY2spOwogICAgICBub2RlLmdhaW4ubGluZWFyUmFtcFRvVmFsdWVBdFRpbWUoc3VzdGFpbiwgdGltZSArIGF0dGFjayArIGRlY2F5KTsKICAgICAgbm9kZS5nYWluLnNldFZhbHVlQXRUaW1lKHN1c3RhaW4sIGhvbGRFbmQpOwogICAgICBub2RlLmdhaW4ubGluZWFyUmFtcFRvVmFsdWVBdFRpbWUoMCwgaG9sZEVuZCArIHJlbGVhc2UpOwoKICAgICAgY29uc3Qgb3NjaWxsYXRvcnMgPSBbXTsKICAgICAgbGV0IHJlbWFpbmluZyA9IHJlY2lwZS5yYXRpb3MubGVuZ3RoOwogICAgICBsZXQgZmluaXNoZWQgPSBmYWxzZTsKICAgICAgZm9yIChsZXQgaSA9IDA7IGkgPCByZWNpcGUucmF0aW9zLmxlbmd0aDsgaSsrKSB7CiAgICAgICAgY29uc3Qgb3NjaWxsYXRvciA9IGN0eC5jcmVhdGVPc2NpbGxhdG9yKCk7CiAgICAgICAgb3NjaWxsYXRvci50eXBlID0gcmVjaXBlLndhdmU7CiAgICAgICAgb3NjaWxsYXRvci5mcmVxdWVuY3kudmFsdWUgPSBNYXRoLm1pbihmcmVxdWVuY3kgKiByZWNpcGUucmF0aW9zW2ldLCBjdHguc2FtcGxlUmF0ZSAvIDIgLSAxKTsKICAgICAgICBjb25zdCBnYWluID0gY3R4LmNyZWF0ZUdhaW4oKTsKICAgICAgICAvLyBQcmVzZXJ2ZSB0aGUgbmF0aXZlIHRyaWFuZ2xlIGxheWVyIGJhbGFuY2UuIENob3JkcyBzaGFyZSBoZWFkcm9vbS4KICAgICAgICBnYWluLmdhaW4udmFsdWUgPSAocmVjaXBlLmxldmVscz8uW2ldID8/IDAuNSAvIHJlY2lwZS5yYXRpb3MubGVuZ3RoKQogICAgICAgICAgKiAocmVjaXBlLndhdmUgPT09ICd0cmlhbmdsZScgPyAtMSA6IDEpOwogICAgICAgIG9zY2lsbGF0b3IuY29ubmVjdChnYWluKS5jb25uZWN0KG5vZGUpOwogICAgICAgIG9zY2lsbGF0b3Iub25lbmRlZCA9ICgpID0+IHsKICAgICAgICAgIG9zY2lsbGF0b3IuZGlzY29ubmVjdCgpOwogICAgICAgICAgZ2Fpbi5kaXNjb25uZWN0KCk7CiAgICAgICAgICBpZiAoLS1yZW1haW5pbmcgPT09IDAgJiYgIWZpbmlzaGVkKSB7CiAgICAgICAgICAgIGZpbmlzaGVkID0gdHJ1ZTsKICAgICAgICAgICAgbm9kZS5kaXNjb25uZWN0KCk7CiAgICAgICAgICAgIG9uZW5kZWQ/LigpOwogICAgICAgICAgfQogICAgICAgIH07CiAgICAgICAgb3NjaWxsYXRvci5zdGFydCh0aW1lKTsKICAgICAgICBvc2NpbGxhdG9yLnN0b3AoZW5kKTsKICAgICAgICBvc2NpbGxhdG9ycy5wdXNoKG9zY2lsbGF0b3IpOwogICAgICB9CiAgICAgIHJldHVybiB7CiAgICAgICAgbm9kZSwKICAgICAgICAvLyBTdHJ1ZGVsIGNhbGxzIHRoaXMgd2hlbiByZXRpcmluZyBhIHZvaWNlLCBpbmNsdWRpbmcgcG9seXBob255IHN0ZWFscy4KICAgICAgICBzdG9wKGF0KSB7CiAgICAgICAgICBpZiAoZmluaXNoZWQpIHJldHVybjsKICAgICAgICAgIGNvbnN0IHN0b3BBdCA9IE1hdGgubWF4KGN0eC5jdXJyZW50VGltZSwgTWF0aC5taW4oYXQsIGVuZCkpOwogICAgICAgICAgbm9kZS5nYWluLmNhbmNlbEFuZEhvbGRBdFRpbWUoc3RvcEF0KTsKICAgICAgICAgIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZSgwLCBzdG9wQXQgKyAwLjAwNSk7CiAgICAgICAgICBmb3IgKGNvbnN0IG9zY2lsbGF0b3Igb2Ygb3NjaWxsYXRvcnMpIG9zY2lsbGF0b3Iuc3RvcChNYXRoLm1pbihlbmQsIHN0b3BBdCArIDAuMDA1KSk7CiAgICAgICAgfSwKICAgICAgfTsKICAgIH0sIHsgdHlwZTogJ3N5bnRoJyB9KTsKICB9CiAgcmV0dXJuIE9iamVjdC5rZXlzKHZvaWNlcyk7Cn0KCnJlZ2lzdGVyTm90ZXBhdCgpOwoKfSkoKQoKLy8gTUFSSU1CQUJBIC8gT1JCSVQgUkVNSVgg4oCUIEBqZWZmcmV5IC8gYWVzdGhldGljLmNvbXB1dGVyCi8vIGh0dHBzOi8vcGF0LmFlc3RoZXRpYy5jb21wdXRlci9tYXJpbWJhYmEtb3JiaXQKLy8gTGVhZDogcG9wL21hcmltYmEvbWFyaW1iYWJhLm5wIOKAlCBhbGwgMjQgYmFycywgb3JpZ2luYWwgcGl0Y2hlcyBhbmQgZHVyYXRpb25zLgovLyBBY2NvbXBhbmltZW50OiBhIHJlZHVjZWQgYXJyYW5nZW1lbnQgaW5mb3JtZWQgYnkgYmluL3JlbmRlci1tYXJpbWJhYmEubWpzLgovLyBUaGlzIGlzIGFuIGVkaXRhYmxlIHNjb3JlIGFkYXB0YXRpb24sIG5vdCBwbGF5YmFjayBvZiB0aGUgcmVsZWFzZWQgbWFzdGVyLgoKLy8gVEVNUE8g4oCUIG9uZSBjeWNsZSA9IG9uZSAzLzQgYmFyLiA4NCBCUE0gcmVtaXg7IHNldCA1Ni8zIGZvciB0aGUgbHVsbGFieS4Kc2V0Y3BtKDg0LzMpCgovLyBNRUxPRFkg4oCUIEAgd2VpZ2h0cyBhcmUgYmVhdHMuIG1pbmkoLi4uKSBwYXJzZXMgdGhlIGFzc2VtYmxlZCBzdHJpbmdzLgovLyBGaXZlIHNlY3Rpb25zOiBodXNoLCB0d2lua2xlLCB3b3csIGJhLWJhLCBzbGVlcC4gVG90YWw6IDcyIGJlYXRzIC8gMjQgYmFycy4KY29uc3QgbWVsb2R5ID0gbWluaShbCiAgLy8gaHVzaDogNCBiYXJzCiAgJ2M1QDEgYTRAMSBmNEAxIGY0QDMgYTRAMS41IGc0QDEuNSBmNEAzJywKICAvLyB0d2lua2xlOiA2IGJhcnMKICAnZjVAMSBhNUAxIGM2QDAuNSBhNUAwLjUgZzVAMyBmNUAxIGc1QDEgYTVAMC41IGc1QDAuNSBmNUAzIGJiNUAxIGQ2QDEgYmI1QDEgYzZAMS41IGE1QDEuNScsCiAgLy8gd293OiA0IGJhcnMKICAnW2c1LGJiNV1ANiBhNUAxIGc1QDEgYTVAMSBmNUAzJywKICAvLyBiYWJhOiA0IGJhcnMKICAnYTVAMC41IGc1QDAuNSBhNUAxIGY1QDEgYzZAMC41IGJiNUAwLjUgYzZAMSBhNUAxIGc1QDEgZjVAMSBlNUAxIGY1QDMnLAogIC8vIHNsZWVwOiA2IGJhcnMKICAnYzVAMS41IGE0QDEuNSBnNEAxIGY0QDEgZjRAMSBhNEAxIGc0QDEgZjRAMSBmNEAzIGYzQDMgfkAzJywKXS5qb2luKCcgJykpLm5vdGUoKS5zbG93KDI0KQoKLy8gU0lOR0VSIOKAlCBhY19tYXJpbWJhIHVzZXMgdGhlIHNvdXJjZSByb3Nld29vZCByYXRpb3MgMTo0OjkuMi4KLy8gLm4oMC4uMSkgY2hhbmdlcyBtYWxsZXQgYnJpZ2h0bmVzcyBhbmQgcmluZzsgY2hhbmdlIC5zKC4uLikgdG8gcmVjYXN0IGl0LgokOiBtZWxvZHkucygnYWNfbWFyaW1iYScpLm4ocGVybGluLnNsb3coMTMpLnJhbmdlKC4zNSwuOSkpCiAgLmF0dGFjayguMDAxKS5yZWxlYXNlKC41KS5nYWluKC44KS5yb29tKC4zKS5fcGlhbm9yb2xsKCkKCi8vIFJPQ0tJTkcgQkFTUyDigJQgc3RhcnRzIG9uIGJlYXQgb25lLiBSb290cyBmb2xsb3cgdGhlIHNvdXJjZSBiYXNzIGZpZ3VyZS4KJDogbm90ZSgiPGYyIGYyIGYyIGYyIGYyIGYzIGYyIGYzIGYyIGYzIGYyIGYyIGYyIGYyIGYyIGMzIGJiMSBmMiBmMiBmMiBmMiBmMiBmMiBmMj4iKS5zKCdhY19zd2FybScpCiAgLm4oLjE1KS5hdHRhY2soLjAwOCkuZGVjYXkoLjI1KS5zdXN0YWluKC4yNSkucmVsZWFzZSguMykuZ2FpbiguMykubHBmKDQ1MCkKCi8vIEhBUk1PTlkg4oCUIEYgLyBCYiAvIEdtIC8gRiAvIEMgLyBGLCBmb2xsb3dpbmcgdGhlIHJlbmRlcmVyJ3MgY2hvcmQgbWFwLgovLyBSZWFydGljdWxhdGVkIG9uY2UgcGVyIGJhcjsgaW50ZW50aW9uYWxseSByZWR1Y2VkIGZyb20gdGhlIGZpbmlzaGVkIG1peC4KJDogbm90ZSgiPFtmMyxhMyxjNF0gW2YzLGEzLGM0XSBbZjMsYTMsYzRdIFtmMyxhMyxjNF0gW2YzLGEzLGM0XSBbZjMsYTMsYzRdIFtmMyxhMyxjNF0gW2JiMixkMyxmM10gW2JiMixkMyxmM10gW2JiMixkMyxmM10gW2czLGJiMyxkNF0gW2czLGJiMyxkNF0gW2czLGJiMyxkNF0gW2czLGJiMyxkNF0gW2YzLGEzLGM0XSBbYzMsZTMsZzNdIFtmMyxhMyxjNF0gW2YzLGEzLGM0XSBbZjMsYTMsYzRdIFtmMyxhMyxjNF0gW2YzLGEzLGM0XSBbZjMsYTMsYzRdIFtmMyxhMyxjNF0gW2YzLGEzLGM0XT4iKS5zKCdhY19ibG9vbScpCiAgLm4oLjA4KS5hdHRhY2soLjA2KS5yZWxlYXNlKC42NSkuZ2FpbiguMTYpLmxwZigxODAwKS5yb29tKC40KQoKLy8gT1JCSVQgRE9VQkxFIOKAlCBleGFjdCBzYW1lIG1lbG9keSwgZGlmZmVyZW50IHNwZWN0cmFsIHJoeXRobSBhbmQgc3RlcmVvIG1vdGlvbi4KLy8gQ29tbWVudCBvdXQgdGhpcyBibG9jayB0byBoZWFyIGp1c3QgdGhlIG1hcmltYmEtbGVkIGFycmFuZ2VtZW50LgokOiBtZWxvZHkucygnYWNfb3JiaXQnKS5uKHBlcmxpbi5zbG93KDE3KS5yYW5nZSguMTUsLjg1KSkKICAuZ2FpbihzaW5lLnNsb3coMjMpLnJhbmdlKC4xLC4yMikpLnBhbihzaW5lLnNsb3coMTkpKQogIC5hdHRhY2soLjAwNCkucmVsZWFzZSguNCkuZGVsYXkoLjIpLmRlbGF5dGltZSguMzU3KS5yb29tKC4zKQoKLy8gQWxsIGFjdGl2ZSBibG9ja3MgcGxheSB0b2dldGhlci4gQ29tbWVudCBvdXQgYSB3aG9sZSAkOiBibG9jayB0byBtdXRlIGl0LgovLyBWb2ljZXMvY29udHJvbHM6IGh0dHBzOi8vcGF0LmFlc3RoZXRpYy5jb21wdXRlci9zb3VyY2UK diff --git a/ac-strudel/marimbaba-orbit.strudel b/ac-strudel/marimbaba-orbit.strudel new file mode 100644 index 0000000000..383304e69f --- /dev/null +++ b/ac-strudel/marimbaba-orbit.strudel @@ -0,0 +1,47 @@ +// MARIMBABA / ORBIT REMIX — @jeffrey / aesthetic.computer +// https://pat.aesthetic.computer/marimbaba-orbit +// Lead: pop/marimba/marimbaba.np — all 24 bars, original pitches and durations. +// Accompaniment: a reduced arrangement informed by bin/render-marimbaba.mjs. +// This is an editable score adaptation, not playback of the released master. +await import('https://pat.aesthetic.computer/s') + +// TEMPO — one cycle = one 3/4 bar. 84 BPM remix; set 56/3 for the lullaby. +setcpm(84/3) + +// MELODY — @ weights are beats. mini(...) parses the assembled strings. +// Five sections: hush, twinkle, wow, ba-ba, sleep. Total: 72 beats / 24 bars. +const melody = mini([ + // hush: 4 bars + 'c5@1 a4@1 f4@1 f4@3 a4@1.5 g4@1.5 f4@3', + // twinkle: 6 bars + 'f5@1 a5@1 c6@0.5 a5@0.5 g5@3 f5@1 g5@1 a5@0.5 g5@0.5 f5@3 bb5@1 d6@1 bb5@1 c6@1.5 a5@1.5', + // wow: 4 bars + '[g5,bb5]@6 a5@1 g5@1 a5@1 f5@3', + // baba: 4 bars + 'a5@0.5 g5@0.5 a5@1 f5@1 c6@0.5 bb5@0.5 c6@1 a5@1 g5@1 f5@1 e5@1 f5@3', + // sleep: 6 bars + 'c5@1.5 a4@1.5 g4@1 f4@1 f4@1 a4@1 g4@1 f4@1 f4@3 f3@3 ~@3', +].join(' ')).note().slow(24) + +// SINGER — ac_marimba uses the source rosewood ratios 1:4:9.2. +// .n(0..1) changes mallet brightness and ring; change .s(...) to recast it. +$: melody.s('ac_marimba').n(perlin.slow(13).range(.35,.9)) + .attack(.001).release(.5).gain(.8).room(.3)._pianoroll() + +// ROCKING BASS — starts on beat one. Roots follow the source bass figure. +$: note("").s('ac_swarm') + .n(.15).attack(.008).decay(.25).sustain(.25).release(.3).gain(.3).lpf(450) + +// HARMONY — F / Bb / Gm / F / C / F, following the renderer's chord map. +// Rearticulated once per bar; intentionally reduced from the finished mix. +$: note("<[f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [bb2,d3,f3] [bb2,d3,f3] [bb2,d3,f3] [g3,bb3,d4] [g3,bb3,d4] [g3,bb3,d4] [g3,bb3,d4] [f3,a3,c4] [c3,e3,g3] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4]>").s('ac_bloom') + .n(.08).attack(.06).release(.65).gain(.16).lpf(1800).room(.4) + +// ORBIT DOUBLE — exact same melody, different spectral rhythm and stereo motion. +// Comment out this block to hear just the marimba-led arrangement. +$: melody.s('ac_orbit').n(perlin.slow(17).range(.15,.85)) + .gain(sine.slow(23).range(.1,.22)).pan(sine.slow(19)) + .attack(.004).release(.4).delay(.2).delaytime(.357).room(.3) + +// All active blocks play together. Comment out a whole $: block to mute it. +// Voices/controls: https://pat.aesthetic.computer/source diff --git a/ac-strudel/marimbaba-orbit.url b/ac-strudel/marimbaba-orbit.url new file mode 100644 index 0000000000..8d6e634588 --- /dev/null +++ b/ac-strudel/marimbaba-orbit.url @@ -0,0 +1 @@ +https://strudel.cc/#Ly8gTUFSSU1CQUJBIC8gT1JCSVQgUkVNSVgg4oCUIEBqZWZmcmV5IC8gYWVzdGhldGljLmNvbXB1dGVyCi8vIGh0dHBzOi8vcGF0LmFlc3RoZXRpYy5jb21wdXRlci9tYXJpbWJhYmEtb3JiaXQKLy8gTGVhZDogcG9wL21hcmltYmEvbWFyaW1iYWJhLm5wIOKAlCBhbGwgMjQgYmFycywgb3JpZ2luYWwgcGl0Y2hlcyBhbmQgZHVyYXRpb25zLgovLyBBY2NvbXBhbmltZW50OiBhIHJlZHVjZWQgYXJyYW5nZW1lbnQgaW5mb3JtZWQgYnkgYmluL3JlbmRlci1tYXJpbWJhYmEubWpzLgovLyBUaGlzIGlzIGFuIGVkaXRhYmxlIHNjb3JlIGFkYXB0YXRpb24sIG5vdCBwbGF5YmFjayBvZiB0aGUgcmVsZWFzZWQgbWFzdGVyLgphd2FpdCBpbXBvcnQoJ2h0dHBzOi8vcGF0LmFlc3RoZXRpYy5jb21wdXRlci9zJykKCi8vIFRFTVBPIOKAlCBvbmUgY3ljbGUgPSBvbmUgMy80IGJhci4gODQgQlBNIHJlbWl4OyBzZXQgNTYvMyBmb3IgdGhlIGx1bGxhYnkuCnNldGNwbSg4NC8zKQoKLy8gTUVMT0RZIOKAlCBAIHdlaWdodHMgYXJlIGJlYXRzLiBtaW5pKC4uLikgcGFyc2VzIHRoZSBhc3NlbWJsZWQgc3RyaW5ncy4KLy8gRml2ZSBzZWN0aW9uczogaHVzaCwgdHdpbmtsZSwgd293LCBiYS1iYSwgc2xlZXAuIFRvdGFsOiA3MiBiZWF0cyAvIDI0IGJhcnMuCmNvbnN0IG1lbG9keSA9IG1pbmkoWwogIC8vIGh1c2g6IDQgYmFycwogICdjNUAxIGE0QDEgZjRAMSBmNEAzIGE0QDEuNSBnNEAxLjUgZjRAMycsCiAgLy8gdHdpbmtsZTogNiBiYXJzCiAgJ2Y1QDEgYTVAMSBjNkAwLjUgYTVAMC41IGc1QDMgZjVAMSBnNUAxIGE1QDAuNSBnNUAwLjUgZjVAMyBiYjVAMSBkNkAxIGJiNUAxIGM2QDEuNSBhNUAxLjUnLAogIC8vIHdvdzogNCBiYXJzCiAgJ1tnNSxiYjVdQDYgYTVAMSBnNUAxIGE1QDEgZjVAMycsCiAgLy8gYmFiYTogNCBiYXJzCiAgJ2E1QDAuNSBnNUAwLjUgYTVAMSBmNUAxIGM2QDAuNSBiYjVAMC41IGM2QDEgYTVAMSBnNUAxIGY1QDEgZTVAMSBmNUAzJywKICAvLyBzbGVlcDogNiBiYXJzCiAgJ2M1QDEuNSBhNEAxLjUgZzRAMSBmNEAxIGY0QDEgYTRAMSBnNEAxIGY0QDEgZjRAMyBmM0AzIH5AMycsCl0uam9pbignICcpKS5ub3RlKCkuc2xvdygyNCkKCi8vIFNJTkdFUiDigJQgYWNfbWFyaW1iYSB1c2VzIHRoZSBzb3VyY2Ugcm9zZXdvb2QgcmF0aW9zIDE6NDo5LjIuCi8vIC5uKDAuLjEpIGNoYW5nZXMgbWFsbGV0IGJyaWdodG5lc3MgYW5kIHJpbmc7IGNoYW5nZSAucyguLi4pIHRvIHJlY2FzdCBpdC4KJDogbWVsb2R5LnMoJ2FjX21hcmltYmEnKS5uKHBlcmxpbi5zbG93KDEzKS5yYW5nZSguMzUsLjkpKQogIC5hdHRhY2soLjAwMSkucmVsZWFzZSguNSkuZ2FpbiguOCkucm9vbSguMykuX3BpYW5vcm9sbCgpCgovLyBST0NLSU5HIEJBU1Mg4oCUIHN0YXJ0cyBvbiBiZWF0IG9uZS4gUm9vdHMgZm9sbG93IHRoZSBzb3VyY2UgYmFzcyBmaWd1cmUuCiQ6IG5vdGUoIjxmMiBmMiBmMiBmMiBmMiBmMyBmMiBmMyBmMiBmMyBmMiBmMiBmMiBmMiBmMiBjMyBiYjEgZjIgZjIgZjIgZjIgZjIgZjIgZjI+IikucygnYWNfc3dhcm0nKQogIC5uKC4xNSkuYXR0YWNrKC4wMDgpLmRlY2F5KC4yNSkuc3VzdGFpbiguMjUpLnJlbGVhc2UoLjMpLmdhaW4oLjMpLmxwZig0NTApCgovLyBIQVJNT05ZIOKAlCBGIC8gQmIgLyBHbSAvIEYgLyBDIC8gRiwgZm9sbG93aW5nIHRoZSByZW5kZXJlcidzIGNob3JkIG1hcC4KLy8gUmVhcnRpY3VsYXRlZCBvbmNlIHBlciBiYXI7IGludGVudGlvbmFsbHkgcmVkdWNlZCBmcm9tIHRoZSBmaW5pc2hlZCBtaXguCiQ6IG5vdGUoIjxbZjMsYTMsYzRdIFtmMyxhMyxjNF0gW2YzLGEzLGM0XSBbZjMsYTMsYzRdIFtmMyxhMyxjNF0gW2YzLGEzLGM0XSBbZjMsYTMsYzRdIFtiYjIsZDMsZjNdIFtiYjIsZDMsZjNdIFtiYjIsZDMsZjNdIFtnMyxiYjMsZDRdIFtnMyxiYjMsZDRdIFtnMyxiYjMsZDRdIFtnMyxiYjMsZDRdIFtmMyxhMyxjNF0gW2MzLGUzLGczXSBbZjMsYTMsYzRdIFtmMyxhMyxjNF0gW2YzLGEzLGM0XSBbZjMsYTMsYzRdIFtmMyxhMyxjNF0gW2YzLGEzLGM0XSBbZjMsYTMsYzRdIFtmMyxhMyxjNF0+IikucygnYWNfYmxvb20nKQogIC5uKC4wOCkuYXR0YWNrKC4wNikucmVsZWFzZSguNjUpLmdhaW4oLjE2KS5scGYoMTgwMCkucm9vbSguNCkKCi8vIE9SQklUIERPVUJMRSDigJQgZXhhY3Qgc2FtZSBtZWxvZHksIGRpZmZlcmVudCBzcGVjdHJhbCByaHl0aG0gYW5kIHN0ZXJlbyBtb3Rpb24uCi8vIENvbW1lbnQgb3V0IHRoaXMgYmxvY2sgdG8gaGVhciBqdXN0IHRoZSBtYXJpbWJhLWxlZCBhcnJhbmdlbWVudC4KJDogbWVsb2R5LnMoJ2FjX29yYml0JykubihwZXJsaW4uc2xvdygxNykucmFuZ2UoLjE1LC44NSkpCiAgLmdhaW4oc2luZS5zbG93KDIzKS5yYW5nZSguMSwuMjIpKS5wYW4oc2luZS5zbG93KDE5KSkKICAuYXR0YWNrKC4wMDQpLnJlbGVhc2UoLjQpLmRlbGF5KC4yKS5kZWxheXRpbWUoLjM1Nykucm9vbSguMykKCi8vIEFsbCBhY3RpdmUgYmxvY2tzIHBsYXkgdG9nZXRoZXIuIENvbW1lbnQgb3V0IGEgd2hvbGUgJDogYmxvY2sgdG8gbXV0ZSBpdC4KLy8gVm9pY2VzL2NvbnRyb2xzOiBodHRwczovL3BhdC5hZXN0aGV0aWMuY29tcHV0ZXIvc291cmNlCg== diff --git a/ac-strudel/marimbaba-paste.strudel b/ac-strudel/marimbaba-paste.strudel new file mode 100644 index 0000000000..e36592ec38 --- /dev/null +++ b/ac-strudel/marimbaba-paste.strudel @@ -0,0 +1,325 @@ +(() => { +// AC OS Notepat voices for Strudel. https://aesthetic.computer +// Recipes: fedac/native/pieces/notepat.mjs (triangle Shift alternate and chords). +// Uses Web Audio's band-limited oscillators; this is not a recording of the +// native mixer, its echo, or its output saturation. +// Glass, swarm, bloom, and orbit extend those recipes as new Strudel sounds. + +const voices = Object.freeze({ + ac_triangle: { wave: 'triangle', ratios: [1], levels: [0.5] }, + ac_triangle_fifth: { wave: 'triangle', ratios: [1, 1.5], levels: [0.42, 0.12] }, + ac_sine: { wave: 'sine', ratios: [1], levels: [0.5] }, + ac_major: { wave: 'sine', ratios: [1, 2 ** (4 / 12), 2 ** (7 / 12)] }, + ac_minor: { wave: 'sine', ratios: [1, 2 ** (3 / 12), 2 ** (7 / 12)] }, + ac_sus2: { wave: 'sine', ratios: [1, 2 ** (2 / 12), 2 ** (7 / 12)] }, + ac_glass: { rich: 'glass' }, + ac_swarm: { rich: 'swarm' }, + ac_bloom: { rich: 'bloom' }, + ac_orbit: { rich: 'orbit' }, + ac_marimba: { modal: true }, + ac_stone: { modal: 'stone' }, + ac_water: { chirp: 'water' }, + ac_kick: { chirp: 'kick' }, +}); + +const bounded = (value, fallback, min, max) => { + const n = Number(value ?? fallback); + return Number.isFinite(n) ? Math.max(min, Math.min(max, n)) : fallback; +}; + +function marimbaVoice(ctx, time, value, frequency, onended, stone = false) { + // Modal reduction of pop/marimba/synths/marimba.mjs, rosewood preset. + // Preserves its 1:4:9.2 ratios and T60 decay relationships; omits the + // source renderer's mallet convolution and resonator tube model. + const color = bounded(value.n, 0.5, 0, 1); + const duration = bounded(value.duration, 0.5, 0.001, 3600); + const attack = Math.min(bounded(value.attack, 0.001, 0, 60), duration); + const decay = Math.min(bounded(value.decay, 0, 0, 60), duration - attack); + const sustain = bounded(value.sustain, 1, 0, 1); + const release = bounded(value.release, 0.25, 0, 60); + const end = time + duration + release + 0.005; + const node = ctx.createGain(); + node.gain.setValueAtTime(attack ? 0 : 1, time); + if (attack) node.gain.linearRampToValueAtTime(1, time + attack); + node.gain.linearRampToValueAtTime(sustain, time + attack + decay); + node.gain.setValueAtTime(sustain, time + duration); + node.gain.linearRampToValueAtTime(0, end - 0.005); + // Stone is a designed inharmonic bar surrogate, not the offline FEM model. + const ratios = stone ? [1, 2.756, 5.404] : [1, 4, 9.2]; + const t60 = stone ? [2.8, 1.1, 0.38] : [1.6, 0.32, 0.09]; + const levels = [1, 0.32 * (0.3 + color * 1.4), 0.1 * (0.3 + color * 1.4)]; + const scale = 0.48 / levels.reduce((a, b) => a + b, 0); + const oscillators = [], gains = []; + let remaining = 3, finished = false; + ratios.forEach((ratio, i) => { + const o = ctx.createOscillator(), g = ctx.createGain(); + o.frequency.value = Math.min(frequency * ratio, ctx.sampleRate * 0.45); + const amplitude = levels[i] * scale; + const ring = t60[i] * (0.8 + color * 0.8); + g.gain.setValueAtTime(amplitude, time); + g.gain.exponentialRampToValueAtTime(amplitude * 0.001, time + ring); + g.gain.exponentialRampToValueAtTime(0.0000001, time + ring * 2); + o.connect(g).connect(node); + o.onended = () => { + if (--remaining === 0 && !finished) { + finished = true; + [...oscillators, ...gains, node].forEach(n => n.disconnect()); + onended?.(); + } + }; + oscillators.push(o); gains.push(g); + o.start(time); o.stop(end); + }); + return { node, stop(at) { + if (finished) return; + const stopAt = Math.max(ctx.currentTime, Math.min(at, end)); + node.gain.cancelAndHoldAtTime(stopAt); + node.gain.linearRampToValueAtTime(0, stopAt + 0.005); + oscillators.forEach(o => o.stop(Math.min(end, stopAt + 0.005))); + } }; +} + +function chirpVoice(ctx, time, value, frequency, kind, onended) { + const color = bounded(value.n, 0.5, 0, 1); + const kick = kind === 'kick'; + const duration = bounded(value.duration, 0.2, 0.001, 3600); + const release = bounded(value.release, 0.08, 0, 60); + const end = time + duration + release + 0.005; + const node = ctx.createGain(), oscillator = ctx.createOscillator(); + const attack = Math.min(bounded(value.attack, kick ? 0.007 : 0.003, 0, 60), duration); + const ring = kick ? 0.12 + color * 0.2 : 0.055 + color * 0.1; + node.gain.setValueAtTime(attack ? 0 : 0.5, time); + if (attack) node.gain.linearRampToValueAtTime(0.5, time + attack); + node.gain.exponentialRampToValueAtTime(0.00001, Math.min(end - 0.005, time + attack + ring)); + node.gain.setValueAtTime(0, end); + oscillator.frequency.setValueAtTime(kick ? 126 + color * 20 : frequency * 0.55, time); + oscillator.frequency.exponentialRampToValueAtTime(kick ? 43 : frequency, + time + Math.min(duration, kick ? 0.055 + color * 0.04 : 0.07)); + oscillator.connect(node); + let finished = false; + oscillator.onended = () => { + if (finished) return; + finished = true; oscillator.disconnect(); node.disconnect(); onended?.(); + }; + oscillator.start(time); oscillator.stop(end); + return { node, stop(at) { + if (finished) return; + const stopAt = Math.max(ctx.currentTime, Math.min(at, end)); + node.gain.cancelAndHoldAtTime(stopAt); + node.gain.linearRampToValueAtTime(0, stopAt + 0.005); + oscillator.stop(Math.min(end, stopAt + 0.005)); + } }; +} + +function richVoice(ctx, time, value, frequency, kind, onended, cps = 0.5) { + // n is a 0..1 timbre macro: FM depth, detune, and filter motion. + const color = bounded(value.n, 0.5, 0, 1); + const duration = bounded(value.duration, 0.5, 0.001, 3600); + const attack = Math.min(bounded(value.attack, kind === 'bloom' ? 0.18 : 0.006, 0, 60), duration); + const decay = Math.min(bounded(value.decay, kind === 'glass' ? 0.3 : 0.1, 0, 60), duration - attack); + const sustain = bounded(value.sustain, kind === 'glass' ? 0.24 : 0.8, 0, 1); + const release = bounded(value.release, kind === 'glass' ? 0.35 : 0.6, 0, 60); + const holdEnd = time + duration; + const end = holdEnd + release + 0.005; + const node = ctx.createGain(); + const allNodes = [node], oscillators = []; + const keep = object => { allNodes.push(object); return object; }; + const gain = level => { + const g = keep(ctx.createGain()); g.gain.value = level; return g; + }; + const osc = (type, hz) => { + const o = keep(ctx.createOscillator()); + o.type = type; + o.frequency.value = Math.max(0.05, Math.min(hz, ctx.sampleRate * 0.4)); + oscillators.push(o); return o; + }; + const filter = keep(ctx.createBiquadFilter()); + filter.type = 'lowpass'; + filter.Q.value = 0.5; + const cutoff = Math.min(ctx.sampleRate * 0.4, frequency * (5 + color * 15)); + filter.frequency.setValueAtTime(cutoff, time); + filter.frequency.exponentialRampToValueAtTime( + Math.max(60, cutoff * (kind === 'glass' ? 0.24 : 0.55)), holdEnd, + ); + filter.connect(node); + node.gain.setValueAtTime(attack ? 0 : 1, time); + if (attack) node.gain.linearRampToValueAtTime(1, time + attack); + node.gain.linearRampToValueAtTime(sustain, time + attack + decay); + node.gain.setValueAtTime(sustain, holdEnd); + node.gain.linearRampToValueAtTime(0, holdEnd + release); + + const layers = kind === 'glass' + ? [[1, 0.24, 'sine', 0], [1.5, 0.10, 'triangle', 0], [2.756, 0.07, 'sine', 0]] + : kind === 'swarm' + ? [[1, 0.12, 'sawtooth', -1], [1, 0.12, 'sawtooth', 1], [1.5, 0.09, 'triangle', 0.4], [0.5, 0.10, 'sine', 0]] + : [[1, 0.15, 'triangle', -1], [1, 0.12, 'triangle', 1], [1.5, 0.09, 'sine', -0.5], [2, 0.06, 'sine', 0.5]]; + layers.forEach(([ratio, level, wave, spread], i) => { + const carrier = osc(wave, frequency * ratio); + carrier.detune.value = spread * (4 + color * 18); + const pan = keep(ctx.createStereoPanner()); + pan.pan.value = spread * 0.45; + const pulse = gain(kind === 'orbit' ? 0.72 : 1); + carrier.connect(gain(level)).connect(pulse).connect(pan).connect(filter); + if (kind === 'orbit') { + // Amplitude pulses locked to the scheduler's cycle rate. + osc('sine', bounded(cps, 0.5, 0.01, 20) * (i + 2)) + .connect(gain(0.28)).connect(pulse.gain); + } + if (kind !== 'swarm') { + const mod = osc('sine', frequency * ratio * (kind === 'glass' ? 2.756 : 1.5)); + const depth = gain(frequency * ratio * (0.08 + color * (kind === 'glass' ? 2.4 : 0.6))); + mod.connect(depth).connect(carrier.frequency); + depth.gain.setValueAtTime(depth.gain.value, time); + depth.gain.exponentialRampToValueAtTime( + Math.max(0.001, depth.gain.value * 0.08), time + Math.min(duration, 0.3 + color), + ); + if (kind === 'orbit') { + // Carrier -> delay -> modulator pitch -> carrier pitch. The delay + // makes the feedback cycle valid, while sine outputs bound its level. + const feedback = keep(ctx.createDelay(0.05)); + feedback.delayTime.value = 0.004 + i * 0.002; + carrier.connect(feedback).connect(gain(frequency * (0.15 + color * 1.4))) + .connect(mod.frequency); + const shape = osc('sine', 0.13 + i * 0.07); + shape.connect(gain(frequency * ratio * color * 0.24)).connect(depth.gain); + } + } + const drift = osc('sine', 0.19 + i * 0.11 + color * 0.35); + drift.connect(gain(2 + color * 6)).connect(carrier.detune); + if (kind === 'bloom' || kind === 'orbit') drift.connect(gain(0.3)).connect(pan.pan); + }); + let remaining = oscillators.length, finished = false; + for (const oscillator of oscillators) { + oscillator.onended = () => { + if (--remaining === 0 && !finished) { + finished = true; + allNodes.forEach(n => n.disconnect()); + onended?.(); + } + }; + oscillator.start(time); + oscillator.stop(end); + } + return { + node, + stop(at) { + if (finished) return; + const stopAt = Math.max(ctx.currentTime, Math.min(at, end)); + node.gain.cancelAndHoldAtTime(stopAt); + node.gain.linearRampToValueAtTime(0, stopAt + 0.005); + for (const o of oscillators) o.stop(Math.min(end, stopAt + 0.005)); + }, + }; +} + +function registerNotepat(api = globalThis) { + const { registerSound, getAudioContext, getFrequencyFromValue } = api; + if (![registerSound, getAudioContext, getFrequencyFromValue].every(fn => typeof fn === 'function')) { + throw new Error('Import Notepat inside Strudel, or pass its audio API to registerNotepat().'); + } + for (const [name, recipe] of Object.entries(voices)) { + registerSound(name, (time, value, onended, cps) => { + const ctx = getAudioContext(); + const frequency = bounded(getFrequencyFromValue(value), 220, 1, ctx.sampleRate / 2 - 1); + if (recipe.modal) return marimbaVoice(ctx, time, value, frequency, onended, recipe.modal === 'stone'); + if (recipe.chirp) return chirpVoice(ctx, time, value, frequency, recipe.chirp, onended); + if (recipe.rich) return richVoice(ctx, time, value, frequency, recipe.rich, onended, cps); + const duration = bounded(value.duration, 0.5, 0.001, 3600); + const attack = Math.min(bounded(value.attack, 0.02, 0, 60), duration); + const decay = Math.min(bounded(value.decay, 0, 0, 60), duration - attack); + const sustain = bounded(value.sustain, 1, 0, 1); + const release = bounded(value.release, 0.1, 0, 60); + const holdEnd = time + duration; + const end = holdEnd + release + 0.005; + const node = ctx.createGain(); + node.gain.setValueAtTime(attack ? 0 : 1, time); + if (attack) node.gain.linearRampToValueAtTime(1, time + attack); + node.gain.linearRampToValueAtTime(sustain, time + attack + decay); + node.gain.setValueAtTime(sustain, holdEnd); + node.gain.linearRampToValueAtTime(0, holdEnd + release); + + const oscillators = []; + let remaining = recipe.ratios.length; + let finished = false; + for (let i = 0; i < recipe.ratios.length; i++) { + const oscillator = ctx.createOscillator(); + oscillator.type = recipe.wave; + oscillator.frequency.value = Math.min(frequency * recipe.ratios[i], ctx.sampleRate / 2 - 1); + const gain = ctx.createGain(); + // Preserve the native triangle layer balance. Chords share headroom. + gain.gain.value = (recipe.levels?.[i] ?? 0.5 / recipe.ratios.length) + * (recipe.wave === 'triangle' ? -1 : 1); + oscillator.connect(gain).connect(node); + oscillator.onended = () => { + oscillator.disconnect(); + gain.disconnect(); + if (--remaining === 0 && !finished) { + finished = true; + node.disconnect(); + onended?.(); + } + }; + oscillator.start(time); + oscillator.stop(end); + oscillators.push(oscillator); + } + return { + node, + // Strudel calls this when retiring a voice, including polyphony steals. + stop(at) { + if (finished) return; + const stopAt = Math.max(ctx.currentTime, Math.min(at, end)); + node.gain.cancelAndHoldAtTime(stopAt); + node.gain.linearRampToValueAtTime(0, stopAt + 0.005); + for (const oscillator of oscillators) oscillator.stop(Math.min(end, stopAt + 0.005)); + }, + }; + }, { type: 'synth' }); + } + return Object.keys(voices); +} + +registerNotepat(); + +})() + +// MARIMBABA — @jeffrey / aesthetic.computer +// https://pat.aesthetic.computer/marimbaba +// Lead: pop/marimba/marimbaba.np — all 24 bars, original pitches and durations. +// Accompaniment: a reduced arrangement informed by bin/render-marimbaba.mjs. +// This is an editable score adaptation, not playback of the released master. + +// TEMPO — one cycle = one 3/4 bar. Original 56 BPM. +setcpm(56/3) + +// MELODY — @ weights are beats. mini(...) parses the assembled strings. +// Five sections: hush, twinkle, wow, ba-ba, sleep. Total: 72 beats / 24 bars. +const melody = mini([ + // hush: 4 bars + 'c5@1 a4@1 f4@1 f4@3 a4@1.5 g4@1.5 f4@3', + // twinkle: 6 bars + 'f5@1 a5@1 c6@0.5 a5@0.5 g5@3 f5@1 g5@1 a5@0.5 g5@0.5 f5@3 bb5@1 d6@1 bb5@1 c6@1.5 a5@1.5', + // wow: 4 bars + '[g5,bb5]@6 a5@1 g5@1 a5@1 f5@3', + // baba: 4 bars + 'a5@0.5 g5@0.5 a5@1 f5@1 c6@0.5 bb5@0.5 c6@1 a5@1 g5@1 f5@1 e5@1 f5@3', + // sleep: 6 bars + 'c5@1.5 a4@1.5 g4@1 f4@1 f4@1 a4@1 g4@1 f4@1 f4@3 f3@3 ~@3', +].join(' ')).note().slow(24) + +// SINGER — ac_marimba uses the source rosewood ratios 1:4:9.2. +// .n(0..1) changes mallet brightness and ring; change .s(...) to recast it. +$: melody.s('ac_marimba').n(.45) + .attack(.001).release(.5).gain(.8).room(.3)._pianoroll() + +// ROCKING BASS — starts on beat one. Roots follow the source bass figure. +$: note("").s('ac_sine') + .n(.15).attack(.008).decay(.25).sustain(.25).release(.3).gain(.38).lpf(450) + +// HARMONY — F / Bb / Gm / F / C / F, following the renderer's chord map. +// Rearticulated once per bar; intentionally reduced from the finished mix. +$: note("<[f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [bb2,d3,f3] [bb2,d3,f3] [bb2,d3,f3] [g3,bb3,d4] [g3,bb3,d4] [g3,bb3,d4] [g3,bb3,d4] [f3,a3,c4] [c3,e3,g3] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4]>").s('ac_bloom') + .n(.08).attack(.06).release(.65).gain(.16).lpf(1800).room(.4) + +// All active blocks play together. Comment out a whole $: block to mute it. +// Voices/controls: https://pat.aesthetic.computer/source diff --git a/ac-strudel/marimbaba-paste.url b/ac-strudel/marimbaba-paste.url new file mode 100644 index 0000000000..f5bef8cf98 --- /dev/null +++ b/ac-strudel/marimbaba-paste.url @@ -0,0 +1 @@ +https://strudel.cc/#KCgpID0+IHsKLy8gQUMgT1MgTm90ZXBhdCB2b2ljZXMgZm9yIFN0cnVkZWwuIGh0dHBzOi8vYWVzdGhldGljLmNvbXB1dGVyCi8vIFJlY2lwZXM6IGZlZGFjL25hdGl2ZS9waWVjZXMvbm90ZXBhdC5tanMgKHRyaWFuZ2xlIFNoaWZ0IGFsdGVybmF0ZSBhbmQgY2hvcmRzKS4KLy8gVXNlcyBXZWIgQXVkaW8ncyBiYW5kLWxpbWl0ZWQgb3NjaWxsYXRvcnM7IHRoaXMgaXMgbm90IGEgcmVjb3JkaW5nIG9mIHRoZQovLyBuYXRpdmUgbWl4ZXIsIGl0cyBlY2hvLCBvciBpdHMgb3V0cHV0IHNhdHVyYXRpb24uCi8vIEdsYXNzLCBzd2FybSwgYmxvb20sIGFuZCBvcmJpdCBleHRlbmQgdGhvc2UgcmVjaXBlcyBhcyBuZXcgU3RydWRlbCBzb3VuZHMuCgpjb25zdCB2b2ljZXMgPSBPYmplY3QuZnJlZXplKHsKICBhY190cmlhbmdsZTogeyB3YXZlOiAndHJpYW5nbGUnLCByYXRpb3M6IFsxXSwgbGV2ZWxzOiBbMC41XSB9LAogIGFjX3RyaWFuZ2xlX2ZpZnRoOiB7IHdhdmU6ICd0cmlhbmdsZScsIHJhdGlvczogWzEsIDEuNV0sIGxldmVsczogWzAuNDIsIDAuMTJdIH0sCiAgYWNfc2luZTogeyB3YXZlOiAnc2luZScsIHJhdGlvczogWzFdLCBsZXZlbHM6IFswLjVdIH0sCiAgYWNfbWFqb3I6IHsgd2F2ZTogJ3NpbmUnLCByYXRpb3M6IFsxLCAyICoqICg0IC8gMTIpLCAyICoqICg3IC8gMTIpXSB9LAogIGFjX21pbm9yOiB7IHdhdmU6ICdzaW5lJywgcmF0aW9zOiBbMSwgMiAqKiAoMyAvIDEyKSwgMiAqKiAoNyAvIDEyKV0gfSwKICBhY19zdXMyOiB7IHdhdmU6ICdzaW5lJywgcmF0aW9zOiBbMSwgMiAqKiAoMiAvIDEyKSwgMiAqKiAoNyAvIDEyKV0gfSwKICBhY19nbGFzczogeyByaWNoOiAnZ2xhc3MnIH0sCiAgYWNfc3dhcm06IHsgcmljaDogJ3N3YXJtJyB9LAogIGFjX2Jsb29tOiB7IHJpY2g6ICdibG9vbScgfSwKICBhY19vcmJpdDogeyByaWNoOiAnb3JiaXQnIH0sCiAgYWNfbWFyaW1iYTogeyBtb2RhbDogdHJ1ZSB9LAogIGFjX3N0b25lOiB7IG1vZGFsOiAnc3RvbmUnIH0sCiAgYWNfd2F0ZXI6IHsgY2hpcnA6ICd3YXRlcicgfSwKICBhY19raWNrOiB7IGNoaXJwOiAna2ljaycgfSwKfSk7Cgpjb25zdCBib3VuZGVkID0gKHZhbHVlLCBmYWxsYmFjaywgbWluLCBtYXgpID0+IHsKICBjb25zdCBuID0gTnVtYmVyKHZhbHVlID8/IGZhbGxiYWNrKTsKICByZXR1cm4gTnVtYmVyLmlzRmluaXRlKG4pID8gTWF0aC5tYXgobWluLCBNYXRoLm1pbihtYXgsIG4pKSA6IGZhbGxiYWNrOwp9OwoKZnVuY3Rpb24gbWFyaW1iYVZvaWNlKGN0eCwgdGltZSwgdmFsdWUsIGZyZXF1ZW5jeSwgb25lbmRlZCwgc3RvbmUgPSBmYWxzZSkgewogIC8vIE1vZGFsIHJlZHVjdGlvbiBvZiBwb3AvbWFyaW1iYS9zeW50aHMvbWFyaW1iYS5tanMsIHJvc2V3b29kIHByZXNldC4KICAvLyBQcmVzZXJ2ZXMgaXRzIDE6NDo5LjIgcmF0aW9zIGFuZCBUNjAgZGVjYXkgcmVsYXRpb25zaGlwczsgb21pdHMgdGhlCiAgLy8gc291cmNlIHJlbmRlcmVyJ3MgbWFsbGV0IGNvbnZvbHV0aW9uIGFuZCByZXNvbmF0b3IgdHViZSBtb2RlbC4KICBjb25zdCBjb2xvciA9IGJvdW5kZWQodmFsdWUubiwgMC41LCAwLCAxKTsKICBjb25zdCBkdXJhdGlvbiA9IGJvdW5kZWQodmFsdWUuZHVyYXRpb24sIDAuNSwgMC4wMDEsIDM2MDApOwogIGNvbnN0IGF0dGFjayA9IE1hdGgubWluKGJvdW5kZWQodmFsdWUuYXR0YWNrLCAwLjAwMSwgMCwgNjApLCBkdXJhdGlvbik7CiAgY29uc3QgZGVjYXkgPSBNYXRoLm1pbihib3VuZGVkKHZhbHVlLmRlY2F5LCAwLCAwLCA2MCksIGR1cmF0aW9uIC0gYXR0YWNrKTsKICBjb25zdCBzdXN0YWluID0gYm91bmRlZCh2YWx1ZS5zdXN0YWluLCAxLCAwLCAxKTsKICBjb25zdCByZWxlYXNlID0gYm91bmRlZCh2YWx1ZS5yZWxlYXNlLCAwLjI1LCAwLCA2MCk7CiAgY29uc3QgZW5kID0gdGltZSArIGR1cmF0aW9uICsgcmVsZWFzZSArIDAuMDA1OwogIGNvbnN0IG5vZGUgPSBjdHguY3JlYXRlR2FpbigpOwogIG5vZGUuZ2Fpbi5zZXRWYWx1ZUF0VGltZShhdHRhY2sgPyAwIDogMSwgdGltZSk7CiAgaWYgKGF0dGFjaykgbm9kZS5nYWluLmxpbmVhclJhbXBUb1ZhbHVlQXRUaW1lKDEsIHRpbWUgKyBhdHRhY2spOwogIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZShzdXN0YWluLCB0aW1lICsgYXR0YWNrICsgZGVjYXkpOwogIG5vZGUuZ2Fpbi5zZXRWYWx1ZUF0VGltZShzdXN0YWluLCB0aW1lICsgZHVyYXRpb24pOwogIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZSgwLCBlbmQgLSAwLjAwNSk7CiAgLy8gU3RvbmUgaXMgYSBkZXNpZ25lZCBpbmhhcm1vbmljIGJhciBzdXJyb2dhdGUsIG5vdCB0aGUgb2ZmbGluZSBGRU0gbW9kZWwuCiAgY29uc3QgcmF0aW9zID0gc3RvbmUgPyBbMSwgMi43NTYsIDUuNDA0XSA6IFsxLCA0LCA5LjJdOwogIGNvbnN0IHQ2MCA9IHN0b25lID8gWzIuOCwgMS4xLCAwLjM4XSA6IFsxLjYsIDAuMzIsIDAuMDldOwogIGNvbnN0IGxldmVscyA9IFsxLCAwLjMyICogKDAuMyArIGNvbG9yICogMS40KSwgMC4xICogKDAuMyArIGNvbG9yICogMS40KV07CiAgY29uc3Qgc2NhbGUgPSAwLjQ4IC8gbGV2ZWxzLnJlZHVjZSgoYSwgYikgPT4gYSArIGIsIDApOwogIGNvbnN0IG9zY2lsbGF0b3JzID0gW10sIGdhaW5zID0gW107CiAgbGV0IHJlbWFpbmluZyA9IDMsIGZpbmlzaGVkID0gZmFsc2U7CiAgcmF0aW9zLmZvckVhY2goKHJhdGlvLCBpKSA9PiB7CiAgICBjb25zdCBvID0gY3R4LmNyZWF0ZU9zY2lsbGF0b3IoKSwgZyA9IGN0eC5jcmVhdGVHYWluKCk7CiAgICBvLmZyZXF1ZW5jeS52YWx1ZSA9IE1hdGgubWluKGZyZXF1ZW5jeSAqIHJhdGlvLCBjdHguc2FtcGxlUmF0ZSAqIDAuNDUpOwogICAgY29uc3QgYW1wbGl0dWRlID0gbGV2ZWxzW2ldICogc2NhbGU7CiAgICBjb25zdCByaW5nID0gdDYwW2ldICogKDAuOCArIGNvbG9yICogMC44KTsKICAgIGcuZ2Fpbi5zZXRWYWx1ZUF0VGltZShhbXBsaXR1ZGUsIHRpbWUpOwogICAgZy5nYWluLmV4cG9uZW50aWFsUmFtcFRvVmFsdWVBdFRpbWUoYW1wbGl0dWRlICogMC4wMDEsIHRpbWUgKyByaW5nKTsKICAgIGcuZ2Fpbi5leHBvbmVudGlhbFJhbXBUb1ZhbHVlQXRUaW1lKDAuMDAwMDAwMSwgdGltZSArIHJpbmcgKiAyKTsKICAgIG8uY29ubmVjdChnKS5jb25uZWN0KG5vZGUpOwogICAgby5vbmVuZGVkID0gKCkgPT4gewogICAgICBpZiAoLS1yZW1haW5pbmcgPT09IDAgJiYgIWZpbmlzaGVkKSB7CiAgICAgICAgZmluaXNoZWQgPSB0cnVlOwogICAgICAgIFsuLi5vc2NpbGxhdG9ycywgLi4uZ2FpbnMsIG5vZGVdLmZvckVhY2gobiA9PiBuLmRpc2Nvbm5lY3QoKSk7CiAgICAgICAgb25lbmRlZD8uKCk7CiAgICAgIH0KICAgIH07CiAgICBvc2NpbGxhdG9ycy5wdXNoKG8pOyBnYWlucy5wdXNoKGcpOwogICAgby5zdGFydCh0aW1lKTsgby5zdG9wKGVuZCk7CiAgfSk7CiAgcmV0dXJuIHsgbm9kZSwgc3RvcChhdCkgewogICAgaWYgKGZpbmlzaGVkKSByZXR1cm47CiAgICBjb25zdCBzdG9wQXQgPSBNYXRoLm1heChjdHguY3VycmVudFRpbWUsIE1hdGgubWluKGF0LCBlbmQpKTsKICAgIG5vZGUuZ2Fpbi5jYW5jZWxBbmRIb2xkQXRUaW1lKHN0b3BBdCk7CiAgICBub2RlLmdhaW4ubGluZWFyUmFtcFRvVmFsdWVBdFRpbWUoMCwgc3RvcEF0ICsgMC4wMDUpOwogICAgb3NjaWxsYXRvcnMuZm9yRWFjaChvID0+IG8uc3RvcChNYXRoLm1pbihlbmQsIHN0b3BBdCArIDAuMDA1KSkpOwogIH0gfTsKfQoKZnVuY3Rpb24gY2hpcnBWb2ljZShjdHgsIHRpbWUsIHZhbHVlLCBmcmVxdWVuY3ksIGtpbmQsIG9uZW5kZWQpIHsKICBjb25zdCBjb2xvciA9IGJvdW5kZWQodmFsdWUubiwgMC41LCAwLCAxKTsKICBjb25zdCBraWNrID0ga2luZCA9PT0gJ2tpY2snOwogIGNvbnN0IGR1cmF0aW9uID0gYm91bmRlZCh2YWx1ZS5kdXJhdGlvbiwgMC4yLCAwLjAwMSwgMzYwMCk7CiAgY29uc3QgcmVsZWFzZSA9IGJvdW5kZWQodmFsdWUucmVsZWFzZSwgMC4wOCwgMCwgNjApOwogIGNvbnN0IGVuZCA9IHRpbWUgKyBkdXJhdGlvbiArIHJlbGVhc2UgKyAwLjAwNTsKICBjb25zdCBub2RlID0gY3R4LmNyZWF0ZUdhaW4oKSwgb3NjaWxsYXRvciA9IGN0eC5jcmVhdGVPc2NpbGxhdG9yKCk7CiAgY29uc3QgYXR0YWNrID0gTWF0aC5taW4oYm91bmRlZCh2YWx1ZS5hdHRhY2ssIGtpY2sgPyAwLjAwNyA6IDAuMDAzLCAwLCA2MCksIGR1cmF0aW9uKTsKICBjb25zdCByaW5nID0ga2ljayA/IDAuMTIgKyBjb2xvciAqIDAuMiA6IDAuMDU1ICsgY29sb3IgKiAwLjE7CiAgbm9kZS5nYWluLnNldFZhbHVlQXRUaW1lKGF0dGFjayA/IDAgOiAwLjUsIHRpbWUpOwogIGlmIChhdHRhY2spIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZSgwLjUsIHRpbWUgKyBhdHRhY2spOwogIG5vZGUuZ2Fpbi5leHBvbmVudGlhbFJhbXBUb1ZhbHVlQXRUaW1lKDAuMDAwMDEsIE1hdGgubWluKGVuZCAtIDAuMDA1LCB0aW1lICsgYXR0YWNrICsgcmluZykpOwogIG5vZGUuZ2Fpbi5zZXRWYWx1ZUF0VGltZSgwLCBlbmQpOwogIG9zY2lsbGF0b3IuZnJlcXVlbmN5LnNldFZhbHVlQXRUaW1lKGtpY2sgPyAxMjYgKyBjb2xvciAqIDIwIDogZnJlcXVlbmN5ICogMC41NSwgdGltZSk7CiAgb3NjaWxsYXRvci5mcmVxdWVuY3kuZXhwb25lbnRpYWxSYW1wVG9WYWx1ZUF0VGltZShraWNrID8gNDMgOiBmcmVxdWVuY3ksCiAgICB0aW1lICsgTWF0aC5taW4oZHVyYXRpb24sIGtpY2sgPyAwLjA1NSArIGNvbG9yICogMC4wNCA6IDAuMDcpKTsKICBvc2NpbGxhdG9yLmNvbm5lY3Qobm9kZSk7CiAgbGV0IGZpbmlzaGVkID0gZmFsc2U7CiAgb3NjaWxsYXRvci5vbmVuZGVkID0gKCkgPT4gewogICAgaWYgKGZpbmlzaGVkKSByZXR1cm47CiAgICBmaW5pc2hlZCA9IHRydWU7IG9zY2lsbGF0b3IuZGlzY29ubmVjdCgpOyBub2RlLmRpc2Nvbm5lY3QoKTsgb25lbmRlZD8uKCk7CiAgfTsKICBvc2NpbGxhdG9yLnN0YXJ0KHRpbWUpOyBvc2NpbGxhdG9yLnN0b3AoZW5kKTsKICByZXR1cm4geyBub2RlLCBzdG9wKGF0KSB7CiAgICBpZiAoZmluaXNoZWQpIHJldHVybjsKICAgIGNvbnN0IHN0b3BBdCA9IE1hdGgubWF4KGN0eC5jdXJyZW50VGltZSwgTWF0aC5taW4oYXQsIGVuZCkpOwogICAgbm9kZS5nYWluLmNhbmNlbEFuZEhvbGRBdFRpbWUoc3RvcEF0KTsKICAgIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZSgwLCBzdG9wQXQgKyAwLjAwNSk7CiAgICBvc2NpbGxhdG9yLnN0b3AoTWF0aC5taW4oZW5kLCBzdG9wQXQgKyAwLjAwNSkpOwogIH0gfTsKfQoKZnVuY3Rpb24gcmljaFZvaWNlKGN0eCwgdGltZSwgdmFsdWUsIGZyZXF1ZW5jeSwga2luZCwgb25lbmRlZCwgY3BzID0gMC41KSB7CiAgLy8gbiBpcyBhIDAuLjEgdGltYnJlIG1hY3JvOiBGTSBkZXB0aCwgZGV0dW5lLCBhbmQgZmlsdGVyIG1vdGlvbi4KICBjb25zdCBjb2xvciA9IGJvdW5kZWQodmFsdWUubiwgMC41LCAwLCAxKTsKICBjb25zdCBkdXJhdGlvbiA9IGJvdW5kZWQodmFsdWUuZHVyYXRpb24sIDAuNSwgMC4wMDEsIDM2MDApOwogIGNvbnN0IGF0dGFjayA9IE1hdGgubWluKGJvdW5kZWQodmFsdWUuYXR0YWNrLCBraW5kID09PSAnYmxvb20nID8gMC4xOCA6IDAuMDA2LCAwLCA2MCksIGR1cmF0aW9uKTsKICBjb25zdCBkZWNheSA9IE1hdGgubWluKGJvdW5kZWQodmFsdWUuZGVjYXksIGtpbmQgPT09ICdnbGFzcycgPyAwLjMgOiAwLjEsIDAsIDYwKSwgZHVyYXRpb24gLSBhdHRhY2spOwogIGNvbnN0IHN1c3RhaW4gPSBib3VuZGVkKHZhbHVlLnN1c3RhaW4sIGtpbmQgPT09ICdnbGFzcycgPyAwLjI0IDogMC44LCAwLCAxKTsKICBjb25zdCByZWxlYXNlID0gYm91bmRlZCh2YWx1ZS5yZWxlYXNlLCBraW5kID09PSAnZ2xhc3MnID8gMC4zNSA6IDAuNiwgMCwgNjApOwogIGNvbnN0IGhvbGRFbmQgPSB0aW1lICsgZHVyYXRpb247CiAgY29uc3QgZW5kID0gaG9sZEVuZCArIHJlbGVhc2UgKyAwLjAwNTsKICBjb25zdCBub2RlID0gY3R4LmNyZWF0ZUdhaW4oKTsKICBjb25zdCBhbGxOb2RlcyA9IFtub2RlXSwgb3NjaWxsYXRvcnMgPSBbXTsKICBjb25zdCBrZWVwID0gb2JqZWN0ID0+IHsgYWxsTm9kZXMucHVzaChvYmplY3QpOyByZXR1cm4gb2JqZWN0OyB9OwogIGNvbnN0IGdhaW4gPSBsZXZlbCA9PiB7CiAgICBjb25zdCBnID0ga2VlcChjdHguY3JlYXRlR2FpbigpKTsgZy5nYWluLnZhbHVlID0gbGV2ZWw7IHJldHVybiBnOwogIH07CiAgY29uc3Qgb3NjID0gKHR5cGUsIGh6KSA9PiB7CiAgICBjb25zdCBvID0ga2VlcChjdHguY3JlYXRlT3NjaWxsYXRvcigpKTsKICAgIG8udHlwZSA9IHR5cGU7CiAgICBvLmZyZXF1ZW5jeS52YWx1ZSA9IE1hdGgubWF4KDAuMDUsIE1hdGgubWluKGh6LCBjdHguc2FtcGxlUmF0ZSAqIDAuNCkpOwogICAgb3NjaWxsYXRvcnMucHVzaChvKTsgcmV0dXJuIG87CiAgfTsKICBjb25zdCBmaWx0ZXIgPSBrZWVwKGN0eC5jcmVhdGVCaXF1YWRGaWx0ZXIoKSk7CiAgZmlsdGVyLnR5cGUgPSAnbG93cGFzcyc7CiAgZmlsdGVyLlEudmFsdWUgPSAwLjU7CiAgY29uc3QgY3V0b2ZmID0gTWF0aC5taW4oY3R4LnNhbXBsZVJhdGUgKiAwLjQsIGZyZXF1ZW5jeSAqICg1ICsgY29sb3IgKiAxNSkpOwogIGZpbHRlci5mcmVxdWVuY3kuc2V0VmFsdWVBdFRpbWUoY3V0b2ZmLCB0aW1lKTsKICBmaWx0ZXIuZnJlcXVlbmN5LmV4cG9uZW50aWFsUmFtcFRvVmFsdWVBdFRpbWUoCiAgICBNYXRoLm1heCg2MCwgY3V0b2ZmICogKGtpbmQgPT09ICdnbGFzcycgPyAwLjI0IDogMC41NSkpLCBob2xkRW5kLAogICk7CiAgZmlsdGVyLmNvbm5lY3Qobm9kZSk7CiAgbm9kZS5nYWluLnNldFZhbHVlQXRUaW1lKGF0dGFjayA/IDAgOiAxLCB0aW1lKTsKICBpZiAoYXR0YWNrKSBub2RlLmdhaW4ubGluZWFyUmFtcFRvVmFsdWVBdFRpbWUoMSwgdGltZSArIGF0dGFjayk7CiAgbm9kZS5nYWluLmxpbmVhclJhbXBUb1ZhbHVlQXRUaW1lKHN1c3RhaW4sIHRpbWUgKyBhdHRhY2sgKyBkZWNheSk7CiAgbm9kZS5nYWluLnNldFZhbHVlQXRUaW1lKHN1c3RhaW4sIGhvbGRFbmQpOwogIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZSgwLCBob2xkRW5kICsgcmVsZWFzZSk7CgogIGNvbnN0IGxheWVycyA9IGtpbmQgPT09ICdnbGFzcycKICAgID8gW1sxLCAwLjI0LCAnc2luZScsIDBdLCBbMS41LCAwLjEwLCAndHJpYW5nbGUnLCAwXSwgWzIuNzU2LCAwLjA3LCAnc2luZScsIDBdXQogICAgOiBraW5kID09PSAnc3dhcm0nCiAgICAgID8gW1sxLCAwLjEyLCAnc2F3dG9vdGgnLCAtMV0sIFsxLCAwLjEyLCAnc2F3dG9vdGgnLCAxXSwgWzEuNSwgMC4wOSwgJ3RyaWFuZ2xlJywgMC40XSwgWzAuNSwgMC4xMCwgJ3NpbmUnLCAwXV0KICAgICAgOiBbWzEsIDAuMTUsICd0cmlhbmdsZScsIC0xXSwgWzEsIDAuMTIsICd0cmlhbmdsZScsIDFdLCBbMS41LCAwLjA5LCAnc2luZScsIC0wLjVdLCBbMiwgMC4wNiwgJ3NpbmUnLCAwLjVdXTsKICBsYXllcnMuZm9yRWFjaCgoW3JhdGlvLCBsZXZlbCwgd2F2ZSwgc3ByZWFkXSwgaSkgPT4gewogICAgY29uc3QgY2FycmllciA9IG9zYyh3YXZlLCBmcmVxdWVuY3kgKiByYXRpbyk7CiAgICBjYXJyaWVyLmRldHVuZS52YWx1ZSA9IHNwcmVhZCAqICg0ICsgY29sb3IgKiAxOCk7CiAgICBjb25zdCBwYW4gPSBrZWVwKGN0eC5jcmVhdGVTdGVyZW9QYW5uZXIoKSk7CiAgICBwYW4ucGFuLnZhbHVlID0gc3ByZWFkICogMC40NTsKICAgIGNvbnN0IHB1bHNlID0gZ2FpbihraW5kID09PSAnb3JiaXQnID8gMC43MiA6IDEpOwogICAgY2Fycmllci5jb25uZWN0KGdhaW4obGV2ZWwpKS5jb25uZWN0KHB1bHNlKS5jb25uZWN0KHBhbikuY29ubmVjdChmaWx0ZXIpOwogICAgaWYgKGtpbmQgPT09ICdvcmJpdCcpIHsKICAgICAgLy8gQW1wbGl0dWRlIHB1bHNlcyBsb2NrZWQgdG8gdGhlIHNjaGVkdWxlcidzIGN5Y2xlIHJhdGUuCiAgICAgIG9zYygnc2luZScsIGJvdW5kZWQoY3BzLCAwLjUsIDAuMDEsIDIwKSAqIChpICsgMikpCiAgICAgICAgLmNvbm5lY3QoZ2FpbigwLjI4KSkuY29ubmVjdChwdWxzZS5nYWluKTsKICAgIH0KICAgIGlmIChraW5kICE9PSAnc3dhcm0nKSB7CiAgICAgIGNvbnN0IG1vZCA9IG9zYygnc2luZScsIGZyZXF1ZW5jeSAqIHJhdGlvICogKGtpbmQgPT09ICdnbGFzcycgPyAyLjc1NiA6IDEuNSkpOwogICAgICBjb25zdCBkZXB0aCA9IGdhaW4oZnJlcXVlbmN5ICogcmF0aW8gKiAoMC4wOCArIGNvbG9yICogKGtpbmQgPT09ICdnbGFzcycgPyAyLjQgOiAwLjYpKSk7CiAgICAgIG1vZC5jb25uZWN0KGRlcHRoKS5jb25uZWN0KGNhcnJpZXIuZnJlcXVlbmN5KTsKICAgICAgZGVwdGguZ2Fpbi5zZXRWYWx1ZUF0VGltZShkZXB0aC5nYWluLnZhbHVlLCB0aW1lKTsKICAgICAgZGVwdGguZ2Fpbi5leHBvbmVudGlhbFJhbXBUb1ZhbHVlQXRUaW1lKAogICAgICAgIE1hdGgubWF4KDAuMDAxLCBkZXB0aC5nYWluLnZhbHVlICogMC4wOCksIHRpbWUgKyBNYXRoLm1pbihkdXJhdGlvbiwgMC4zICsgY29sb3IpLAogICAgICApOwogICAgICBpZiAoa2luZCA9PT0gJ29yYml0JykgewogICAgICAgIC8vIENhcnJpZXIgLT4gZGVsYXkgLT4gbW9kdWxhdG9yIHBpdGNoIC0+IGNhcnJpZXIgcGl0Y2guIFRoZSBkZWxheQogICAgICAgIC8vIG1ha2VzIHRoZSBmZWVkYmFjayBjeWNsZSB2YWxpZCwgd2hpbGUgc2luZSBvdXRwdXRzIGJvdW5kIGl0cyBsZXZlbC4KICAgICAgICBjb25zdCBmZWVkYmFjayA9IGtlZXAoY3R4LmNyZWF0ZURlbGF5KDAuMDUpKTsKICAgICAgICBmZWVkYmFjay5kZWxheVRpbWUudmFsdWUgPSAwLjAwNCArIGkgKiAwLjAwMjsKICAgICAgICBjYXJyaWVyLmNvbm5lY3QoZmVlZGJhY2spLmNvbm5lY3QoZ2FpbihmcmVxdWVuY3kgKiAoMC4xNSArIGNvbG9yICogMS40KSkpCiAgICAgICAgICAuY29ubmVjdChtb2QuZnJlcXVlbmN5KTsKICAgICAgICBjb25zdCBzaGFwZSA9IG9zYygnc2luZScsIDAuMTMgKyBpICogMC4wNyk7CiAgICAgICAgc2hhcGUuY29ubmVjdChnYWluKGZyZXF1ZW5jeSAqIHJhdGlvICogY29sb3IgKiAwLjI0KSkuY29ubmVjdChkZXB0aC5nYWluKTsKICAgICAgfQogICAgfQogICAgY29uc3QgZHJpZnQgPSBvc2MoJ3NpbmUnLCAwLjE5ICsgaSAqIDAuMTEgKyBjb2xvciAqIDAuMzUpOwogICAgZHJpZnQuY29ubmVjdChnYWluKDIgKyBjb2xvciAqIDYpKS5jb25uZWN0KGNhcnJpZXIuZGV0dW5lKTsKICAgIGlmIChraW5kID09PSAnYmxvb20nIHx8IGtpbmQgPT09ICdvcmJpdCcpIGRyaWZ0LmNvbm5lY3QoZ2FpbigwLjMpKS5jb25uZWN0KHBhbi5wYW4pOwogIH0pOwogIGxldCByZW1haW5pbmcgPSBvc2NpbGxhdG9ycy5sZW5ndGgsIGZpbmlzaGVkID0gZmFsc2U7CiAgZm9yIChjb25zdCBvc2NpbGxhdG9yIG9mIG9zY2lsbGF0b3JzKSB7CiAgICBvc2NpbGxhdG9yLm9uZW5kZWQgPSAoKSA9PiB7CiAgICAgIGlmICgtLXJlbWFpbmluZyA9PT0gMCAmJiAhZmluaXNoZWQpIHsKICAgICAgICBmaW5pc2hlZCA9IHRydWU7CiAgICAgICAgYWxsTm9kZXMuZm9yRWFjaChuID0+IG4uZGlzY29ubmVjdCgpKTsKICAgICAgICBvbmVuZGVkPy4oKTsKICAgICAgfQogICAgfTsKICAgIG9zY2lsbGF0b3Iuc3RhcnQodGltZSk7CiAgICBvc2NpbGxhdG9yLnN0b3AoZW5kKTsKICB9CiAgcmV0dXJuIHsKICAgIG5vZGUsCiAgICBzdG9wKGF0KSB7CiAgICAgIGlmIChmaW5pc2hlZCkgcmV0dXJuOwogICAgICBjb25zdCBzdG9wQXQgPSBNYXRoLm1heChjdHguY3VycmVudFRpbWUsIE1hdGgubWluKGF0LCBlbmQpKTsKICAgICAgbm9kZS5nYWluLmNhbmNlbEFuZEhvbGRBdFRpbWUoc3RvcEF0KTsKICAgICAgbm9kZS5nYWluLmxpbmVhclJhbXBUb1ZhbHVlQXRUaW1lKDAsIHN0b3BBdCArIDAuMDA1KTsKICAgICAgZm9yIChjb25zdCBvIG9mIG9zY2lsbGF0b3JzKSBvLnN0b3AoTWF0aC5taW4oZW5kLCBzdG9wQXQgKyAwLjAwNSkpOwogICAgfSwKICB9Owp9CgpmdW5jdGlvbiByZWdpc3Rlck5vdGVwYXQoYXBpID0gZ2xvYmFsVGhpcykgewogIGNvbnN0IHsgcmVnaXN0ZXJTb3VuZCwgZ2V0QXVkaW9Db250ZXh0LCBnZXRGcmVxdWVuY3lGcm9tVmFsdWUgfSA9IGFwaTsKICBpZiAoIVtyZWdpc3RlclNvdW5kLCBnZXRBdWRpb0NvbnRleHQsIGdldEZyZXF1ZW5jeUZyb21WYWx1ZV0uZXZlcnkoZm4gPT4gdHlwZW9mIGZuID09PSAnZnVuY3Rpb24nKSkgewogICAgdGhyb3cgbmV3IEVycm9yKCdJbXBvcnQgTm90ZXBhdCBpbnNpZGUgU3RydWRlbCwgb3IgcGFzcyBpdHMgYXVkaW8gQVBJIHRvIHJlZ2lzdGVyTm90ZXBhdCgpLicpOwogIH0KICBmb3IgKGNvbnN0IFtuYW1lLCByZWNpcGVdIG9mIE9iamVjdC5lbnRyaWVzKHZvaWNlcykpIHsKICAgIHJlZ2lzdGVyU291bmQobmFtZSwgKHRpbWUsIHZhbHVlLCBvbmVuZGVkLCBjcHMpID0+IHsKICAgICAgY29uc3QgY3R4ID0gZ2V0QXVkaW9Db250ZXh0KCk7CiAgICAgIGNvbnN0IGZyZXF1ZW5jeSA9IGJvdW5kZWQoZ2V0RnJlcXVlbmN5RnJvbVZhbHVlKHZhbHVlKSwgMjIwLCAxLCBjdHguc2FtcGxlUmF0ZSAvIDIgLSAxKTsKICAgICAgaWYgKHJlY2lwZS5tb2RhbCkgcmV0dXJuIG1hcmltYmFWb2ljZShjdHgsIHRpbWUsIHZhbHVlLCBmcmVxdWVuY3ksIG9uZW5kZWQsIHJlY2lwZS5tb2RhbCA9PT0gJ3N0b25lJyk7CiAgICAgIGlmIChyZWNpcGUuY2hpcnApIHJldHVybiBjaGlycFZvaWNlKGN0eCwgdGltZSwgdmFsdWUsIGZyZXF1ZW5jeSwgcmVjaXBlLmNoaXJwLCBvbmVuZGVkKTsKICAgICAgaWYgKHJlY2lwZS5yaWNoKSByZXR1cm4gcmljaFZvaWNlKGN0eCwgdGltZSwgdmFsdWUsIGZyZXF1ZW5jeSwgcmVjaXBlLnJpY2gsIG9uZW5kZWQsIGNwcyk7CiAgICAgIGNvbnN0IGR1cmF0aW9uID0gYm91bmRlZCh2YWx1ZS5kdXJhdGlvbiwgMC41LCAwLjAwMSwgMzYwMCk7CiAgICAgIGNvbnN0IGF0dGFjayA9IE1hdGgubWluKGJvdW5kZWQodmFsdWUuYXR0YWNrLCAwLjAyLCAwLCA2MCksIGR1cmF0aW9uKTsKICAgICAgY29uc3QgZGVjYXkgPSBNYXRoLm1pbihib3VuZGVkKHZhbHVlLmRlY2F5LCAwLCAwLCA2MCksIGR1cmF0aW9uIC0gYXR0YWNrKTsKICAgICAgY29uc3Qgc3VzdGFpbiA9IGJvdW5kZWQodmFsdWUuc3VzdGFpbiwgMSwgMCwgMSk7CiAgICAgIGNvbnN0IHJlbGVhc2UgPSBib3VuZGVkKHZhbHVlLnJlbGVhc2UsIDAuMSwgMCwgNjApOwogICAgICBjb25zdCBob2xkRW5kID0gdGltZSArIGR1cmF0aW9uOwogICAgICBjb25zdCBlbmQgPSBob2xkRW5kICsgcmVsZWFzZSArIDAuMDA1OwogICAgICBjb25zdCBub2RlID0gY3R4LmNyZWF0ZUdhaW4oKTsKICAgICAgbm9kZS5nYWluLnNldFZhbHVlQXRUaW1lKGF0dGFjayA/IDAgOiAxLCB0aW1lKTsKICAgICAgaWYgKGF0dGFjaykgbm9kZS5nYWluLmxpbmVhclJhbXBUb1ZhbHVlQXRUaW1lKDEsIHRpbWUgKyBhdHRhY2spOwogICAgICBub2RlLmdhaW4ubGluZWFyUmFtcFRvVmFsdWVBdFRpbWUoc3VzdGFpbiwgdGltZSArIGF0dGFjayArIGRlY2F5KTsKICAgICAgbm9kZS5nYWluLnNldFZhbHVlQXRUaW1lKHN1c3RhaW4sIGhvbGRFbmQpOwogICAgICBub2RlLmdhaW4ubGluZWFyUmFtcFRvVmFsdWVBdFRpbWUoMCwgaG9sZEVuZCArIHJlbGVhc2UpOwoKICAgICAgY29uc3Qgb3NjaWxsYXRvcnMgPSBbXTsKICAgICAgbGV0IHJlbWFpbmluZyA9IHJlY2lwZS5yYXRpb3MubGVuZ3RoOwogICAgICBsZXQgZmluaXNoZWQgPSBmYWxzZTsKICAgICAgZm9yIChsZXQgaSA9IDA7IGkgPCByZWNpcGUucmF0aW9zLmxlbmd0aDsgaSsrKSB7CiAgICAgICAgY29uc3Qgb3NjaWxsYXRvciA9IGN0eC5jcmVhdGVPc2NpbGxhdG9yKCk7CiAgICAgICAgb3NjaWxsYXRvci50eXBlID0gcmVjaXBlLndhdmU7CiAgICAgICAgb3NjaWxsYXRvci5mcmVxdWVuY3kudmFsdWUgPSBNYXRoLm1pbihmcmVxdWVuY3kgKiByZWNpcGUucmF0aW9zW2ldLCBjdHguc2FtcGxlUmF0ZSAvIDIgLSAxKTsKICAgICAgICBjb25zdCBnYWluID0gY3R4LmNyZWF0ZUdhaW4oKTsKICAgICAgICAvLyBQcmVzZXJ2ZSB0aGUgbmF0aXZlIHRyaWFuZ2xlIGxheWVyIGJhbGFuY2UuIENob3JkcyBzaGFyZSBoZWFkcm9vbS4KICAgICAgICBnYWluLmdhaW4udmFsdWUgPSAocmVjaXBlLmxldmVscz8uW2ldID8/IDAuNSAvIHJlY2lwZS5yYXRpb3MubGVuZ3RoKQogICAgICAgICAgKiAocmVjaXBlLndhdmUgPT09ICd0cmlhbmdsZScgPyAtMSA6IDEpOwogICAgICAgIG9zY2lsbGF0b3IuY29ubmVjdChnYWluKS5jb25uZWN0KG5vZGUpOwogICAgICAgIG9zY2lsbGF0b3Iub25lbmRlZCA9ICgpID0+IHsKICAgICAgICAgIG9zY2lsbGF0b3IuZGlzY29ubmVjdCgpOwogICAgICAgICAgZ2Fpbi5kaXNjb25uZWN0KCk7CiAgICAgICAgICBpZiAoLS1yZW1haW5pbmcgPT09IDAgJiYgIWZpbmlzaGVkKSB7CiAgICAgICAgICAgIGZpbmlzaGVkID0gdHJ1ZTsKICAgICAgICAgICAgbm9kZS5kaXNjb25uZWN0KCk7CiAgICAgICAgICAgIG9uZW5kZWQ/LigpOwogICAgICAgICAgfQogICAgICAgIH07CiAgICAgICAgb3NjaWxsYXRvci5zdGFydCh0aW1lKTsKICAgICAgICBvc2NpbGxhdG9yLnN0b3AoZW5kKTsKICAgICAgICBvc2NpbGxhdG9ycy5wdXNoKG9zY2lsbGF0b3IpOwogICAgICB9CiAgICAgIHJldHVybiB7CiAgICAgICAgbm9kZSwKICAgICAgICAvLyBTdHJ1ZGVsIGNhbGxzIHRoaXMgd2hlbiByZXRpcmluZyBhIHZvaWNlLCBpbmNsdWRpbmcgcG9seXBob255IHN0ZWFscy4KICAgICAgICBzdG9wKGF0KSB7CiAgICAgICAgICBpZiAoZmluaXNoZWQpIHJldHVybjsKICAgICAgICAgIGNvbnN0IHN0b3BBdCA9IE1hdGgubWF4KGN0eC5jdXJyZW50VGltZSwgTWF0aC5taW4oYXQsIGVuZCkpOwogICAgICAgICAgbm9kZS5nYWluLmNhbmNlbEFuZEhvbGRBdFRpbWUoc3RvcEF0KTsKICAgICAgICAgIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZSgwLCBzdG9wQXQgKyAwLjAwNSk7CiAgICAgICAgICBmb3IgKGNvbnN0IG9zY2lsbGF0b3Igb2Ygb3NjaWxsYXRvcnMpIG9zY2lsbGF0b3Iuc3RvcChNYXRoLm1pbihlbmQsIHN0b3BBdCArIDAuMDA1KSk7CiAgICAgICAgfSwKICAgICAgfTsKICAgIH0sIHsgdHlwZTogJ3N5bnRoJyB9KTsKICB9CiAgcmV0dXJuIE9iamVjdC5rZXlzKHZvaWNlcyk7Cn0KCnJlZ2lzdGVyTm90ZXBhdCgpOwoKfSkoKQoKLy8gTUFSSU1CQUJBIOKAlCBAamVmZnJleSAvIGFlc3RoZXRpYy5jb21wdXRlcgovLyBodHRwczovL3BhdC5hZXN0aGV0aWMuY29tcHV0ZXIvbWFyaW1iYWJhCi8vIExlYWQ6IHBvcC9tYXJpbWJhL21hcmltYmFiYS5ucCDigJQgYWxsIDI0IGJhcnMsIG9yaWdpbmFsIHBpdGNoZXMgYW5kIGR1cmF0aW9ucy4KLy8gQWNjb21wYW5pbWVudDogYSByZWR1Y2VkIGFycmFuZ2VtZW50IGluZm9ybWVkIGJ5IGJpbi9yZW5kZXItbWFyaW1iYWJhLm1qcy4KLy8gVGhpcyBpcyBhbiBlZGl0YWJsZSBzY29yZSBhZGFwdGF0aW9uLCBub3QgcGxheWJhY2sgb2YgdGhlIHJlbGVhc2VkIG1hc3Rlci4KCi8vIFRFTVBPIOKAlCBvbmUgY3ljbGUgPSBvbmUgMy80IGJhci4gT3JpZ2luYWwgNTYgQlBNLgpzZXRjcG0oNTYvMykKCi8vIE1FTE9EWSDigJQgQCB3ZWlnaHRzIGFyZSBiZWF0cy4gbWluaSguLi4pIHBhcnNlcyB0aGUgYXNzZW1ibGVkIHN0cmluZ3MuCi8vIEZpdmUgc2VjdGlvbnM6IGh1c2gsIHR3aW5rbGUsIHdvdywgYmEtYmEsIHNsZWVwLiBUb3RhbDogNzIgYmVhdHMgLyAyNCBiYXJzLgpjb25zdCBtZWxvZHkgPSBtaW5pKFsKICAvLyBodXNoOiA0IGJhcnMKICAnYzVAMSBhNEAxIGY0QDEgZjRAMyBhNEAxLjUgZzRAMS41IGY0QDMnLAogIC8vIHR3aW5rbGU6IDYgYmFycwogICdmNUAxIGE1QDEgYzZAMC41IGE1QDAuNSBnNUAzIGY1QDEgZzVAMSBhNUAwLjUgZzVAMC41IGY1QDMgYmI1QDEgZDZAMSBiYjVAMSBjNkAxLjUgYTVAMS41JywKICAvLyB3b3c6IDQgYmFycwogICdbZzUsYmI1XUA2IGE1QDEgZzVAMSBhNUAxIGY1QDMnLAogIC8vIGJhYmE6IDQgYmFycwogICdhNUAwLjUgZzVAMC41IGE1QDEgZjVAMSBjNkAwLjUgYmI1QDAuNSBjNkAxIGE1QDEgZzVAMSBmNUAxIGU1QDEgZjVAMycsCiAgLy8gc2xlZXA6IDYgYmFycwogICdjNUAxLjUgYTRAMS41IGc0QDEgZjRAMSBmNEAxIGE0QDEgZzRAMSBmNEAxIGY0QDMgZjNAMyB+QDMnLApdLmpvaW4oJyAnKSkubm90ZSgpLnNsb3coMjQpCgovLyBTSU5HRVIg4oCUIGFjX21hcmltYmEgdXNlcyB0aGUgc291cmNlIHJvc2V3b29kIHJhdGlvcyAxOjQ6OS4yLgovLyAubigwLi4xKSBjaGFuZ2VzIG1hbGxldCBicmlnaHRuZXNzIGFuZCByaW5nOyBjaGFuZ2UgLnMoLi4uKSB0byByZWNhc3QgaXQuCiQ6IG1lbG9keS5zKCdhY19tYXJpbWJhJykubiguNDUpCiAgLmF0dGFjayguMDAxKS5yZWxlYXNlKC41KS5nYWluKC44KS5yb29tKC4zKS5fcGlhbm9yb2xsKCkKCi8vIFJPQ0tJTkcgQkFTUyDigJQgc3RhcnRzIG9uIGJlYXQgb25lLiBSb290cyBmb2xsb3cgdGhlIHNvdXJjZSBiYXNzIGZpZ3VyZS4KJDogbm90ZSgiPGYyIGYyIGYyIGYyIGYyIGYzIGYyIGYzIGYyIGYzIGYyIGYyIGYyIGYyIGYyIGMzIGJiMSBmMiBmMiBmMiBmMiBmMiBmMiBmMj4iKS5zKCdhY19zaW5lJykKICAubiguMTUpLmF0dGFjayguMDA4KS5kZWNheSguMjUpLnN1c3RhaW4oLjI1KS5yZWxlYXNlKC4zKS5nYWluKC4zOCkubHBmKDQ1MCkKCi8vIEhBUk1PTlkg4oCUIEYgLyBCYiAvIEdtIC8gRiAvIEMgLyBGLCBmb2xsb3dpbmcgdGhlIHJlbmRlcmVyJ3MgY2hvcmQgbWFwLgovLyBSZWFydGljdWxhdGVkIG9uY2UgcGVyIGJhcjsgaW50ZW50aW9uYWxseSByZWR1Y2VkIGZyb20gdGhlIGZpbmlzaGVkIG1peC4KJDogbm90ZSgiPFtmMyxhMyxjNF0gW2YzLGEzLGM0XSBbZjMsYTMsYzRdIFtmMyxhMyxjNF0gW2YzLGEzLGM0XSBbZjMsYTMsYzRdIFtmMyxhMyxjNF0gW2JiMixkMyxmM10gW2JiMixkMyxmM10gW2JiMixkMyxmM10gW2czLGJiMyxkNF0gW2czLGJiMyxkNF0gW2czLGJiMyxkNF0gW2czLGJiMyxkNF0gW2YzLGEzLGM0XSBbYzMsZTMsZzNdIFtmMyxhMyxjNF0gW2YzLGEzLGM0XSBbZjMsYTMsYzRdIFtmMyxhMyxjNF0gW2YzLGEzLGM0XSBbZjMsYTMsYzRdIFtmMyxhMyxjNF0gW2YzLGEzLGM0XT4iKS5zKCdhY19ibG9vbScpCiAgLm4oLjA4KS5hdHRhY2soLjA2KS5yZWxlYXNlKC42NSkuZ2FpbiguMTYpLmxwZigxODAwKS5yb29tKC40KQoKLy8gQWxsIGFjdGl2ZSBibG9ja3MgcGxheSB0b2dldGhlci4gQ29tbWVudCBvdXQgYSB3aG9sZSAkOiBibG9jayB0byBtdXRlIGl0LgovLyBWb2ljZXMvY29udHJvbHM6IGh0dHBzOi8vcGF0LmFlc3RoZXRpYy5jb21wdXRlci9zb3VyY2UK diff --git a/ac-strudel/marimbaba-score.json b/ac-strudel/marimbaba-score.json new file mode 100644 index 0000000000..f085bce637 --- /dev/null +++ b/ac-strudel/marimbaba-score.json @@ -0,0 +1,356 @@ +{ + "bars": 24, + "beats": 72, + "bpm": 56, + "events": [ + { + "pitches": [ + "c5" + ], + "beats": 1, + "at": 0 + }, + { + "pitches": [ + "a4" + ], + "beats": 1, + "at": 1 + }, + { + "pitches": [ + "f4" + ], + "beats": 1, + "at": 2 + }, + { + "pitches": [ + "f4" + ], + "beats": 3, + "at": 3 + }, + { + "pitches": [ + "a4" + ], + "beats": 1.5, + "at": 6 + }, + { + "pitches": [ + "g4" + ], + "beats": 1.5, + "at": 7.5 + }, + { + "pitches": [ + "f4" + ], + "beats": 3, + "at": 9 + }, + { + "pitches": [ + "f5" + ], + "beats": 1, + "at": 12 + }, + { + "pitches": [ + "a5" + ], + "beats": 1, + "at": 13 + }, + { + "pitches": [ + "c6" + ], + "beats": 0.5, + "at": 14 + }, + { + "pitches": [ + "a5" + ], + "beats": 0.5, + "at": 14.5 + }, + { + "pitches": [ + "g5" + ], + "beats": 3, + "at": 15 + }, + { + "pitches": [ + "f5" + ], + "beats": 1, + "at": 18 + }, + { + "pitches": [ + "g5" + ], + "beats": 1, + "at": 19 + }, + { + "pitches": [ + "a5" + ], + "beats": 0.5, + "at": 20 + }, + { + "pitches": [ + "g5" + ], + "beats": 0.5, + "at": 20.5 + }, + { + "pitches": [ + "f5" + ], + "beats": 3, + "at": 21 + }, + { + "pitches": [ + "bb5" + ], + "beats": 1, + "at": 24 + }, + { + "pitches": [ + "d6" + ], + "beats": 1, + "at": 25 + }, + { + "pitches": [ + "bb5" + ], + "beats": 1, + "at": 26 + }, + { + "pitches": [ + "c6" + ], + "beats": 1.5, + "at": 27 + }, + { + "pitches": [ + "a5" + ], + "beats": 1.5, + "at": 28.5 + }, + { + "pitches": [ + "g5", + "bb5" + ], + "beats": 6, + "at": 30 + }, + { + "pitches": [ + "a5" + ], + "beats": 1, + "at": 36 + }, + { + "pitches": [ + "g5" + ], + "beats": 1, + "at": 37 + }, + { + "pitches": [ + "a5" + ], + "beats": 1, + "at": 38 + }, + { + "pitches": [ + "f5" + ], + "beats": 3, + "at": 39 + }, + { + "pitches": [ + "a5" + ], + "beats": 0.5, + "at": 42 + }, + { + "pitches": [ + "g5" + ], + "beats": 0.5, + "at": 42.5 + }, + { + "pitches": [ + "a5" + ], + "beats": 1, + "at": 43 + }, + { + "pitches": [ + "f5" + ], + "beats": 1, + "at": 44 + }, + { + "pitches": [ + "c6" + ], + "beats": 0.5, + "at": 45 + }, + { + "pitches": [ + "bb5" + ], + "beats": 0.5, + "at": 45.5 + }, + { + "pitches": [ + "c6" + ], + "beats": 1, + "at": 46 + }, + { + "pitches": [ + "a5" + ], + "beats": 1, + "at": 47 + }, + { + "pitches": [ + "g5" + ], + "beats": 1, + "at": 48 + }, + { + "pitches": [ + "f5" + ], + "beats": 1, + "at": 49 + }, + { + "pitches": [ + "e5" + ], + "beats": 1, + "at": 50 + }, + { + "pitches": [ + "f5" + ], + "beats": 3, + "at": 51 + }, + { + "pitches": [ + "c5" + ], + "beats": 1.5, + "at": 54 + }, + { + "pitches": [ + "a4" + ], + "beats": 1.5, + "at": 55.5 + }, + { + "pitches": [ + "g4" + ], + "beats": 1, + "at": 57 + }, + { + "pitches": [ + "f4" + ], + "beats": 1, + "at": 58 + }, + { + "pitches": [ + "f4" + ], + "beats": 1, + "at": 59 + }, + { + "pitches": [ + "a4" + ], + "beats": 1, + "at": 60 + }, + { + "pitches": [ + "g4" + ], + "beats": 1, + "at": 61 + }, + { + "pitches": [ + "f4" + ], + "beats": 1, + "at": 62 + }, + { + "pitches": [ + "f4" + ], + "beats": 3, + "at": 63 + }, + { + "pitches": [ + "f3" + ], + "beats": 3, + "at": 66 + }, + { + "pitches": [], + "beats": 3, + "at": 69 + } + ] +} diff --git a/ac-strudel/marimbaba.strudel b/ac-strudel/marimbaba.strudel new file mode 100644 index 0000000000..23ada16162 --- /dev/null +++ b/ac-strudel/marimbaba.strudel @@ -0,0 +1,41 @@ +// MARIMBABA — @jeffrey / aesthetic.computer +// https://pat.aesthetic.computer/marimbaba +// Lead: pop/marimba/marimbaba.np — all 24 bars, original pitches and durations. +// Accompaniment: a reduced arrangement informed by bin/render-marimbaba.mjs. +// This is an editable score adaptation, not playback of the released master. +await import('https://pat.aesthetic.computer/s') + +// TEMPO — one cycle = one 3/4 bar. Original 56 BPM. +setcpm(56/3) + +// MELODY — @ weights are beats. mini(...) parses the assembled strings. +// Five sections: hush, twinkle, wow, ba-ba, sleep. Total: 72 beats / 24 bars. +const melody = mini([ + // hush: 4 bars + 'c5@1 a4@1 f4@1 f4@3 a4@1.5 g4@1.5 f4@3', + // twinkle: 6 bars + 'f5@1 a5@1 c6@0.5 a5@0.5 g5@3 f5@1 g5@1 a5@0.5 g5@0.5 f5@3 bb5@1 d6@1 bb5@1 c6@1.5 a5@1.5', + // wow: 4 bars + '[g5,bb5]@6 a5@1 g5@1 a5@1 f5@3', + // baba: 4 bars + 'a5@0.5 g5@0.5 a5@1 f5@1 c6@0.5 bb5@0.5 c6@1 a5@1 g5@1 f5@1 e5@1 f5@3', + // sleep: 6 bars + 'c5@1.5 a4@1.5 g4@1 f4@1 f4@1 a4@1 g4@1 f4@1 f4@3 f3@3 ~@3', +].join(' ')).note().slow(24) + +// SINGER — ac_marimba uses the source rosewood ratios 1:4:9.2. +// .n(0..1) changes mallet brightness and ring; change .s(...) to recast it. +$: melody.s('ac_marimba').n(.45) + .attack(.001).release(.5).gain(.8).room(.3)._pianoroll() + +// ROCKING BASS — starts on beat one. Roots follow the source bass figure. +$: note("").s('ac_sine') + .n(.15).attack(.008).decay(.25).sustain(.25).release(.3).gain(.38).lpf(450) + +// HARMONY — F / Bb / Gm / F / C / F, following the renderer's chord map. +// Rearticulated once per bar; intentionally reduced from the finished mix. +$: note("<[f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [bb2,d3,f3] [bb2,d3,f3] [bb2,d3,f3] [g3,bb3,d4] [g3,bb3,d4] [g3,bb3,d4] [g3,bb3,d4] [f3,a3,c4] [c3,e3,g3] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4] [f3,a3,c4]>").s('ac_bloom') + .n(.08).attack(.06).release(.65).gain(.16).lpf(1800).room(.4) + +// All active blocks play together. Comment out a whole $: block to mute it. +// Voices/controls: https://pat.aesthetic.computer/source diff --git a/ac-strudel/marimbaba.url b/ac-strudel/marimbaba.url new file mode 100644 index 0000000000..ae48d8def5 --- /dev/null +++ b/ac-strudel/marimbaba.url @@ -0,0 +1 @@ +https://strudel.cc/#Ly8gTUFSSU1CQUJBIOKAlCBAamVmZnJleSAvIGFlc3RoZXRpYy5jb21wdXRlcgovLyBodHRwczovL3BhdC5hZXN0aGV0aWMuY29tcHV0ZXIvbWFyaW1iYWJhCi8vIExlYWQ6IHBvcC9tYXJpbWJhL21hcmltYmFiYS5ucCDigJQgYWxsIDI0IGJhcnMsIG9yaWdpbmFsIHBpdGNoZXMgYW5kIGR1cmF0aW9ucy4KLy8gQWNjb21wYW5pbWVudDogYSByZWR1Y2VkIGFycmFuZ2VtZW50IGluZm9ybWVkIGJ5IGJpbi9yZW5kZXItbWFyaW1iYWJhLm1qcy4KLy8gVGhpcyBpcyBhbiBlZGl0YWJsZSBzY29yZSBhZGFwdGF0aW9uLCBub3QgcGxheWJhY2sgb2YgdGhlIHJlbGVhc2VkIG1hc3Rlci4KYXdhaXQgaW1wb3J0KCdodHRwczovL3BhdC5hZXN0aGV0aWMuY29tcHV0ZXIvcycpCgovLyBURU1QTyDigJQgb25lIGN5Y2xlID0gb25lIDMvNCBiYXIuIE9yaWdpbmFsIDU2IEJQTS4Kc2V0Y3BtKDU2LzMpCgovLyBNRUxPRFkg4oCUIEAgd2VpZ2h0cyBhcmUgYmVhdHMuIG1pbmkoLi4uKSBwYXJzZXMgdGhlIGFzc2VtYmxlZCBzdHJpbmdzLgovLyBGaXZlIHNlY3Rpb25zOiBodXNoLCB0d2lua2xlLCB3b3csIGJhLWJhLCBzbGVlcC4gVG90YWw6IDcyIGJlYXRzIC8gMjQgYmFycy4KY29uc3QgbWVsb2R5ID0gbWluaShbCiAgLy8gaHVzaDogNCBiYXJzCiAgJ2M1QDEgYTRAMSBmNEAxIGY0QDMgYTRAMS41IGc0QDEuNSBmNEAzJywKICAvLyB0d2lua2xlOiA2IGJhcnMKICAnZjVAMSBhNUAxIGM2QDAuNSBhNUAwLjUgZzVAMyBmNUAxIGc1QDEgYTVAMC41IGc1QDAuNSBmNUAzIGJiNUAxIGQ2QDEgYmI1QDEgYzZAMS41IGE1QDEuNScsCiAgLy8gd293OiA0IGJhcnMKICAnW2c1LGJiNV1ANiBhNUAxIGc1QDEgYTVAMSBmNUAzJywKICAvLyBiYWJhOiA0IGJhcnMKICAnYTVAMC41IGc1QDAuNSBhNUAxIGY1QDEgYzZAMC41IGJiNUAwLjUgYzZAMSBhNUAxIGc1QDEgZjVAMSBlNUAxIGY1QDMnLAogIC8vIHNsZWVwOiA2IGJhcnMKICAnYzVAMS41IGE0QDEuNSBnNEAxIGY0QDEgZjRAMSBhNEAxIGc0QDEgZjRAMSBmNEAzIGYzQDMgfkAzJywKXS5qb2luKCcgJykpLm5vdGUoKS5zbG93KDI0KQoKLy8gU0lOR0VSIOKAlCBhY19tYXJpbWJhIHVzZXMgdGhlIHNvdXJjZSByb3Nld29vZCByYXRpb3MgMTo0OjkuMi4KLy8gLm4oMC4uMSkgY2hhbmdlcyBtYWxsZXQgYnJpZ2h0bmVzcyBhbmQgcmluZzsgY2hhbmdlIC5zKC4uLikgdG8gcmVjYXN0IGl0LgokOiBtZWxvZHkucygnYWNfbWFyaW1iYScpLm4oLjQ1KQogIC5hdHRhY2soLjAwMSkucmVsZWFzZSguNSkuZ2FpbiguOCkucm9vbSguMykuX3BpYW5vcm9sbCgpCgovLyBST0NLSU5HIEJBU1Mg4oCUIHN0YXJ0cyBvbiBiZWF0IG9uZS4gUm9vdHMgZm9sbG93IHRoZSBzb3VyY2UgYmFzcyBmaWd1cmUuCiQ6IG5vdGUoIjxmMiBmMiBmMiBmMiBmMiBmMyBmMiBmMyBmMiBmMyBmMiBmMiBmMiBmMiBmMiBjMyBiYjEgZjIgZjIgZjIgZjIgZjIgZjIgZjI+IikucygnYWNfc2luZScpCiAgLm4oLjE1KS5hdHRhY2soLjAwOCkuZGVjYXkoLjI1KS5zdXN0YWluKC4yNSkucmVsZWFzZSguMykuZ2FpbiguMzgpLmxwZig0NTApCgovLyBIQVJNT05ZIOKAlCBGIC8gQmIgLyBHbSAvIEYgLyBDIC8gRiwgZm9sbG93aW5nIHRoZSByZW5kZXJlcidzIGNob3JkIG1hcC4KLy8gUmVhcnRpY3VsYXRlZCBvbmNlIHBlciBiYXI7IGludGVudGlvbmFsbHkgcmVkdWNlZCBmcm9tIHRoZSBmaW5pc2hlZCBtaXguCiQ6IG5vdGUoIjxbZjMsYTMsYzRdIFtmMyxhMyxjNF0gW2YzLGEzLGM0XSBbZjMsYTMsYzRdIFtmMyxhMyxjNF0gW2YzLGEzLGM0XSBbZjMsYTMsYzRdIFtiYjIsZDMsZjNdIFtiYjIsZDMsZjNdIFtiYjIsZDMsZjNdIFtnMyxiYjMsZDRdIFtnMyxiYjMsZDRdIFtnMyxiYjMsZDRdIFtnMyxiYjMsZDRdIFtmMyxhMyxjNF0gW2MzLGUzLGczXSBbZjMsYTMsYzRdIFtmMyxhMyxjNF0gW2YzLGEzLGM0XSBbZjMsYTMsYzRdIFtmMyxhMyxjNF0gW2YzLGEzLGM0XSBbZjMsYTMsYzRdIFtmMyxhMyxjNF0+IikucygnYWNfYmxvb20nKQogIC5uKC4wOCkuYXR0YWNrKC4wNikucmVsZWFzZSguNjUpLmdhaW4oLjE2KS5scGYoMTgwMCkucm9vbSguNCkKCi8vIEFsbCBhY3RpdmUgYmxvY2tzIHBsYXkgdG9nZXRoZXIuIENvbW1lbnQgb3V0IGEgd2hvbGUgJDogYmxvY2sgdG8gbXV0ZSBpdC4KLy8gVm9pY2VzL2NvbnRyb2xzOiBodHRwczovL3BhdC5hZXN0aGV0aWMuY29tcHV0ZXIvc291cmNlCg== diff --git a/ac-strudel/notepat-paste.strudel b/ac-strudel/notepat-paste.strudel new file mode 100644 index 0000000000..cb07ea0de2 --- /dev/null +++ b/ac-strudel/notepat-paste.strudel @@ -0,0 +1,360 @@ +(() => { +// AC OS Notepat voices for Strudel. https://aesthetic.computer +// Recipes: fedac/native/pieces/notepat.mjs (triangle Shift alternate and chords). +// Uses Web Audio's band-limited oscillators; this is not a recording of the +// native mixer, its echo, or its output saturation. +// Glass, swarm, bloom, and orbit extend those recipes as new Strudel sounds. + +const voices = Object.freeze({ + ac_triangle: { wave: 'triangle', ratios: [1], levels: [0.5] }, + ac_triangle_fifth: { wave: 'triangle', ratios: [1, 1.5], levels: [0.42, 0.12] }, + ac_sine: { wave: 'sine', ratios: [1], levels: [0.5] }, + ac_major: { wave: 'sine', ratios: [1, 2 ** (4 / 12), 2 ** (7 / 12)] }, + ac_minor: { wave: 'sine', ratios: [1, 2 ** (3 / 12), 2 ** (7 / 12)] }, + ac_sus2: { wave: 'sine', ratios: [1, 2 ** (2 / 12), 2 ** (7 / 12)] }, + ac_glass: { rich: 'glass' }, + ac_swarm: { rich: 'swarm' }, + ac_bloom: { rich: 'bloom' }, + ac_orbit: { rich: 'orbit' }, + ac_marimba: { modal: true }, + ac_stone: { modal: 'stone' }, + ac_water: { chirp: 'water' }, + ac_kick: { chirp: 'kick' }, +}); + +const bounded = (value, fallback, min, max) => { + const n = Number(value ?? fallback); + return Number.isFinite(n) ? Math.max(min, Math.min(max, n)) : fallback; +}; + +function marimbaVoice(ctx, time, value, frequency, onended, stone = false) { + // Modal reduction of pop/marimba/synths/marimba.mjs, rosewood preset. + // Preserves its 1:4:9.2 ratios and T60 decay relationships; omits the + // source renderer's mallet convolution and resonator tube model. + const color = bounded(value.n, 0.5, 0, 1); + const duration = bounded(value.duration, 0.5, 0.001, 3600); + const attack = Math.min(bounded(value.attack, 0.001, 0, 60), duration); + const decay = Math.min(bounded(value.decay, 0, 0, 60), duration - attack); + const sustain = bounded(value.sustain, 1, 0, 1); + const release = bounded(value.release, 0.25, 0, 60); + const end = time + duration + release + 0.005; + const node = ctx.createGain(); + node.gain.setValueAtTime(attack ? 0 : 1, time); + if (attack) node.gain.linearRampToValueAtTime(1, time + attack); + node.gain.linearRampToValueAtTime(sustain, time + attack + decay); + node.gain.setValueAtTime(sustain, time + duration); + node.gain.linearRampToValueAtTime(0, end - 0.005); + // Stone is a designed inharmonic bar surrogate, not the offline FEM model. + const ratios = stone ? [1, 2.756, 5.404] : [1, 4, 9.2]; + const t60 = stone ? [2.8, 1.1, 0.38] : [1.6, 0.32, 0.09]; + const levels = [1, 0.32 * (0.3 + color * 1.4), 0.1 * (0.3 + color * 1.4)]; + const scale = 0.48 / levels.reduce((a, b) => a + b, 0); + const oscillators = [], gains = []; + let remaining = 3, finished = false; + ratios.forEach((ratio, i) => { + const o = ctx.createOscillator(), g = ctx.createGain(); + o.frequency.value = Math.min(frequency * ratio, ctx.sampleRate * 0.45); + const amplitude = levels[i] * scale; + const ring = t60[i] * (0.8 + color * 0.8); + g.gain.setValueAtTime(amplitude, time); + g.gain.exponentialRampToValueAtTime(amplitude * 0.001, time + ring); + g.gain.exponentialRampToValueAtTime(0.0000001, time + ring * 2); + o.connect(g).connect(node); + o.onended = () => { + if (--remaining === 0 && !finished) { + finished = true; + [...oscillators, ...gains, node].forEach(n => n.disconnect()); + onended?.(); + } + }; + oscillators.push(o); gains.push(g); + o.start(time); o.stop(end); + }); + return { node, stop(at) { + if (finished) return; + const stopAt = Math.max(ctx.currentTime, Math.min(at, end)); + node.gain.cancelAndHoldAtTime(stopAt); + node.gain.linearRampToValueAtTime(0, stopAt + 0.005); + oscillators.forEach(o => o.stop(Math.min(end, stopAt + 0.005))); + } }; +} + +function chirpVoice(ctx, time, value, frequency, kind, onended) { + const color = bounded(value.n, 0.5, 0, 1); + const kick = kind === 'kick'; + const duration = bounded(value.duration, 0.2, 0.001, 3600); + const release = bounded(value.release, 0.08, 0, 60); + const end = time + duration + release + 0.005; + const node = ctx.createGain(), oscillator = ctx.createOscillator(); + const attack = Math.min(bounded(value.attack, kick ? 0.007 : 0.003, 0, 60), duration); + const ring = kick ? 0.12 + color * 0.2 : 0.055 + color * 0.1; + node.gain.setValueAtTime(attack ? 0 : 0.5, time); + if (attack) node.gain.linearRampToValueAtTime(0.5, time + attack); + node.gain.exponentialRampToValueAtTime(0.00001, Math.min(end - 0.005, time + attack + ring)); + node.gain.setValueAtTime(0, end); + oscillator.frequency.setValueAtTime(kick ? 126 + color * 20 : frequency * 0.55, time); + oscillator.frequency.exponentialRampToValueAtTime(kick ? 43 : frequency, + time + Math.min(duration, kick ? 0.055 + color * 0.04 : 0.07)); + oscillator.connect(node); + let finished = false; + oscillator.onended = () => { + if (finished) return; + finished = true; oscillator.disconnect(); node.disconnect(); onended?.(); + }; + oscillator.start(time); oscillator.stop(end); + return { node, stop(at) { + if (finished) return; + const stopAt = Math.max(ctx.currentTime, Math.min(at, end)); + node.gain.cancelAndHoldAtTime(stopAt); + node.gain.linearRampToValueAtTime(0, stopAt + 0.005); + oscillator.stop(Math.min(end, stopAt + 0.005)); + } }; +} + +function richVoice(ctx, time, value, frequency, kind, onended, cps = 0.5) { + // n is a 0..1 timbre macro: FM depth, detune, and filter motion. + const color = bounded(value.n, 0.5, 0, 1); + const duration = bounded(value.duration, 0.5, 0.001, 3600); + const attack = Math.min(bounded(value.attack, kind === 'bloom' ? 0.18 : 0.006, 0, 60), duration); + const decay = Math.min(bounded(value.decay, kind === 'glass' ? 0.3 : 0.1, 0, 60), duration - attack); + const sustain = bounded(value.sustain, kind === 'glass' ? 0.24 : 0.8, 0, 1); + const release = bounded(value.release, kind === 'glass' ? 0.35 : 0.6, 0, 60); + const holdEnd = time + duration; + const end = holdEnd + release + 0.005; + const node = ctx.createGain(); + const allNodes = [node], oscillators = []; + const keep = object => { allNodes.push(object); return object; }; + const gain = level => { + const g = keep(ctx.createGain()); g.gain.value = level; return g; + }; + const osc = (type, hz) => { + const o = keep(ctx.createOscillator()); + o.type = type; + o.frequency.value = Math.max(0.05, Math.min(hz, ctx.sampleRate * 0.4)); + oscillators.push(o); return o; + }; + const filter = keep(ctx.createBiquadFilter()); + filter.type = 'lowpass'; + filter.Q.value = 0.5; + const cutoff = Math.min(ctx.sampleRate * 0.4, frequency * (5 + color * 15)); + filter.frequency.setValueAtTime(cutoff, time); + filter.frequency.exponentialRampToValueAtTime( + Math.max(60, cutoff * (kind === 'glass' ? 0.24 : 0.55)), holdEnd, + ); + filter.connect(node); + node.gain.setValueAtTime(attack ? 0 : 1, time); + if (attack) node.gain.linearRampToValueAtTime(1, time + attack); + node.gain.linearRampToValueAtTime(sustain, time + attack + decay); + node.gain.setValueAtTime(sustain, holdEnd); + node.gain.linearRampToValueAtTime(0, holdEnd + release); + + const layers = kind === 'glass' + ? [[1, 0.24, 'sine', 0], [1.5, 0.10, 'triangle', 0], [2.756, 0.07, 'sine', 0]] + : kind === 'swarm' + ? [[1, 0.12, 'sawtooth', -1], [1, 0.12, 'sawtooth', 1], [1.5, 0.09, 'triangle', 0.4], [0.5, 0.10, 'sine', 0]] + : [[1, 0.15, 'triangle', -1], [1, 0.12, 'triangle', 1], [1.5, 0.09, 'sine', -0.5], [2, 0.06, 'sine', 0.5]]; + layers.forEach(([ratio, level, wave, spread], i) => { + const carrier = osc(wave, frequency * ratio); + carrier.detune.value = spread * (4 + color * 18); + const pan = keep(ctx.createStereoPanner()); + pan.pan.value = spread * 0.45; + const pulse = gain(kind === 'orbit' ? 0.72 : 1); + carrier.connect(gain(level)).connect(pulse).connect(pan).connect(filter); + if (kind === 'orbit') { + // Amplitude pulses locked to the scheduler's cycle rate. + osc('sine', bounded(cps, 0.5, 0.01, 20) * (i + 2)) + .connect(gain(0.28)).connect(pulse.gain); + } + if (kind !== 'swarm') { + const mod = osc('sine', frequency * ratio * (kind === 'glass' ? 2.756 : 1.5)); + const depth = gain(frequency * ratio * (0.08 + color * (kind === 'glass' ? 2.4 : 0.6))); + mod.connect(depth).connect(carrier.frequency); + depth.gain.setValueAtTime(depth.gain.value, time); + depth.gain.exponentialRampToValueAtTime( + Math.max(0.001, depth.gain.value * 0.08), time + Math.min(duration, 0.3 + color), + ); + if (kind === 'orbit') { + // Carrier -> delay -> modulator pitch -> carrier pitch. The delay + // makes the feedback cycle valid, while sine outputs bound its level. + const feedback = keep(ctx.createDelay(0.05)); + feedback.delayTime.value = 0.004 + i * 0.002; + carrier.connect(feedback).connect(gain(frequency * (0.15 + color * 1.4))) + .connect(mod.frequency); + const shape = osc('sine', 0.13 + i * 0.07); + shape.connect(gain(frequency * ratio * color * 0.24)).connect(depth.gain); + } + } + const drift = osc('sine', 0.19 + i * 0.11 + color * 0.35); + drift.connect(gain(2 + color * 6)).connect(carrier.detune); + if (kind === 'bloom' || kind === 'orbit') drift.connect(gain(0.3)).connect(pan.pan); + }); + let remaining = oscillators.length, finished = false; + for (const oscillator of oscillators) { + oscillator.onended = () => { + if (--remaining === 0 && !finished) { + finished = true; + allNodes.forEach(n => n.disconnect()); + onended?.(); + } + }; + oscillator.start(time); + oscillator.stop(end); + } + return { + node, + stop(at) { + if (finished) return; + const stopAt = Math.max(ctx.currentTime, Math.min(at, end)); + node.gain.cancelAndHoldAtTime(stopAt); + node.gain.linearRampToValueAtTime(0, stopAt + 0.005); + for (const o of oscillators) o.stop(Math.min(end, stopAt + 0.005)); + }, + }; +} + +function registerNotepat(api = globalThis) { + const { registerSound, getAudioContext, getFrequencyFromValue } = api; + if (![registerSound, getAudioContext, getFrequencyFromValue].every(fn => typeof fn === 'function')) { + throw new Error('Import Notepat inside Strudel, or pass its audio API to registerNotepat().'); + } + for (const [name, recipe] of Object.entries(voices)) { + registerSound(name, (time, value, onended, cps) => { + const ctx = getAudioContext(); + const frequency = bounded(getFrequencyFromValue(value), 220, 1, ctx.sampleRate / 2 - 1); + if (recipe.modal) return marimbaVoice(ctx, time, value, frequency, onended, recipe.modal === 'stone'); + if (recipe.chirp) return chirpVoice(ctx, time, value, frequency, recipe.chirp, onended); + if (recipe.rich) return richVoice(ctx, time, value, frequency, recipe.rich, onended, cps); + const duration = bounded(value.duration, 0.5, 0.001, 3600); + const attack = Math.min(bounded(value.attack, 0.02, 0, 60), duration); + const decay = Math.min(bounded(value.decay, 0, 0, 60), duration - attack); + const sustain = bounded(value.sustain, 1, 0, 1); + const release = bounded(value.release, 0.1, 0, 60); + const holdEnd = time + duration; + const end = holdEnd + release + 0.005; + const node = ctx.createGain(); + node.gain.setValueAtTime(attack ? 0 : 1, time); + if (attack) node.gain.linearRampToValueAtTime(1, time + attack); + node.gain.linearRampToValueAtTime(sustain, time + attack + decay); + node.gain.setValueAtTime(sustain, holdEnd); + node.gain.linearRampToValueAtTime(0, holdEnd + release); + + const oscillators = []; + let remaining = recipe.ratios.length; + let finished = false; + for (let i = 0; i < recipe.ratios.length; i++) { + const oscillator = ctx.createOscillator(); + oscillator.type = recipe.wave; + oscillator.frequency.value = Math.min(frequency * recipe.ratios[i], ctx.sampleRate / 2 - 1); + const gain = ctx.createGain(); + // Preserve the native triangle layer balance. Chords share headroom. + gain.gain.value = (recipe.levels?.[i] ?? 0.5 / recipe.ratios.length) + * (recipe.wave === 'triangle' ? -1 : 1); + oscillator.connect(gain).connect(node); + oscillator.onended = () => { + oscillator.disconnect(); + gain.disconnect(); + if (--remaining === 0 && !finished) { + finished = true; + node.disconnect(); + onended?.(); + } + }; + oscillator.start(time); + oscillator.stop(end); + oscillators.push(oscillator); + } + return { + node, + // Strudel calls this when retiring a voice, including polyphony steals. + stop(at) { + if (finished) return; + const stopAt = Math.max(ctx.currentTime, Math.min(at, end)); + node.gain.cancelAndHoldAtTime(stopAt); + node.gain.linearRampToValueAtTime(0, stopAt + 0.005); + for (const oscillator of oscillators) oscillator.stop(Math.min(end, stopAt + 0.005)); + }, + }; + }, { type: 'synth' }); + } + return Object.keys(voices); +} + +registerNotepat(); + +})() + +// PAT / for Will — https://pat.aesthetic.computer + +// VOICES — change .s(...) in any block below. +// ac_orbit: feedback FM, interlocking pulses, moving stereo +// ac_glass: metallic FM, triangle fifth, inharmonic overtones +// ac_swarm: detuned saws, triangle fifth, sine sub +// ac_bloom: softer FM, triangle/sine layers, stereo drift +// ac_marimba: rosewood modal mallet; .n() changes brightness and ring +// ac_stone: inharmonic bar; ac_water: rising drip; ac_kick: falling sine drum +// Stone Wattajetta arrangement: https://pat.aesthetic.computer/stone +// ac_triangle_fifth / ac_triangle / ac_sine: original simple voices +// ac_major / ac_minor / ac_sus2: one root triggers a three-note sine chord +// These are custom recipes using established synthesis techniques. +// The original six derive from AC OS Notepat; the four rich voices extend them. +// Track arrangements: https://pat.aesthetic.computer/marimbaba +// Faster remix: https://pat.aesthetic.computer/marimbaba-orbit + +// TIME — new notes/key every 10 minutes when you open or re-evaluate. +// Keep a version by replacing Date.now() with a fixed timestamp. +// While playing, signals keep changing timbre, space, and layer levels. +// Generated strings need mini(...); quoted pattern literals use double quotes. +const epoch = Math.floor(Date.now() / 600000) +let seed = epoch >>> 0 +const dice = () => ((seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0) / 4294967296) +const root = [45, 48, 50, 53][epoch % 4] +const scale = [0, 2, 3, 5, 7, 9, 10] +const phrase = (count, opening) => Array.from({length: count}, (_, i) => + i === 0 ? String(root + 24 + opening) : + dice() < .16 ? '~' : String(root + 24 + scale[Math.floor(dice() * scale.length)]) +).join(' ') +const melody = phrase(13, 0) +const sparks = phrase(7, 7) +const chord = (offset, intervals) => '[' + intervals.map(n => root + offset + n).join(',') + ']' +const harmony = '<' + [chord(0, [0,3,7,10]), chord(-5, [0,3,7]), chord(-4, [0,4,7]), chord(-2, [0,4,7])].join(' ') + '>' +setcpm(24) + +// ORBIT — .n(0..1) is the timbre macro: feedback/FM depth and brightness. +// sine/perlin are Strudel signals; different periods avoid a short shared loop. +// Each $: block is independent. Select a whole block and toggle // to mute it. +// All four enter on beat one: tonic lead, fifth accent, chord, and bass. +$: note(mini(melody)).s('ac_orbit').slow(2) + .n(perlin.slow(11).range(.12,.95)) + .attack(.008).decay(.2).sustain(.55).release(.6) + .gain(sine.slow(17).range(.32,.5)) + .pan(sine.slow(19)).lpf(sine.slow(23).range(600,6000)) + .delay(.3).delaytime(.375).delayfeedback(.35).room(.35) + ._pianoroll() + +// GLASS — seven-note phrase against orbit's thirteen; notes/rests vary per epoch. +$: note(mini(sparks)).s('ac_glass').slow(3) + .n(sine.slow(13).range(.1,.9)) + .attack(.004).gain(sine.slow(29).range(.16,.3)).release(.8) + .pan(sine.slow(31)).delay(.25).delaytime(.5).room(.45) + +// BLOOM — extended chords; attack/decay/sustain/release are seconds, except +// sustain (0..1). .slow(4) stretches the harmonic progression. +$: note(mini(harmony)).s('ac_bloom').slow(4) + .n(perlin.slow(37).range(.15,.8)) + .attack(.06).decay(.4).sustain(.65).release(1.8) + .gain(.2).lpf(sine.slow(41).range(650,3200)).room(.5) + +// SWARM — bass with changing tone and level. Filters/delay/reverb/pan are +// Strudel effects; FM, detune, and internal movement belong to these voices. +$: note(mini('<' + [root-12, root-17, root-16, root-14].join(' ') + '> ~ ~ [~ ' + root + ']')) + .s('ac_swarm').slow(2).n(sine.slow(7).range(.15,.8)) + .attack(.006).release(.25).gain(.4).lpf(850) + +// SOLO LAB — comment out the four active blocks above and uncomment this. +// note(...) accepts note names or MIDI numbers; freq(...) accepts hertz. +// All voices: .attack .decay .sustain .release .gain .pan .lpf .hpf .delay .room +// Rich voices: .n(0..1). Synth-specific Strudel controls such as .fm() are +// not wired into these custom voices; use .n() for their internal modulation. +// $: note("c3 eb3 g3 bb3").s('ac_orbit').n("<0 .25 .5 .75 1>") +// .attack(.02).release(.7).gain(.4).room(.3) +// $: freq("110 165 220").s('ac_triangle_fifth').gain(.4) diff --git a/ac-strudel/notepat.mjs b/ac-strudel/notepat.mjs new file mode 100644 index 0000000000..2a478af01f --- /dev/null +++ b/ac-strudel/notepat.mjs @@ -0,0 +1,281 @@ +// AC OS Notepat voices for Strudel. https://aesthetic.computer +// Recipes: fedac/native/pieces/notepat.mjs (triangle Shift alternate and chords). +// Uses Web Audio's band-limited oscillators; this is not a recording of the +// native mixer, its echo, or its output saturation. +// Glass, swarm, bloom, and orbit extend those recipes as new Strudel sounds. + +export const voices = Object.freeze({ + ac_triangle: { wave: 'triangle', ratios: [1], levels: [0.5] }, + ac_triangle_fifth: { wave: 'triangle', ratios: [1, 1.5], levels: [0.42, 0.12] }, + ac_sine: { wave: 'sine', ratios: [1], levels: [0.5] }, + ac_major: { wave: 'sine', ratios: [1, 2 ** (4 / 12), 2 ** (7 / 12)] }, + ac_minor: { wave: 'sine', ratios: [1, 2 ** (3 / 12), 2 ** (7 / 12)] }, + ac_sus2: { wave: 'sine', ratios: [1, 2 ** (2 / 12), 2 ** (7 / 12)] }, + ac_glass: { rich: 'glass' }, + ac_swarm: { rich: 'swarm' }, + ac_bloom: { rich: 'bloom' }, + ac_orbit: { rich: 'orbit' }, + ac_marimba: { modal: true }, + ac_stone: { modal: 'stone' }, + ac_water: { chirp: 'water' }, + ac_kick: { chirp: 'kick' }, +}); + +const bounded = (value, fallback, min, max) => { + const n = Number(value ?? fallback); + return Number.isFinite(n) ? Math.max(min, Math.min(max, n)) : fallback; +}; + +function marimbaVoice(ctx, time, value, frequency, onended, stone = false) { + // Modal reduction of pop/marimba/synths/marimba.mjs, rosewood preset. + // Preserves its 1:4:9.2 ratios and T60 decay relationships; omits the + // source renderer's mallet convolution and resonator tube model. + const color = bounded(value.n, 0.5, 0, 1); + const duration = bounded(value.duration, 0.5, 0.001, 3600); + const attack = Math.min(bounded(value.attack, 0.001, 0, 60), duration); + const decay = Math.min(bounded(value.decay, 0, 0, 60), duration - attack); + const sustain = bounded(value.sustain, 1, 0, 1); + const release = bounded(value.release, 0.25, 0, 60); + const end = time + duration + release + 0.005; + const node = ctx.createGain(); + node.gain.setValueAtTime(attack ? 0 : 1, time); + if (attack) node.gain.linearRampToValueAtTime(1, time + attack); + node.gain.linearRampToValueAtTime(sustain, time + attack + decay); + node.gain.setValueAtTime(sustain, time + duration); + node.gain.linearRampToValueAtTime(0, end - 0.005); + // Stone is a designed inharmonic bar surrogate, not the offline FEM model. + const ratios = stone ? [1, 2.756, 5.404] : [1, 4, 9.2]; + const t60 = stone ? [2.8, 1.1, 0.38] : [1.6, 0.32, 0.09]; + const levels = [1, 0.32 * (0.3 + color * 1.4), 0.1 * (0.3 + color * 1.4)]; + const scale = 0.48 / levels.reduce((a, b) => a + b, 0); + const oscillators = [], gains = []; + let remaining = 3, finished = false; + ratios.forEach((ratio, i) => { + const o = ctx.createOscillator(), g = ctx.createGain(); + o.frequency.value = Math.min(frequency * ratio, ctx.sampleRate * 0.45); + const amplitude = levels[i] * scale; + const ring = t60[i] * (0.8 + color * 0.8); + g.gain.setValueAtTime(amplitude, time); + g.gain.exponentialRampToValueAtTime(amplitude * 0.001, time + ring); + g.gain.exponentialRampToValueAtTime(0.0000001, time + ring * 2); + o.connect(g).connect(node); + o.onended = () => { + if (--remaining === 0 && !finished) { + finished = true; + [...oscillators, ...gains, node].forEach(n => n.disconnect()); + onended?.(); + } + }; + oscillators.push(o); gains.push(g); + o.start(time); o.stop(end); + }); + return { node, stop(at) { + if (finished) return; + const stopAt = Math.max(ctx.currentTime, Math.min(at, end)); + node.gain.cancelAndHoldAtTime(stopAt); + node.gain.linearRampToValueAtTime(0, stopAt + 0.005); + oscillators.forEach(o => o.stop(Math.min(end, stopAt + 0.005))); + } }; +} + +function chirpVoice(ctx, time, value, frequency, kind, onended) { + const color = bounded(value.n, 0.5, 0, 1); + const kick = kind === 'kick'; + const duration = bounded(value.duration, 0.2, 0.001, 3600); + const release = bounded(value.release, 0.08, 0, 60); + const end = time + duration + release + 0.005; + const node = ctx.createGain(), oscillator = ctx.createOscillator(); + const attack = Math.min(bounded(value.attack, kick ? 0.007 : 0.003, 0, 60), duration); + const ring = kick ? 0.12 + color * 0.2 : 0.055 + color * 0.1; + node.gain.setValueAtTime(attack ? 0 : 0.5, time); + if (attack) node.gain.linearRampToValueAtTime(0.5, time + attack); + node.gain.exponentialRampToValueAtTime(0.00001, Math.min(end - 0.005, time + attack + ring)); + node.gain.setValueAtTime(0, end); + oscillator.frequency.setValueAtTime(kick ? 126 + color * 20 : frequency * 0.55, time); + oscillator.frequency.exponentialRampToValueAtTime(kick ? 43 : frequency, + time + Math.min(duration, kick ? 0.055 + color * 0.04 : 0.07)); + oscillator.connect(node); + let finished = false; + oscillator.onended = () => { + if (finished) return; + finished = true; oscillator.disconnect(); node.disconnect(); onended?.(); + }; + oscillator.start(time); oscillator.stop(end); + return { node, stop(at) { + if (finished) return; + const stopAt = Math.max(ctx.currentTime, Math.min(at, end)); + node.gain.cancelAndHoldAtTime(stopAt); + node.gain.linearRampToValueAtTime(0, stopAt + 0.005); + oscillator.stop(Math.min(end, stopAt + 0.005)); + } }; +} + +function richVoice(ctx, time, value, frequency, kind, onended, cps = 0.5) { + // n is a 0..1 timbre macro: FM depth, detune, and filter motion. + const color = bounded(value.n, 0.5, 0, 1); + const duration = bounded(value.duration, 0.5, 0.001, 3600); + const attack = Math.min(bounded(value.attack, kind === 'bloom' ? 0.18 : 0.006, 0, 60), duration); + const decay = Math.min(bounded(value.decay, kind === 'glass' ? 0.3 : 0.1, 0, 60), duration - attack); + const sustain = bounded(value.sustain, kind === 'glass' ? 0.24 : 0.8, 0, 1); + const release = bounded(value.release, kind === 'glass' ? 0.35 : 0.6, 0, 60); + const holdEnd = time + duration; + const end = holdEnd + release + 0.005; + const node = ctx.createGain(); + const allNodes = [node], oscillators = []; + const keep = object => { allNodes.push(object); return object; }; + const gain = level => { + const g = keep(ctx.createGain()); g.gain.value = level; return g; + }; + const osc = (type, hz) => { + const o = keep(ctx.createOscillator()); + o.type = type; + o.frequency.value = Math.max(0.05, Math.min(hz, ctx.sampleRate * 0.4)); + oscillators.push(o); return o; + }; + const filter = keep(ctx.createBiquadFilter()); + filter.type = 'lowpass'; + filter.Q.value = 0.5; + const cutoff = Math.min(ctx.sampleRate * 0.4, frequency * (5 + color * 15)); + filter.frequency.setValueAtTime(cutoff, time); + filter.frequency.exponentialRampToValueAtTime( + Math.max(60, cutoff * (kind === 'glass' ? 0.24 : 0.55)), holdEnd, + ); + filter.connect(node); + node.gain.setValueAtTime(attack ? 0 : 1, time); + if (attack) node.gain.linearRampToValueAtTime(1, time + attack); + node.gain.linearRampToValueAtTime(sustain, time + attack + decay); + node.gain.setValueAtTime(sustain, holdEnd); + node.gain.linearRampToValueAtTime(0, holdEnd + release); + + const layers = kind === 'glass' + ? [[1, 0.24, 'sine', 0], [1.5, 0.10, 'triangle', 0], [2.756, 0.07, 'sine', 0]] + : kind === 'swarm' + ? [[1, 0.12, 'sawtooth', -1], [1, 0.12, 'sawtooth', 1], [1.5, 0.09, 'triangle', 0.4], [0.5, 0.10, 'sine', 0]] + : [[1, 0.15, 'triangle', -1], [1, 0.12, 'triangle', 1], [1.5, 0.09, 'sine', -0.5], [2, 0.06, 'sine', 0.5]]; + layers.forEach(([ratio, level, wave, spread], i) => { + const carrier = osc(wave, frequency * ratio); + carrier.detune.value = spread * (4 + color * 18); + const pan = keep(ctx.createStereoPanner()); + pan.pan.value = spread * 0.45; + const pulse = gain(kind === 'orbit' ? 0.72 : 1); + carrier.connect(gain(level)).connect(pulse).connect(pan).connect(filter); + if (kind === 'orbit') { + // Amplitude pulses locked to the scheduler's cycle rate. + osc('sine', bounded(cps, 0.5, 0.01, 20) * (i + 2)) + .connect(gain(0.28)).connect(pulse.gain); + } + if (kind !== 'swarm') { + const mod = osc('sine', frequency * ratio * (kind === 'glass' ? 2.756 : 1.5)); + const depth = gain(frequency * ratio * (0.08 + color * (kind === 'glass' ? 2.4 : 0.6))); + mod.connect(depth).connect(carrier.frequency); + depth.gain.setValueAtTime(depth.gain.value, time); + depth.gain.exponentialRampToValueAtTime( + Math.max(0.001, depth.gain.value * 0.08), time + Math.min(duration, 0.3 + color), + ); + if (kind === 'orbit') { + // Carrier -> delay -> modulator pitch -> carrier pitch. The delay + // makes the feedback cycle valid, while sine outputs bound its level. + const feedback = keep(ctx.createDelay(0.05)); + feedback.delayTime.value = 0.004 + i * 0.002; + carrier.connect(feedback).connect(gain(frequency * (0.15 + color * 1.4))) + .connect(mod.frequency); + const shape = osc('sine', 0.13 + i * 0.07); + shape.connect(gain(frequency * ratio * color * 0.24)).connect(depth.gain); + } + } + const drift = osc('sine', 0.19 + i * 0.11 + color * 0.35); + drift.connect(gain(2 + color * 6)).connect(carrier.detune); + if (kind === 'bloom' || kind === 'orbit') drift.connect(gain(0.3)).connect(pan.pan); + }); + let remaining = oscillators.length, finished = false; + for (const oscillator of oscillators) { + oscillator.onended = () => { + if (--remaining === 0 && !finished) { + finished = true; + allNodes.forEach(n => n.disconnect()); + onended?.(); + } + }; + oscillator.start(time); + oscillator.stop(end); + } + return { + node, + stop(at) { + if (finished) return; + const stopAt = Math.max(ctx.currentTime, Math.min(at, end)); + node.gain.cancelAndHoldAtTime(stopAt); + node.gain.linearRampToValueAtTime(0, stopAt + 0.005); + for (const o of oscillators) o.stop(Math.min(end, stopAt + 0.005)); + }, + }; +} + +export function registerNotepat(api = globalThis) { + const { registerSound, getAudioContext, getFrequencyFromValue } = api; + if (![registerSound, getAudioContext, getFrequencyFromValue].every(fn => typeof fn === 'function')) { + throw new Error('Import Notepat inside Strudel, or pass its audio API to registerNotepat().'); + } + for (const [name, recipe] of Object.entries(voices)) { + registerSound(name, (time, value, onended, cps) => { + const ctx = getAudioContext(); + const frequency = bounded(getFrequencyFromValue(value), 220, 1, ctx.sampleRate / 2 - 1); + if (recipe.modal) return marimbaVoice(ctx, time, value, frequency, onended, recipe.modal === 'stone'); + if (recipe.chirp) return chirpVoice(ctx, time, value, frequency, recipe.chirp, onended); + if (recipe.rich) return richVoice(ctx, time, value, frequency, recipe.rich, onended, cps); + const duration = bounded(value.duration, 0.5, 0.001, 3600); + const attack = Math.min(bounded(value.attack, 0.02, 0, 60), duration); + const decay = Math.min(bounded(value.decay, 0, 0, 60), duration - attack); + const sustain = bounded(value.sustain, 1, 0, 1); + const release = bounded(value.release, 0.1, 0, 60); + const holdEnd = time + duration; + const end = holdEnd + release + 0.005; + const node = ctx.createGain(); + node.gain.setValueAtTime(attack ? 0 : 1, time); + if (attack) node.gain.linearRampToValueAtTime(1, time + attack); + node.gain.linearRampToValueAtTime(sustain, time + attack + decay); + node.gain.setValueAtTime(sustain, holdEnd); + node.gain.linearRampToValueAtTime(0, holdEnd + release); + + const oscillators = []; + let remaining = recipe.ratios.length; + let finished = false; + for (let i = 0; i < recipe.ratios.length; i++) { + const oscillator = ctx.createOscillator(); + oscillator.type = recipe.wave; + oscillator.frequency.value = Math.min(frequency * recipe.ratios[i], ctx.sampleRate / 2 - 1); + const gain = ctx.createGain(); + // Preserve the native triangle layer balance. Chords share headroom. + gain.gain.value = (recipe.levels?.[i] ?? 0.5 / recipe.ratios.length) + * (recipe.wave === 'triangle' ? -1 : 1); + oscillator.connect(gain).connect(node); + oscillator.onended = () => { + oscillator.disconnect(); + gain.disconnect(); + if (--remaining === 0 && !finished) { + finished = true; + node.disconnect(); + onended?.(); + } + }; + oscillator.start(time); + oscillator.stop(end); + oscillators.push(oscillator); + } + return { + node, + // Strudel calls this when retiring a voice, including polyphony steals. + stop(at) { + if (finished) return; + const stopAt = Math.max(ctx.currentTime, Math.min(at, end)); + node.gain.cancelAndHoldAtTime(stopAt); + node.gain.linearRampToValueAtTime(0, stopAt + 0.005); + for (const oscillator of oscillators) oscillator.stop(Math.min(end, stopAt + 0.005)); + }, + }; + }, { type: 'synth' }); + } + return Object.keys(voices); +} + +registerNotepat(); diff --git a/ac-strudel/standalone.url b/ac-strudel/standalone.url new file mode 100644 index 0000000000..815464c3b5 --- /dev/null +++ b/ac-strudel/standalone.url @@ -0,0 +1 @@ +https://strudel.cc/#KCgpID0+IHsKLy8gQUMgT1MgTm90ZXBhdCB2b2ljZXMgZm9yIFN0cnVkZWwuIGh0dHBzOi8vYWVzdGhldGljLmNvbXB1dGVyCi8vIFJlY2lwZXM6IGZlZGFjL25hdGl2ZS9waWVjZXMvbm90ZXBhdC5tanMgKHRyaWFuZ2xlIFNoaWZ0IGFsdGVybmF0ZSBhbmQgY2hvcmRzKS4KLy8gVXNlcyBXZWIgQXVkaW8ncyBiYW5kLWxpbWl0ZWQgb3NjaWxsYXRvcnM7IHRoaXMgaXMgbm90IGEgcmVjb3JkaW5nIG9mIHRoZQovLyBuYXRpdmUgbWl4ZXIsIGl0cyBlY2hvLCBvciBpdHMgb3V0cHV0IHNhdHVyYXRpb24uCi8vIEdsYXNzLCBzd2FybSwgYmxvb20sIGFuZCBvcmJpdCBleHRlbmQgdGhvc2UgcmVjaXBlcyBhcyBuZXcgU3RydWRlbCBzb3VuZHMuCgpjb25zdCB2b2ljZXMgPSBPYmplY3QuZnJlZXplKHsKICBhY190cmlhbmdsZTogeyB3YXZlOiAndHJpYW5nbGUnLCByYXRpb3M6IFsxXSwgbGV2ZWxzOiBbMC41XSB9LAogIGFjX3RyaWFuZ2xlX2ZpZnRoOiB7IHdhdmU6ICd0cmlhbmdsZScsIHJhdGlvczogWzEsIDEuNV0sIGxldmVsczogWzAuNDIsIDAuMTJdIH0sCiAgYWNfc2luZTogeyB3YXZlOiAnc2luZScsIHJhdGlvczogWzFdLCBsZXZlbHM6IFswLjVdIH0sCiAgYWNfbWFqb3I6IHsgd2F2ZTogJ3NpbmUnLCByYXRpb3M6IFsxLCAyICoqICg0IC8gMTIpLCAyICoqICg3IC8gMTIpXSB9LAogIGFjX21pbm9yOiB7IHdhdmU6ICdzaW5lJywgcmF0aW9zOiBbMSwgMiAqKiAoMyAvIDEyKSwgMiAqKiAoNyAvIDEyKV0gfSwKICBhY19zdXMyOiB7IHdhdmU6ICdzaW5lJywgcmF0aW9zOiBbMSwgMiAqKiAoMiAvIDEyKSwgMiAqKiAoNyAvIDEyKV0gfSwKICBhY19nbGFzczogeyByaWNoOiAnZ2xhc3MnIH0sCiAgYWNfc3dhcm06IHsgcmljaDogJ3N3YXJtJyB9LAogIGFjX2Jsb29tOiB7IHJpY2g6ICdibG9vbScgfSwKICBhY19vcmJpdDogeyByaWNoOiAnb3JiaXQnIH0sCiAgYWNfbWFyaW1iYTogeyBtb2RhbDogdHJ1ZSB9LAogIGFjX3N0b25lOiB7IG1vZGFsOiAnc3RvbmUnIH0sCiAgYWNfd2F0ZXI6IHsgY2hpcnA6ICd3YXRlcicgfSwKICBhY19raWNrOiB7IGNoaXJwOiAna2ljaycgfSwKfSk7Cgpjb25zdCBib3VuZGVkID0gKHZhbHVlLCBmYWxsYmFjaywgbWluLCBtYXgpID0+IHsKICBjb25zdCBuID0gTnVtYmVyKHZhbHVlID8/IGZhbGxiYWNrKTsKICByZXR1cm4gTnVtYmVyLmlzRmluaXRlKG4pID8gTWF0aC5tYXgobWluLCBNYXRoLm1pbihtYXgsIG4pKSA6IGZhbGxiYWNrOwp9OwoKZnVuY3Rpb24gbWFyaW1iYVZvaWNlKGN0eCwgdGltZSwgdmFsdWUsIGZyZXF1ZW5jeSwgb25lbmRlZCwgc3RvbmUgPSBmYWxzZSkgewogIC8vIE1vZGFsIHJlZHVjdGlvbiBvZiBwb3AvbWFyaW1iYS9zeW50aHMvbWFyaW1iYS5tanMsIHJvc2V3b29kIHByZXNldC4KICAvLyBQcmVzZXJ2ZXMgaXRzIDE6NDo5LjIgcmF0aW9zIGFuZCBUNjAgZGVjYXkgcmVsYXRpb25zaGlwczsgb21pdHMgdGhlCiAgLy8gc291cmNlIHJlbmRlcmVyJ3MgbWFsbGV0IGNvbnZvbHV0aW9uIGFuZCByZXNvbmF0b3IgdHViZSBtb2RlbC4KICBjb25zdCBjb2xvciA9IGJvdW5kZWQodmFsdWUubiwgMC41LCAwLCAxKTsKICBjb25zdCBkdXJhdGlvbiA9IGJvdW5kZWQodmFsdWUuZHVyYXRpb24sIDAuNSwgMC4wMDEsIDM2MDApOwogIGNvbnN0IGF0dGFjayA9IE1hdGgubWluKGJvdW5kZWQodmFsdWUuYXR0YWNrLCAwLjAwMSwgMCwgNjApLCBkdXJhdGlvbik7CiAgY29uc3QgZGVjYXkgPSBNYXRoLm1pbihib3VuZGVkKHZhbHVlLmRlY2F5LCAwLCAwLCA2MCksIGR1cmF0aW9uIC0gYXR0YWNrKTsKICBjb25zdCBzdXN0YWluID0gYm91bmRlZCh2YWx1ZS5zdXN0YWluLCAxLCAwLCAxKTsKICBjb25zdCByZWxlYXNlID0gYm91bmRlZCh2YWx1ZS5yZWxlYXNlLCAwLjI1LCAwLCA2MCk7CiAgY29uc3QgZW5kID0gdGltZSArIGR1cmF0aW9uICsgcmVsZWFzZSArIDAuMDA1OwogIGNvbnN0IG5vZGUgPSBjdHguY3JlYXRlR2FpbigpOwogIG5vZGUuZ2Fpbi5zZXRWYWx1ZUF0VGltZShhdHRhY2sgPyAwIDogMSwgdGltZSk7CiAgaWYgKGF0dGFjaykgbm9kZS5nYWluLmxpbmVhclJhbXBUb1ZhbHVlQXRUaW1lKDEsIHRpbWUgKyBhdHRhY2spOwogIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZShzdXN0YWluLCB0aW1lICsgYXR0YWNrICsgZGVjYXkpOwogIG5vZGUuZ2Fpbi5zZXRWYWx1ZUF0VGltZShzdXN0YWluLCB0aW1lICsgZHVyYXRpb24pOwogIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZSgwLCBlbmQgLSAwLjAwNSk7CiAgLy8gU3RvbmUgaXMgYSBkZXNpZ25lZCBpbmhhcm1vbmljIGJhciBzdXJyb2dhdGUsIG5vdCB0aGUgb2ZmbGluZSBGRU0gbW9kZWwuCiAgY29uc3QgcmF0aW9zID0gc3RvbmUgPyBbMSwgMi43NTYsIDUuNDA0XSA6IFsxLCA0LCA5LjJdOwogIGNvbnN0IHQ2MCA9IHN0b25lID8gWzIuOCwgMS4xLCAwLjM4XSA6IFsxLjYsIDAuMzIsIDAuMDldOwogIGNvbnN0IGxldmVscyA9IFsxLCAwLjMyICogKDAuMyArIGNvbG9yICogMS40KSwgMC4xICogKDAuMyArIGNvbG9yICogMS40KV07CiAgY29uc3Qgc2NhbGUgPSAwLjQ4IC8gbGV2ZWxzLnJlZHVjZSgoYSwgYikgPT4gYSArIGIsIDApOwogIGNvbnN0IG9zY2lsbGF0b3JzID0gW10sIGdhaW5zID0gW107CiAgbGV0IHJlbWFpbmluZyA9IDMsIGZpbmlzaGVkID0gZmFsc2U7CiAgcmF0aW9zLmZvckVhY2goKHJhdGlvLCBpKSA9PiB7CiAgICBjb25zdCBvID0gY3R4LmNyZWF0ZU9zY2lsbGF0b3IoKSwgZyA9IGN0eC5jcmVhdGVHYWluKCk7CiAgICBvLmZyZXF1ZW5jeS52YWx1ZSA9IE1hdGgubWluKGZyZXF1ZW5jeSAqIHJhdGlvLCBjdHguc2FtcGxlUmF0ZSAqIDAuNDUpOwogICAgY29uc3QgYW1wbGl0dWRlID0gbGV2ZWxzW2ldICogc2NhbGU7CiAgICBjb25zdCByaW5nID0gdDYwW2ldICogKDAuOCArIGNvbG9yICogMC44KTsKICAgIGcuZ2Fpbi5zZXRWYWx1ZUF0VGltZShhbXBsaXR1ZGUsIHRpbWUpOwogICAgZy5nYWluLmV4cG9uZW50aWFsUmFtcFRvVmFsdWVBdFRpbWUoYW1wbGl0dWRlICogMC4wMDEsIHRpbWUgKyByaW5nKTsKICAgIGcuZ2Fpbi5leHBvbmVudGlhbFJhbXBUb1ZhbHVlQXRUaW1lKDAuMDAwMDAwMSwgdGltZSArIHJpbmcgKiAyKTsKICAgIG8uY29ubmVjdChnKS5jb25uZWN0KG5vZGUpOwogICAgby5vbmVuZGVkID0gKCkgPT4gewogICAgICBpZiAoLS1yZW1haW5pbmcgPT09IDAgJiYgIWZpbmlzaGVkKSB7CiAgICAgICAgZmluaXNoZWQgPSB0cnVlOwogICAgICAgIFsuLi5vc2NpbGxhdG9ycywgLi4uZ2FpbnMsIG5vZGVdLmZvckVhY2gobiA9PiBuLmRpc2Nvbm5lY3QoKSk7CiAgICAgICAgb25lbmRlZD8uKCk7CiAgICAgIH0KICAgIH07CiAgICBvc2NpbGxhdG9ycy5wdXNoKG8pOyBnYWlucy5wdXNoKGcpOwogICAgby5zdGFydCh0aW1lKTsgby5zdG9wKGVuZCk7CiAgfSk7CiAgcmV0dXJuIHsgbm9kZSwgc3RvcChhdCkgewogICAgaWYgKGZpbmlzaGVkKSByZXR1cm47CiAgICBjb25zdCBzdG9wQXQgPSBNYXRoLm1heChjdHguY3VycmVudFRpbWUsIE1hdGgubWluKGF0LCBlbmQpKTsKICAgIG5vZGUuZ2Fpbi5jYW5jZWxBbmRIb2xkQXRUaW1lKHN0b3BBdCk7CiAgICBub2RlLmdhaW4ubGluZWFyUmFtcFRvVmFsdWVBdFRpbWUoMCwgc3RvcEF0ICsgMC4wMDUpOwogICAgb3NjaWxsYXRvcnMuZm9yRWFjaChvID0+IG8uc3RvcChNYXRoLm1pbihlbmQsIHN0b3BBdCArIDAuMDA1KSkpOwogIH0gfTsKfQoKZnVuY3Rpb24gY2hpcnBWb2ljZShjdHgsIHRpbWUsIHZhbHVlLCBmcmVxdWVuY3ksIGtpbmQsIG9uZW5kZWQpIHsKICBjb25zdCBjb2xvciA9IGJvdW5kZWQodmFsdWUubiwgMC41LCAwLCAxKTsKICBjb25zdCBraWNrID0ga2luZCA9PT0gJ2tpY2snOwogIGNvbnN0IGR1cmF0aW9uID0gYm91bmRlZCh2YWx1ZS5kdXJhdGlvbiwgMC4yLCAwLjAwMSwgMzYwMCk7CiAgY29uc3QgcmVsZWFzZSA9IGJvdW5kZWQodmFsdWUucmVsZWFzZSwgMC4wOCwgMCwgNjApOwogIGNvbnN0IGVuZCA9IHRpbWUgKyBkdXJhdGlvbiArIHJlbGVhc2UgKyAwLjAwNTsKICBjb25zdCBub2RlID0gY3R4LmNyZWF0ZUdhaW4oKSwgb3NjaWxsYXRvciA9IGN0eC5jcmVhdGVPc2NpbGxhdG9yKCk7CiAgY29uc3QgYXR0YWNrID0gTWF0aC5taW4oYm91bmRlZCh2YWx1ZS5hdHRhY2ssIGtpY2sgPyAwLjAwNyA6IDAuMDAzLCAwLCA2MCksIGR1cmF0aW9uKTsKICBjb25zdCByaW5nID0ga2ljayA/IDAuMTIgKyBjb2xvciAqIDAuMiA6IDAuMDU1ICsgY29sb3IgKiAwLjE7CiAgbm9kZS5nYWluLnNldFZhbHVlQXRUaW1lKGF0dGFjayA/IDAgOiAwLjUsIHRpbWUpOwogIGlmIChhdHRhY2spIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZSgwLjUsIHRpbWUgKyBhdHRhY2spOwogIG5vZGUuZ2Fpbi5leHBvbmVudGlhbFJhbXBUb1ZhbHVlQXRUaW1lKDAuMDAwMDEsIE1hdGgubWluKGVuZCAtIDAuMDA1LCB0aW1lICsgYXR0YWNrICsgcmluZykpOwogIG5vZGUuZ2Fpbi5zZXRWYWx1ZUF0VGltZSgwLCBlbmQpOwogIG9zY2lsbGF0b3IuZnJlcXVlbmN5LnNldFZhbHVlQXRUaW1lKGtpY2sgPyAxMjYgKyBjb2xvciAqIDIwIDogZnJlcXVlbmN5ICogMC41NSwgdGltZSk7CiAgb3NjaWxsYXRvci5mcmVxdWVuY3kuZXhwb25lbnRpYWxSYW1wVG9WYWx1ZUF0VGltZShraWNrID8gNDMgOiBmcmVxdWVuY3ksCiAgICB0aW1lICsgTWF0aC5taW4oZHVyYXRpb24sIGtpY2sgPyAwLjA1NSArIGNvbG9yICogMC4wNCA6IDAuMDcpKTsKICBvc2NpbGxhdG9yLmNvbm5lY3Qobm9kZSk7CiAgbGV0IGZpbmlzaGVkID0gZmFsc2U7CiAgb3NjaWxsYXRvci5vbmVuZGVkID0gKCkgPT4gewogICAgaWYgKGZpbmlzaGVkKSByZXR1cm47CiAgICBmaW5pc2hlZCA9IHRydWU7IG9zY2lsbGF0b3IuZGlzY29ubmVjdCgpOyBub2RlLmRpc2Nvbm5lY3QoKTsgb25lbmRlZD8uKCk7CiAgfTsKICBvc2NpbGxhdG9yLnN0YXJ0KHRpbWUpOyBvc2NpbGxhdG9yLnN0b3AoZW5kKTsKICByZXR1cm4geyBub2RlLCBzdG9wKGF0KSB7CiAgICBpZiAoZmluaXNoZWQpIHJldHVybjsKICAgIGNvbnN0IHN0b3BBdCA9IE1hdGgubWF4KGN0eC5jdXJyZW50VGltZSwgTWF0aC5taW4oYXQsIGVuZCkpOwogICAgbm9kZS5nYWluLmNhbmNlbEFuZEhvbGRBdFRpbWUoc3RvcEF0KTsKICAgIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZSgwLCBzdG9wQXQgKyAwLjAwNSk7CiAgICBvc2NpbGxhdG9yLnN0b3AoTWF0aC5taW4oZW5kLCBzdG9wQXQgKyAwLjAwNSkpOwogIH0gfTsKfQoKZnVuY3Rpb24gcmljaFZvaWNlKGN0eCwgdGltZSwgdmFsdWUsIGZyZXF1ZW5jeSwga2luZCwgb25lbmRlZCwgY3BzID0gMC41KSB7CiAgLy8gbiBpcyBhIDAuLjEgdGltYnJlIG1hY3JvOiBGTSBkZXB0aCwgZGV0dW5lLCBhbmQgZmlsdGVyIG1vdGlvbi4KICBjb25zdCBjb2xvciA9IGJvdW5kZWQodmFsdWUubiwgMC41LCAwLCAxKTsKICBjb25zdCBkdXJhdGlvbiA9IGJvdW5kZWQodmFsdWUuZHVyYXRpb24sIDAuNSwgMC4wMDEsIDM2MDApOwogIGNvbnN0IGF0dGFjayA9IE1hdGgubWluKGJvdW5kZWQodmFsdWUuYXR0YWNrLCBraW5kID09PSAnYmxvb20nID8gMC4xOCA6IDAuMDA2LCAwLCA2MCksIGR1cmF0aW9uKTsKICBjb25zdCBkZWNheSA9IE1hdGgubWluKGJvdW5kZWQodmFsdWUuZGVjYXksIGtpbmQgPT09ICdnbGFzcycgPyAwLjMgOiAwLjEsIDAsIDYwKSwgZHVyYXRpb24gLSBhdHRhY2spOwogIGNvbnN0IHN1c3RhaW4gPSBib3VuZGVkKHZhbHVlLnN1c3RhaW4sIGtpbmQgPT09ICdnbGFzcycgPyAwLjI0IDogMC44LCAwLCAxKTsKICBjb25zdCByZWxlYXNlID0gYm91bmRlZCh2YWx1ZS5yZWxlYXNlLCBraW5kID09PSAnZ2xhc3MnID8gMC4zNSA6IDAuNiwgMCwgNjApOwogIGNvbnN0IGhvbGRFbmQgPSB0aW1lICsgZHVyYXRpb247CiAgY29uc3QgZW5kID0gaG9sZEVuZCArIHJlbGVhc2UgKyAwLjAwNTsKICBjb25zdCBub2RlID0gY3R4LmNyZWF0ZUdhaW4oKTsKICBjb25zdCBhbGxOb2RlcyA9IFtub2RlXSwgb3NjaWxsYXRvcnMgPSBbXTsKICBjb25zdCBrZWVwID0gb2JqZWN0ID0+IHsgYWxsTm9kZXMucHVzaChvYmplY3QpOyByZXR1cm4gb2JqZWN0OyB9OwogIGNvbnN0IGdhaW4gPSBsZXZlbCA9PiB7CiAgICBjb25zdCBnID0ga2VlcChjdHguY3JlYXRlR2FpbigpKTsgZy5nYWluLnZhbHVlID0gbGV2ZWw7IHJldHVybiBnOwogIH07CiAgY29uc3Qgb3NjID0gKHR5cGUsIGh6KSA9PiB7CiAgICBjb25zdCBvID0ga2VlcChjdHguY3JlYXRlT3NjaWxsYXRvcigpKTsKICAgIG8udHlwZSA9IHR5cGU7CiAgICBvLmZyZXF1ZW5jeS52YWx1ZSA9IE1hdGgubWF4KDAuMDUsIE1hdGgubWluKGh6LCBjdHguc2FtcGxlUmF0ZSAqIDAuNCkpOwogICAgb3NjaWxsYXRvcnMucHVzaChvKTsgcmV0dXJuIG87CiAgfTsKICBjb25zdCBmaWx0ZXIgPSBrZWVwKGN0eC5jcmVhdGVCaXF1YWRGaWx0ZXIoKSk7CiAgZmlsdGVyLnR5cGUgPSAnbG93cGFzcyc7CiAgZmlsdGVyLlEudmFsdWUgPSAwLjU7CiAgY29uc3QgY3V0b2ZmID0gTWF0aC5taW4oY3R4LnNhbXBsZVJhdGUgKiAwLjQsIGZyZXF1ZW5jeSAqICg1ICsgY29sb3IgKiAxNSkpOwogIGZpbHRlci5mcmVxdWVuY3kuc2V0VmFsdWVBdFRpbWUoY3V0b2ZmLCB0aW1lKTsKICBmaWx0ZXIuZnJlcXVlbmN5LmV4cG9uZW50aWFsUmFtcFRvVmFsdWVBdFRpbWUoCiAgICBNYXRoLm1heCg2MCwgY3V0b2ZmICogKGtpbmQgPT09ICdnbGFzcycgPyAwLjI0IDogMC41NSkpLCBob2xkRW5kLAogICk7CiAgZmlsdGVyLmNvbm5lY3Qobm9kZSk7CiAgbm9kZS5nYWluLnNldFZhbHVlQXRUaW1lKGF0dGFjayA/IDAgOiAxLCB0aW1lKTsKICBpZiAoYXR0YWNrKSBub2RlLmdhaW4ubGluZWFyUmFtcFRvVmFsdWVBdFRpbWUoMSwgdGltZSArIGF0dGFjayk7CiAgbm9kZS5nYWluLmxpbmVhclJhbXBUb1ZhbHVlQXRUaW1lKHN1c3RhaW4sIHRpbWUgKyBhdHRhY2sgKyBkZWNheSk7CiAgbm9kZS5nYWluLnNldFZhbHVlQXRUaW1lKHN1c3RhaW4sIGhvbGRFbmQpOwogIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZSgwLCBob2xkRW5kICsgcmVsZWFzZSk7CgogIGNvbnN0IGxheWVycyA9IGtpbmQgPT09ICdnbGFzcycKICAgID8gW1sxLCAwLjI0LCAnc2luZScsIDBdLCBbMS41LCAwLjEwLCAndHJpYW5nbGUnLCAwXSwgWzIuNzU2LCAwLjA3LCAnc2luZScsIDBdXQogICAgOiBraW5kID09PSAnc3dhcm0nCiAgICAgID8gW1sxLCAwLjEyLCAnc2F3dG9vdGgnLCAtMV0sIFsxLCAwLjEyLCAnc2F3dG9vdGgnLCAxXSwgWzEuNSwgMC4wOSwgJ3RyaWFuZ2xlJywgMC40XSwgWzAuNSwgMC4xMCwgJ3NpbmUnLCAwXV0KICAgICAgOiBbWzEsIDAuMTUsICd0cmlhbmdsZScsIC0xXSwgWzEsIDAuMTIsICd0cmlhbmdsZScsIDFdLCBbMS41LCAwLjA5LCAnc2luZScsIC0wLjVdLCBbMiwgMC4wNiwgJ3NpbmUnLCAwLjVdXTsKICBsYXllcnMuZm9yRWFjaCgoW3JhdGlvLCBsZXZlbCwgd2F2ZSwgc3ByZWFkXSwgaSkgPT4gewogICAgY29uc3QgY2FycmllciA9IG9zYyh3YXZlLCBmcmVxdWVuY3kgKiByYXRpbyk7CiAgICBjYXJyaWVyLmRldHVuZS52YWx1ZSA9IHNwcmVhZCAqICg0ICsgY29sb3IgKiAxOCk7CiAgICBjb25zdCBwYW4gPSBrZWVwKGN0eC5jcmVhdGVTdGVyZW9QYW5uZXIoKSk7CiAgICBwYW4ucGFuLnZhbHVlID0gc3ByZWFkICogMC40NTsKICAgIGNvbnN0IHB1bHNlID0gZ2FpbihraW5kID09PSAnb3JiaXQnID8gMC43MiA6IDEpOwogICAgY2Fycmllci5jb25uZWN0KGdhaW4obGV2ZWwpKS5jb25uZWN0KHB1bHNlKS5jb25uZWN0KHBhbikuY29ubmVjdChmaWx0ZXIpOwogICAgaWYgKGtpbmQgPT09ICdvcmJpdCcpIHsKICAgICAgLy8gQW1wbGl0dWRlIHB1bHNlcyBsb2NrZWQgdG8gdGhlIHNjaGVkdWxlcidzIGN5Y2xlIHJhdGUuCiAgICAgIG9zYygnc2luZScsIGJvdW5kZWQoY3BzLCAwLjUsIDAuMDEsIDIwKSAqIChpICsgMikpCiAgICAgICAgLmNvbm5lY3QoZ2FpbigwLjI4KSkuY29ubmVjdChwdWxzZS5nYWluKTsKICAgIH0KICAgIGlmIChraW5kICE9PSAnc3dhcm0nKSB7CiAgICAgIGNvbnN0IG1vZCA9IG9zYygnc2luZScsIGZyZXF1ZW5jeSAqIHJhdGlvICogKGtpbmQgPT09ICdnbGFzcycgPyAyLjc1NiA6IDEuNSkpOwogICAgICBjb25zdCBkZXB0aCA9IGdhaW4oZnJlcXVlbmN5ICogcmF0aW8gKiAoMC4wOCArIGNvbG9yICogKGtpbmQgPT09ICdnbGFzcycgPyAyLjQgOiAwLjYpKSk7CiAgICAgIG1vZC5jb25uZWN0KGRlcHRoKS5jb25uZWN0KGNhcnJpZXIuZnJlcXVlbmN5KTsKICAgICAgZGVwdGguZ2Fpbi5zZXRWYWx1ZUF0VGltZShkZXB0aC5nYWluLnZhbHVlLCB0aW1lKTsKICAgICAgZGVwdGguZ2Fpbi5leHBvbmVudGlhbFJhbXBUb1ZhbHVlQXRUaW1lKAogICAgICAgIE1hdGgubWF4KDAuMDAxLCBkZXB0aC5nYWluLnZhbHVlICogMC4wOCksIHRpbWUgKyBNYXRoLm1pbihkdXJhdGlvbiwgMC4zICsgY29sb3IpLAogICAgICApOwogICAgICBpZiAoa2luZCA9PT0gJ29yYml0JykgewogICAgICAgIC8vIENhcnJpZXIgLT4gZGVsYXkgLT4gbW9kdWxhdG9yIHBpdGNoIC0+IGNhcnJpZXIgcGl0Y2guIFRoZSBkZWxheQogICAgICAgIC8vIG1ha2VzIHRoZSBmZWVkYmFjayBjeWNsZSB2YWxpZCwgd2hpbGUgc2luZSBvdXRwdXRzIGJvdW5kIGl0cyBsZXZlbC4KICAgICAgICBjb25zdCBmZWVkYmFjayA9IGtlZXAoY3R4LmNyZWF0ZURlbGF5KDAuMDUpKTsKICAgICAgICBmZWVkYmFjay5kZWxheVRpbWUudmFsdWUgPSAwLjAwNCArIGkgKiAwLjAwMjsKICAgICAgICBjYXJyaWVyLmNvbm5lY3QoZmVlZGJhY2spLmNvbm5lY3QoZ2FpbihmcmVxdWVuY3kgKiAoMC4xNSArIGNvbG9yICogMS40KSkpCiAgICAgICAgICAuY29ubmVjdChtb2QuZnJlcXVlbmN5KTsKICAgICAgICBjb25zdCBzaGFwZSA9IG9zYygnc2luZScsIDAuMTMgKyBpICogMC4wNyk7CiAgICAgICAgc2hhcGUuY29ubmVjdChnYWluKGZyZXF1ZW5jeSAqIHJhdGlvICogY29sb3IgKiAwLjI0KSkuY29ubmVjdChkZXB0aC5nYWluKTsKICAgICAgfQogICAgfQogICAgY29uc3QgZHJpZnQgPSBvc2MoJ3NpbmUnLCAwLjE5ICsgaSAqIDAuMTEgKyBjb2xvciAqIDAuMzUpOwogICAgZHJpZnQuY29ubmVjdChnYWluKDIgKyBjb2xvciAqIDYpKS5jb25uZWN0KGNhcnJpZXIuZGV0dW5lKTsKICAgIGlmIChraW5kID09PSAnYmxvb20nIHx8IGtpbmQgPT09ICdvcmJpdCcpIGRyaWZ0LmNvbm5lY3QoZ2FpbigwLjMpKS5jb25uZWN0KHBhbi5wYW4pOwogIH0pOwogIGxldCByZW1haW5pbmcgPSBvc2NpbGxhdG9ycy5sZW5ndGgsIGZpbmlzaGVkID0gZmFsc2U7CiAgZm9yIChjb25zdCBvc2NpbGxhdG9yIG9mIG9zY2lsbGF0b3JzKSB7CiAgICBvc2NpbGxhdG9yLm9uZW5kZWQgPSAoKSA9PiB7CiAgICAgIGlmICgtLXJlbWFpbmluZyA9PT0gMCAmJiAhZmluaXNoZWQpIHsKICAgICAgICBmaW5pc2hlZCA9IHRydWU7CiAgICAgICAgYWxsTm9kZXMuZm9yRWFjaChuID0+IG4uZGlzY29ubmVjdCgpKTsKICAgICAgICBvbmVuZGVkPy4oKTsKICAgICAgfQogICAgfTsKICAgIG9zY2lsbGF0b3Iuc3RhcnQodGltZSk7CiAgICBvc2NpbGxhdG9yLnN0b3AoZW5kKTsKICB9CiAgcmV0dXJuIHsKICAgIG5vZGUsCiAgICBzdG9wKGF0KSB7CiAgICAgIGlmIChmaW5pc2hlZCkgcmV0dXJuOwogICAgICBjb25zdCBzdG9wQXQgPSBNYXRoLm1heChjdHguY3VycmVudFRpbWUsIE1hdGgubWluKGF0LCBlbmQpKTsKICAgICAgbm9kZS5nYWluLmNhbmNlbEFuZEhvbGRBdFRpbWUoc3RvcEF0KTsKICAgICAgbm9kZS5nYWluLmxpbmVhclJhbXBUb1ZhbHVlQXRUaW1lKDAsIHN0b3BBdCArIDAuMDA1KTsKICAgICAgZm9yIChjb25zdCBvIG9mIG9zY2lsbGF0b3JzKSBvLnN0b3AoTWF0aC5taW4oZW5kLCBzdG9wQXQgKyAwLjAwNSkpOwogICAgfSwKICB9Owp9CgpmdW5jdGlvbiByZWdpc3Rlck5vdGVwYXQoYXBpID0gZ2xvYmFsVGhpcykgewogIGNvbnN0IHsgcmVnaXN0ZXJTb3VuZCwgZ2V0QXVkaW9Db250ZXh0LCBnZXRGcmVxdWVuY3lGcm9tVmFsdWUgfSA9IGFwaTsKICBpZiAoIVtyZWdpc3RlclNvdW5kLCBnZXRBdWRpb0NvbnRleHQsIGdldEZyZXF1ZW5jeUZyb21WYWx1ZV0uZXZlcnkoZm4gPT4gdHlwZW9mIGZuID09PSAnZnVuY3Rpb24nKSkgewogICAgdGhyb3cgbmV3IEVycm9yKCdJbXBvcnQgTm90ZXBhdCBpbnNpZGUgU3RydWRlbCwgb3IgcGFzcyBpdHMgYXVkaW8gQVBJIHRvIHJlZ2lzdGVyTm90ZXBhdCgpLicpOwogIH0KICBmb3IgKGNvbnN0IFtuYW1lLCByZWNpcGVdIG9mIE9iamVjdC5lbnRyaWVzKHZvaWNlcykpIHsKICAgIHJlZ2lzdGVyU291bmQobmFtZSwgKHRpbWUsIHZhbHVlLCBvbmVuZGVkLCBjcHMpID0+IHsKICAgICAgY29uc3QgY3R4ID0gZ2V0QXVkaW9Db250ZXh0KCk7CiAgICAgIGNvbnN0IGZyZXF1ZW5jeSA9IGJvdW5kZWQoZ2V0RnJlcXVlbmN5RnJvbVZhbHVlKHZhbHVlKSwgMjIwLCAxLCBjdHguc2FtcGxlUmF0ZSAvIDIgLSAxKTsKICAgICAgaWYgKHJlY2lwZS5tb2RhbCkgcmV0dXJuIG1hcmltYmFWb2ljZShjdHgsIHRpbWUsIHZhbHVlLCBmcmVxdWVuY3ksIG9uZW5kZWQsIHJlY2lwZS5tb2RhbCA9PT0gJ3N0b25lJyk7CiAgICAgIGlmIChyZWNpcGUuY2hpcnApIHJldHVybiBjaGlycFZvaWNlKGN0eCwgdGltZSwgdmFsdWUsIGZyZXF1ZW5jeSwgcmVjaXBlLmNoaXJwLCBvbmVuZGVkKTsKICAgICAgaWYgKHJlY2lwZS5yaWNoKSByZXR1cm4gcmljaFZvaWNlKGN0eCwgdGltZSwgdmFsdWUsIGZyZXF1ZW5jeSwgcmVjaXBlLnJpY2gsIG9uZW5kZWQsIGNwcyk7CiAgICAgIGNvbnN0IGR1cmF0aW9uID0gYm91bmRlZCh2YWx1ZS5kdXJhdGlvbiwgMC41LCAwLjAwMSwgMzYwMCk7CiAgICAgIGNvbnN0IGF0dGFjayA9IE1hdGgubWluKGJvdW5kZWQodmFsdWUuYXR0YWNrLCAwLjAyLCAwLCA2MCksIGR1cmF0aW9uKTsKICAgICAgY29uc3QgZGVjYXkgPSBNYXRoLm1pbihib3VuZGVkKHZhbHVlLmRlY2F5LCAwLCAwLCA2MCksIGR1cmF0aW9uIC0gYXR0YWNrKTsKICAgICAgY29uc3Qgc3VzdGFpbiA9IGJvdW5kZWQodmFsdWUuc3VzdGFpbiwgMSwgMCwgMSk7CiAgICAgIGNvbnN0IHJlbGVhc2UgPSBib3VuZGVkKHZhbHVlLnJlbGVhc2UsIDAuMSwgMCwgNjApOwogICAgICBjb25zdCBob2xkRW5kID0gdGltZSArIGR1cmF0aW9uOwogICAgICBjb25zdCBlbmQgPSBob2xkRW5kICsgcmVsZWFzZSArIDAuMDA1OwogICAgICBjb25zdCBub2RlID0gY3R4LmNyZWF0ZUdhaW4oKTsKICAgICAgbm9kZS5nYWluLnNldFZhbHVlQXRUaW1lKGF0dGFjayA/IDAgOiAxLCB0aW1lKTsKICAgICAgaWYgKGF0dGFjaykgbm9kZS5nYWluLmxpbmVhclJhbXBUb1ZhbHVlQXRUaW1lKDEsIHRpbWUgKyBhdHRhY2spOwogICAgICBub2RlLmdhaW4ubGluZWFyUmFtcFRvVmFsdWVBdFRpbWUoc3VzdGFpbiwgdGltZSArIGF0dGFjayArIGRlY2F5KTsKICAgICAgbm9kZS5nYWluLnNldFZhbHVlQXRUaW1lKHN1c3RhaW4sIGhvbGRFbmQpOwogICAgICBub2RlLmdhaW4ubGluZWFyUmFtcFRvVmFsdWVBdFRpbWUoMCwgaG9sZEVuZCArIHJlbGVhc2UpOwoKICAgICAgY29uc3Qgb3NjaWxsYXRvcnMgPSBbXTsKICAgICAgbGV0IHJlbWFpbmluZyA9IHJlY2lwZS5yYXRpb3MubGVuZ3RoOwogICAgICBsZXQgZmluaXNoZWQgPSBmYWxzZTsKICAgICAgZm9yIChsZXQgaSA9IDA7IGkgPCByZWNpcGUucmF0aW9zLmxlbmd0aDsgaSsrKSB7CiAgICAgICAgY29uc3Qgb3NjaWxsYXRvciA9IGN0eC5jcmVhdGVPc2NpbGxhdG9yKCk7CiAgICAgICAgb3NjaWxsYXRvci50eXBlID0gcmVjaXBlLndhdmU7CiAgICAgICAgb3NjaWxsYXRvci5mcmVxdWVuY3kudmFsdWUgPSBNYXRoLm1pbihmcmVxdWVuY3kgKiByZWNpcGUucmF0aW9zW2ldLCBjdHguc2FtcGxlUmF0ZSAvIDIgLSAxKTsKICAgICAgICBjb25zdCBnYWluID0gY3R4LmNyZWF0ZUdhaW4oKTsKICAgICAgICAvLyBQcmVzZXJ2ZSB0aGUgbmF0aXZlIHRyaWFuZ2xlIGxheWVyIGJhbGFuY2UuIENob3JkcyBzaGFyZSBoZWFkcm9vbS4KICAgICAgICBnYWluLmdhaW4udmFsdWUgPSAocmVjaXBlLmxldmVscz8uW2ldID8/IDAuNSAvIHJlY2lwZS5yYXRpb3MubGVuZ3RoKQogICAgICAgICAgKiAocmVjaXBlLndhdmUgPT09ICd0cmlhbmdsZScgPyAtMSA6IDEpOwogICAgICAgIG9zY2lsbGF0b3IuY29ubmVjdChnYWluKS5jb25uZWN0KG5vZGUpOwogICAgICAgIG9zY2lsbGF0b3Iub25lbmRlZCA9ICgpID0+IHsKICAgICAgICAgIG9zY2lsbGF0b3IuZGlzY29ubmVjdCgpOwogICAgICAgICAgZ2Fpbi5kaXNjb25uZWN0KCk7CiAgICAgICAgICBpZiAoLS1yZW1haW5pbmcgPT09IDAgJiYgIWZpbmlzaGVkKSB7CiAgICAgICAgICAgIGZpbmlzaGVkID0gdHJ1ZTsKICAgICAgICAgICAgbm9kZS5kaXNjb25uZWN0KCk7CiAgICAgICAgICAgIG9uZW5kZWQ/LigpOwogICAgICAgICAgfQogICAgICAgIH07CiAgICAgICAgb3NjaWxsYXRvci5zdGFydCh0aW1lKTsKICAgICAgICBvc2NpbGxhdG9yLnN0b3AoZW5kKTsKICAgICAgICBvc2NpbGxhdG9ycy5wdXNoKG9zY2lsbGF0b3IpOwogICAgICB9CiAgICAgIHJldHVybiB7CiAgICAgICAgbm9kZSwKICAgICAgICAvLyBTdHJ1ZGVsIGNhbGxzIHRoaXMgd2hlbiByZXRpcmluZyBhIHZvaWNlLCBpbmNsdWRpbmcgcG9seXBob255IHN0ZWFscy4KICAgICAgICBzdG9wKGF0KSB7CiAgICAgICAgICBpZiAoZmluaXNoZWQpIHJldHVybjsKICAgICAgICAgIGNvbnN0IHN0b3BBdCA9IE1hdGgubWF4KGN0eC5jdXJyZW50VGltZSwgTWF0aC5taW4oYXQsIGVuZCkpOwogICAgICAgICAgbm9kZS5nYWluLmNhbmNlbEFuZEhvbGRBdFRpbWUoc3RvcEF0KTsKICAgICAgICAgIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZSgwLCBzdG9wQXQgKyAwLjAwNSk7CiAgICAgICAgICBmb3IgKGNvbnN0IG9zY2lsbGF0b3Igb2Ygb3NjaWxsYXRvcnMpIG9zY2lsbGF0b3Iuc3RvcChNYXRoLm1pbihlbmQsIHN0b3BBdCArIDAuMDA1KSk7CiAgICAgICAgfSwKICAgICAgfTsKICAgIH0sIHsgdHlwZTogJ3N5bnRoJyB9KTsKICB9CiAgcmV0dXJuIE9iamVjdC5rZXlzKHZvaWNlcyk7Cn0KCnJlZ2lzdGVyTm90ZXBhdCgpOwoKfSkoKQoKLy8gUEFUIC8gZm9yIFdpbGwg4oCUIGh0dHBzOi8vcGF0LmFlc3RoZXRpYy5jb21wdXRlcgoKLy8gVk9JQ0VTIOKAlCBjaGFuZ2UgLnMoLi4uKSBpbiBhbnkgYmxvY2sgYmVsb3cuCi8vIGFjX29yYml0OiBmZWVkYmFjayBGTSwgaW50ZXJsb2NraW5nIHB1bHNlcywgbW92aW5nIHN0ZXJlbwovLyBhY19nbGFzczogbWV0YWxsaWMgRk0sIHRyaWFuZ2xlIGZpZnRoLCBpbmhhcm1vbmljIG92ZXJ0b25lcwovLyBhY19zd2FybTogZGV0dW5lZCBzYXdzLCB0cmlhbmdsZSBmaWZ0aCwgc2luZSBzdWIKLy8gYWNfYmxvb206IHNvZnRlciBGTSwgdHJpYW5nbGUvc2luZSBsYXllcnMsIHN0ZXJlbyBkcmlmdAovLyBhY19tYXJpbWJhOiByb3Nld29vZCBtb2RhbCBtYWxsZXQ7IC5uKCkgY2hhbmdlcyBicmlnaHRuZXNzIGFuZCByaW5nCi8vIGFjX3N0b25lOiBpbmhhcm1vbmljIGJhcjsgYWNfd2F0ZXI6IHJpc2luZyBkcmlwOyBhY19raWNrOiBmYWxsaW5nIHNpbmUgZHJ1bQovLyBTdG9uZSBXYXR0YWpldHRhIGFycmFuZ2VtZW50OiBodHRwczovL3BhdC5hZXN0aGV0aWMuY29tcHV0ZXIvc3RvbmUKLy8gYWNfdHJpYW5nbGVfZmlmdGggLyBhY190cmlhbmdsZSAvIGFjX3NpbmU6IG9yaWdpbmFsIHNpbXBsZSB2b2ljZXMKLy8gYWNfbWFqb3IgLyBhY19taW5vciAvIGFjX3N1czI6IG9uZSByb290IHRyaWdnZXJzIGEgdGhyZWUtbm90ZSBzaW5lIGNob3JkCi8vIFRoZXNlIGFyZSBjdXN0b20gcmVjaXBlcyB1c2luZyBlc3RhYmxpc2hlZCBzeW50aGVzaXMgdGVjaG5pcXVlcy4KLy8gVGhlIG9yaWdpbmFsIHNpeCBkZXJpdmUgZnJvbSBBQyBPUyBOb3RlcGF0OyB0aGUgZm91ciByaWNoIHZvaWNlcyBleHRlbmQgdGhlbS4KLy8gVHJhY2sgYXJyYW5nZW1lbnRzOiBodHRwczovL3BhdC5hZXN0aGV0aWMuY29tcHV0ZXIvbWFyaW1iYWJhCi8vIEZhc3RlciByZW1peDogaHR0cHM6Ly9wYXQuYWVzdGhldGljLmNvbXB1dGVyL21hcmltYmFiYS1vcmJpdAoKLy8gVElNRSDigJQgbmV3IG5vdGVzL2tleSBldmVyeSAxMCBtaW51dGVzIHdoZW4geW91IG9wZW4gb3IgcmUtZXZhbHVhdGUuCi8vIEtlZXAgYSB2ZXJzaW9uIGJ5IHJlcGxhY2luZyBEYXRlLm5vdygpIHdpdGggYSBmaXhlZCB0aW1lc3RhbXAuCi8vIFdoaWxlIHBsYXlpbmcsIHNpZ25hbHMga2VlcCBjaGFuZ2luZyB0aW1icmUsIHNwYWNlLCBhbmQgbGF5ZXIgbGV2ZWxzLgovLyBHZW5lcmF0ZWQgc3RyaW5ncyBuZWVkIG1pbmkoLi4uKTsgcXVvdGVkIHBhdHRlcm4gbGl0ZXJhbHMgdXNlIGRvdWJsZSBxdW90ZXMuCmNvbnN0IGVwb2NoID0gTWF0aC5mbG9vcihEYXRlLm5vdygpIC8gNjAwMDAwKQpsZXQgc2VlZCA9IGVwb2NoID4+PiAwCmNvbnN0IGRpY2UgPSAoKSA9PiAoKHNlZWQgPSAoTWF0aC5pbXVsKHNlZWQsIDE2NjQ1MjUpICsgMTAxMzkwNDIyMykgPj4+IDApIC8gNDI5NDk2NzI5NikKY29uc3Qgcm9vdCA9IFs0NSwgNDgsIDUwLCA1M11bZXBvY2ggJSA0XQpjb25zdCBzY2FsZSA9IFswLCAyLCAzLCA1LCA3LCA5LCAxMF0KY29uc3QgcGhyYXNlID0gKGNvdW50LCBvcGVuaW5nKSA9PiBBcnJheS5mcm9tKHtsZW5ndGg6IGNvdW50fSwgKF8sIGkpID0+CiAgaSA9PT0gMCA/IFN0cmluZyhyb290ICsgMjQgKyBvcGVuaW5nKSA6CiAgICBkaWNlKCkgPCAuMTYgPyAnficgOiBTdHJpbmcocm9vdCArIDI0ICsgc2NhbGVbTWF0aC5mbG9vcihkaWNlKCkgKiBzY2FsZS5sZW5ndGgpXSkKKS5qb2luKCcgJykKY29uc3QgbWVsb2R5ID0gcGhyYXNlKDEzLCAwKQpjb25zdCBzcGFya3MgPSBwaHJhc2UoNywgNykKY29uc3QgY2hvcmQgPSAob2Zmc2V0LCBpbnRlcnZhbHMpID0+ICdbJyArIGludGVydmFscy5tYXAobiA9PiByb290ICsgb2Zmc2V0ICsgbikuam9pbignLCcpICsgJ10nCmNvbnN0IGhhcm1vbnkgPSAnPCcgKyBbY2hvcmQoMCwgWzAsMyw3LDEwXSksIGNob3JkKC01LCBbMCwzLDddKSwgY2hvcmQoLTQsIFswLDQsN10pLCBjaG9yZCgtMiwgWzAsNCw3XSldLmpvaW4oJyAnKSArICc+JwpzZXRjcG0oMjQpCgovLyBPUkJJVCDigJQgLm4oMC4uMSkgaXMgdGhlIHRpbWJyZSBtYWNybzogZmVlZGJhY2svRk0gZGVwdGggYW5kIGJyaWdodG5lc3MuCi8vIHNpbmUvcGVybGluIGFyZSBTdHJ1ZGVsIHNpZ25hbHM7IGRpZmZlcmVudCBwZXJpb2RzIGF2b2lkIGEgc2hvcnQgc2hhcmVkIGxvb3AuCi8vIEVhY2ggJDogYmxvY2sgaXMgaW5kZXBlbmRlbnQuIFNlbGVjdCBhIHdob2xlIGJsb2NrIGFuZCB0b2dnbGUgLy8gdG8gbXV0ZSBpdC4KLy8gQWxsIGZvdXIgZW50ZXIgb24gYmVhdCBvbmU6IHRvbmljIGxlYWQsIGZpZnRoIGFjY2VudCwgY2hvcmQsIGFuZCBiYXNzLgokOiBub3RlKG1pbmkobWVsb2R5KSkucygnYWNfb3JiaXQnKS5zbG93KDIpCiAgLm4ocGVybGluLnNsb3coMTEpLnJhbmdlKC4xMiwuOTUpKQogIC5hdHRhY2soLjAwOCkuZGVjYXkoLjIpLnN1c3RhaW4oLjU1KS5yZWxlYXNlKC42KQogIC5nYWluKHNpbmUuc2xvdygxNykucmFuZ2UoLjMyLC41KSkKICAucGFuKHNpbmUuc2xvdygxOSkpLmxwZihzaW5lLnNsb3coMjMpLnJhbmdlKDYwMCw2MDAwKSkKICAuZGVsYXkoLjMpLmRlbGF5dGltZSguMzc1KS5kZWxheWZlZWRiYWNrKC4zNSkucm9vbSguMzUpCiAgLl9waWFub3JvbGwoKQoKLy8gR0xBU1Mg4oCUIHNldmVuLW5vdGUgcGhyYXNlIGFnYWluc3Qgb3JiaXQncyB0aGlydGVlbjsgbm90ZXMvcmVzdHMgdmFyeSBwZXIgZXBvY2guCiQ6IG5vdGUobWluaShzcGFya3MpKS5zKCdhY19nbGFzcycpLnNsb3coMykKICAubihzaW5lLnNsb3coMTMpLnJhbmdlKC4xLC45KSkKICAuYXR0YWNrKC4wMDQpLmdhaW4oc2luZS5zbG93KDI5KS5yYW5nZSguMTYsLjMpKS5yZWxlYXNlKC44KQogIC5wYW4oc2luZS5zbG93KDMxKSkuZGVsYXkoLjI1KS5kZWxheXRpbWUoLjUpLnJvb20oLjQ1KQoKLy8gQkxPT00g4oCUIGV4dGVuZGVkIGNob3JkczsgYXR0YWNrL2RlY2F5L3N1c3RhaW4vcmVsZWFzZSBhcmUgc2Vjb25kcywgZXhjZXB0Ci8vIHN1c3RhaW4gKDAuLjEpLiAuc2xvdyg0KSBzdHJldGNoZXMgdGhlIGhhcm1vbmljIHByb2dyZXNzaW9uLgokOiBub3RlKG1pbmkoaGFybW9ueSkpLnMoJ2FjX2Jsb29tJykuc2xvdyg0KQogIC5uKHBlcmxpbi5zbG93KDM3KS5yYW5nZSguMTUsLjgpKQogIC5hdHRhY2soLjA2KS5kZWNheSguNCkuc3VzdGFpbiguNjUpLnJlbGVhc2UoMS44KQogIC5nYWluKC4yKS5scGYoc2luZS5zbG93KDQxKS5yYW5nZSg2NTAsMzIwMCkpLnJvb20oLjUpCgovLyBTV0FSTSDigJQgYmFzcyB3aXRoIGNoYW5naW5nIHRvbmUgYW5kIGxldmVsLiBGaWx0ZXJzL2RlbGF5L3JldmVyYi9wYW4gYXJlCi8vIFN0cnVkZWwgZWZmZWN0czsgRk0sIGRldHVuZSwgYW5kIGludGVybmFsIG1vdmVtZW50IGJlbG9uZyB0byB0aGVzZSB2b2ljZXMuCiQ6IG5vdGUobWluaSgnPCcgKyBbcm9vdC0xMiwgcm9vdC0xNywgcm9vdC0xNiwgcm9vdC0xNF0uam9pbignICcpICsgJz4gfiB+IFt+ICcgKyByb290ICsgJ10nKSkKICAucygnYWNfc3dhcm0nKS5zbG93KDIpLm4oc2luZS5zbG93KDcpLnJhbmdlKC4xNSwuOCkpCiAgLmF0dGFjayguMDA2KS5yZWxlYXNlKC4yNSkuZ2FpbiguNCkubHBmKDg1MCkKCi8vIFNPTE8gTEFCIOKAlCBjb21tZW50IG91dCB0aGUgZm91ciBhY3RpdmUgYmxvY2tzIGFib3ZlIGFuZCB1bmNvbW1lbnQgdGhpcy4KLy8gbm90ZSguLi4pIGFjY2VwdHMgbm90ZSBuYW1lcyBvciBNSURJIG51bWJlcnM7IGZyZXEoLi4uKSBhY2NlcHRzIGhlcnR6LgovLyBBbGwgdm9pY2VzOiAuYXR0YWNrIC5kZWNheSAuc3VzdGFpbiAucmVsZWFzZSAuZ2FpbiAucGFuIC5scGYgLmhwZiAuZGVsYXkgLnJvb20KLy8gUmljaCB2b2ljZXM6IC5uKDAuLjEpLiBTeW50aC1zcGVjaWZpYyBTdHJ1ZGVsIGNvbnRyb2xzIHN1Y2ggYXMgLmZtKCkgYXJlCi8vIG5vdCB3aXJlZCBpbnRvIHRoZXNlIGN1c3RvbSB2b2ljZXM7IHVzZSAubigpIGZvciB0aGVpciBpbnRlcm5hbCBtb2R1bGF0aW9uLgovLyAkOiBub3RlKCJjMyBlYjMgZzMgYmIzIikucygnYWNfb3JiaXQnKS5uKCI8MCAuMjUgLjUgLjc1IDE+IikKLy8gICAuYXR0YWNrKC4wMikucmVsZWFzZSguNykuZ2FpbiguNCkucm9vbSguMykKLy8gJDogZnJlcSgiMTEwIDE2NSAyMjAiKS5zKCdhY190cmlhbmdsZV9maWZ0aCcpLmdhaW4oLjQpCg== diff --git a/ac-strudel/stone-paste.strudel b/ac-strudel/stone-paste.strudel new file mode 100644 index 0000000000..5ae45dad37 --- /dev/null +++ b/ac-strudel/stone-paste.strudel @@ -0,0 +1,333 @@ +(() => { +// AC OS Notepat voices for Strudel. https://aesthetic.computer +// Recipes: fedac/native/pieces/notepat.mjs (triangle Shift alternate and chords). +// Uses Web Audio's band-limited oscillators; this is not a recording of the +// native mixer, its echo, or its output saturation. +// Glass, swarm, bloom, and orbit extend those recipes as new Strudel sounds. + +const voices = Object.freeze({ + ac_triangle: { wave: 'triangle', ratios: [1], levels: [0.5] }, + ac_triangle_fifth: { wave: 'triangle', ratios: [1, 1.5], levels: [0.42, 0.12] }, + ac_sine: { wave: 'sine', ratios: [1], levels: [0.5] }, + ac_major: { wave: 'sine', ratios: [1, 2 ** (4 / 12), 2 ** (7 / 12)] }, + ac_minor: { wave: 'sine', ratios: [1, 2 ** (3 / 12), 2 ** (7 / 12)] }, + ac_sus2: { wave: 'sine', ratios: [1, 2 ** (2 / 12), 2 ** (7 / 12)] }, + ac_glass: { rich: 'glass' }, + ac_swarm: { rich: 'swarm' }, + ac_bloom: { rich: 'bloom' }, + ac_orbit: { rich: 'orbit' }, + ac_marimba: { modal: true }, + ac_stone: { modal: 'stone' }, + ac_water: { chirp: 'water' }, + ac_kick: { chirp: 'kick' }, +}); + +const bounded = (value, fallback, min, max) => { + const n = Number(value ?? fallback); + return Number.isFinite(n) ? Math.max(min, Math.min(max, n)) : fallback; +}; + +function marimbaVoice(ctx, time, value, frequency, onended, stone = false) { + // Modal reduction of pop/marimba/synths/marimba.mjs, rosewood preset. + // Preserves its 1:4:9.2 ratios and T60 decay relationships; omits the + // source renderer's mallet convolution and resonator tube model. + const color = bounded(value.n, 0.5, 0, 1); + const duration = bounded(value.duration, 0.5, 0.001, 3600); + const attack = Math.min(bounded(value.attack, 0.001, 0, 60), duration); + const decay = Math.min(bounded(value.decay, 0, 0, 60), duration - attack); + const sustain = bounded(value.sustain, 1, 0, 1); + const release = bounded(value.release, 0.25, 0, 60); + const end = time + duration + release + 0.005; + const node = ctx.createGain(); + node.gain.setValueAtTime(attack ? 0 : 1, time); + if (attack) node.gain.linearRampToValueAtTime(1, time + attack); + node.gain.linearRampToValueAtTime(sustain, time + attack + decay); + node.gain.setValueAtTime(sustain, time + duration); + node.gain.linearRampToValueAtTime(0, end - 0.005); + // Stone is a designed inharmonic bar surrogate, not the offline FEM model. + const ratios = stone ? [1, 2.756, 5.404] : [1, 4, 9.2]; + const t60 = stone ? [2.8, 1.1, 0.38] : [1.6, 0.32, 0.09]; + const levels = [1, 0.32 * (0.3 + color * 1.4), 0.1 * (0.3 + color * 1.4)]; + const scale = 0.48 / levels.reduce((a, b) => a + b, 0); + const oscillators = [], gains = []; + let remaining = 3, finished = false; + ratios.forEach((ratio, i) => { + const o = ctx.createOscillator(), g = ctx.createGain(); + o.frequency.value = Math.min(frequency * ratio, ctx.sampleRate * 0.45); + const amplitude = levels[i] * scale; + const ring = t60[i] * (0.8 + color * 0.8); + g.gain.setValueAtTime(amplitude, time); + g.gain.exponentialRampToValueAtTime(amplitude * 0.001, time + ring); + g.gain.exponentialRampToValueAtTime(0.0000001, time + ring * 2); + o.connect(g).connect(node); + o.onended = () => { + if (--remaining === 0 && !finished) { + finished = true; + [...oscillators, ...gains, node].forEach(n => n.disconnect()); + onended?.(); + } + }; + oscillators.push(o); gains.push(g); + o.start(time); o.stop(end); + }); + return { node, stop(at) { + if (finished) return; + const stopAt = Math.max(ctx.currentTime, Math.min(at, end)); + node.gain.cancelAndHoldAtTime(stopAt); + node.gain.linearRampToValueAtTime(0, stopAt + 0.005); + oscillators.forEach(o => o.stop(Math.min(end, stopAt + 0.005))); + } }; +} + +function chirpVoice(ctx, time, value, frequency, kind, onended) { + const color = bounded(value.n, 0.5, 0, 1); + const kick = kind === 'kick'; + const duration = bounded(value.duration, 0.2, 0.001, 3600); + const release = bounded(value.release, 0.08, 0, 60); + const end = time + duration + release + 0.005; + const node = ctx.createGain(), oscillator = ctx.createOscillator(); + const attack = Math.min(bounded(value.attack, kick ? 0.007 : 0.003, 0, 60), duration); + const ring = kick ? 0.12 + color * 0.2 : 0.055 + color * 0.1; + node.gain.setValueAtTime(attack ? 0 : 0.5, time); + if (attack) node.gain.linearRampToValueAtTime(0.5, time + attack); + node.gain.exponentialRampToValueAtTime(0.00001, Math.min(end - 0.005, time + attack + ring)); + node.gain.setValueAtTime(0, end); + oscillator.frequency.setValueAtTime(kick ? 126 + color * 20 : frequency * 0.55, time); + oscillator.frequency.exponentialRampToValueAtTime(kick ? 43 : frequency, + time + Math.min(duration, kick ? 0.055 + color * 0.04 : 0.07)); + oscillator.connect(node); + let finished = false; + oscillator.onended = () => { + if (finished) return; + finished = true; oscillator.disconnect(); node.disconnect(); onended?.(); + }; + oscillator.start(time); oscillator.stop(end); + return { node, stop(at) { + if (finished) return; + const stopAt = Math.max(ctx.currentTime, Math.min(at, end)); + node.gain.cancelAndHoldAtTime(stopAt); + node.gain.linearRampToValueAtTime(0, stopAt + 0.005); + oscillator.stop(Math.min(end, stopAt + 0.005)); + } }; +} + +function richVoice(ctx, time, value, frequency, kind, onended, cps = 0.5) { + // n is a 0..1 timbre macro: FM depth, detune, and filter motion. + const color = bounded(value.n, 0.5, 0, 1); + const duration = bounded(value.duration, 0.5, 0.001, 3600); + const attack = Math.min(bounded(value.attack, kind === 'bloom' ? 0.18 : 0.006, 0, 60), duration); + const decay = Math.min(bounded(value.decay, kind === 'glass' ? 0.3 : 0.1, 0, 60), duration - attack); + const sustain = bounded(value.sustain, kind === 'glass' ? 0.24 : 0.8, 0, 1); + const release = bounded(value.release, kind === 'glass' ? 0.35 : 0.6, 0, 60); + const holdEnd = time + duration; + const end = holdEnd + release + 0.005; + const node = ctx.createGain(); + const allNodes = [node], oscillators = []; + const keep = object => { allNodes.push(object); return object; }; + const gain = level => { + const g = keep(ctx.createGain()); g.gain.value = level; return g; + }; + const osc = (type, hz) => { + const o = keep(ctx.createOscillator()); + o.type = type; + o.frequency.value = Math.max(0.05, Math.min(hz, ctx.sampleRate * 0.4)); + oscillators.push(o); return o; + }; + const filter = keep(ctx.createBiquadFilter()); + filter.type = 'lowpass'; + filter.Q.value = 0.5; + const cutoff = Math.min(ctx.sampleRate * 0.4, frequency * (5 + color * 15)); + filter.frequency.setValueAtTime(cutoff, time); + filter.frequency.exponentialRampToValueAtTime( + Math.max(60, cutoff * (kind === 'glass' ? 0.24 : 0.55)), holdEnd, + ); + filter.connect(node); + node.gain.setValueAtTime(attack ? 0 : 1, time); + if (attack) node.gain.linearRampToValueAtTime(1, time + attack); + node.gain.linearRampToValueAtTime(sustain, time + attack + decay); + node.gain.setValueAtTime(sustain, holdEnd); + node.gain.linearRampToValueAtTime(0, holdEnd + release); + + const layers = kind === 'glass' + ? [[1, 0.24, 'sine', 0], [1.5, 0.10, 'triangle', 0], [2.756, 0.07, 'sine', 0]] + : kind === 'swarm' + ? [[1, 0.12, 'sawtooth', -1], [1, 0.12, 'sawtooth', 1], [1.5, 0.09, 'triangle', 0.4], [0.5, 0.10, 'sine', 0]] + : [[1, 0.15, 'triangle', -1], [1, 0.12, 'triangle', 1], [1.5, 0.09, 'sine', -0.5], [2, 0.06, 'sine', 0.5]]; + layers.forEach(([ratio, level, wave, spread], i) => { + const carrier = osc(wave, frequency * ratio); + carrier.detune.value = spread * (4 + color * 18); + const pan = keep(ctx.createStereoPanner()); + pan.pan.value = spread * 0.45; + const pulse = gain(kind === 'orbit' ? 0.72 : 1); + carrier.connect(gain(level)).connect(pulse).connect(pan).connect(filter); + if (kind === 'orbit') { + // Amplitude pulses locked to the scheduler's cycle rate. + osc('sine', bounded(cps, 0.5, 0.01, 20) * (i + 2)) + .connect(gain(0.28)).connect(pulse.gain); + } + if (kind !== 'swarm') { + const mod = osc('sine', frequency * ratio * (kind === 'glass' ? 2.756 : 1.5)); + const depth = gain(frequency * ratio * (0.08 + color * (kind === 'glass' ? 2.4 : 0.6))); + mod.connect(depth).connect(carrier.frequency); + depth.gain.setValueAtTime(depth.gain.value, time); + depth.gain.exponentialRampToValueAtTime( + Math.max(0.001, depth.gain.value * 0.08), time + Math.min(duration, 0.3 + color), + ); + if (kind === 'orbit') { + // Carrier -> delay -> modulator pitch -> carrier pitch. The delay + // makes the feedback cycle valid, while sine outputs bound its level. + const feedback = keep(ctx.createDelay(0.05)); + feedback.delayTime.value = 0.004 + i * 0.002; + carrier.connect(feedback).connect(gain(frequency * (0.15 + color * 1.4))) + .connect(mod.frequency); + const shape = osc('sine', 0.13 + i * 0.07); + shape.connect(gain(frequency * ratio * color * 0.24)).connect(depth.gain); + } + } + const drift = osc('sine', 0.19 + i * 0.11 + color * 0.35); + drift.connect(gain(2 + color * 6)).connect(carrier.detune); + if (kind === 'bloom' || kind === 'orbit') drift.connect(gain(0.3)).connect(pan.pan); + }); + let remaining = oscillators.length, finished = false; + for (const oscillator of oscillators) { + oscillator.onended = () => { + if (--remaining === 0 && !finished) { + finished = true; + allNodes.forEach(n => n.disconnect()); + onended?.(); + } + }; + oscillator.start(time); + oscillator.stop(end); + } + return { + node, + stop(at) { + if (finished) return; + const stopAt = Math.max(ctx.currentTime, Math.min(at, end)); + node.gain.cancelAndHoldAtTime(stopAt); + node.gain.linearRampToValueAtTime(0, stopAt + 0.005); + for (const o of oscillators) o.stop(Math.min(end, stopAt + 0.005)); + }, + }; +} + +function registerNotepat(api = globalThis) { + const { registerSound, getAudioContext, getFrequencyFromValue } = api; + if (![registerSound, getAudioContext, getFrequencyFromValue].every(fn => typeof fn === 'function')) { + throw new Error('Import Notepat inside Strudel, or pass its audio API to registerNotepat().'); + } + for (const [name, recipe] of Object.entries(voices)) { + registerSound(name, (time, value, onended, cps) => { + const ctx = getAudioContext(); + const frequency = bounded(getFrequencyFromValue(value), 220, 1, ctx.sampleRate / 2 - 1); + if (recipe.modal) return marimbaVoice(ctx, time, value, frequency, onended, recipe.modal === 'stone'); + if (recipe.chirp) return chirpVoice(ctx, time, value, frequency, recipe.chirp, onended); + if (recipe.rich) return richVoice(ctx, time, value, frequency, recipe.rich, onended, cps); + const duration = bounded(value.duration, 0.5, 0.001, 3600); + const attack = Math.min(bounded(value.attack, 0.02, 0, 60), duration); + const decay = Math.min(bounded(value.decay, 0, 0, 60), duration - attack); + const sustain = bounded(value.sustain, 1, 0, 1); + const release = bounded(value.release, 0.1, 0, 60); + const holdEnd = time + duration; + const end = holdEnd + release + 0.005; + const node = ctx.createGain(); + node.gain.setValueAtTime(attack ? 0 : 1, time); + if (attack) node.gain.linearRampToValueAtTime(1, time + attack); + node.gain.linearRampToValueAtTime(sustain, time + attack + decay); + node.gain.setValueAtTime(sustain, holdEnd); + node.gain.linearRampToValueAtTime(0, holdEnd + release); + + const oscillators = []; + let remaining = recipe.ratios.length; + let finished = false; + for (let i = 0; i < recipe.ratios.length; i++) { + const oscillator = ctx.createOscillator(); + oscillator.type = recipe.wave; + oscillator.frequency.value = Math.min(frequency * recipe.ratios[i], ctx.sampleRate / 2 - 1); + const gain = ctx.createGain(); + // Preserve the native triangle layer balance. Chords share headroom. + gain.gain.value = (recipe.levels?.[i] ?? 0.5 / recipe.ratios.length) + * (recipe.wave === 'triangle' ? -1 : 1); + oscillator.connect(gain).connect(node); + oscillator.onended = () => { + oscillator.disconnect(); + gain.disconnect(); + if (--remaining === 0 && !finished) { + finished = true; + node.disconnect(); + onended?.(); + } + }; + oscillator.start(time); + oscillator.stop(end); + oscillators.push(oscillator); + } + return { + node, + // Strudel calls this when retiring a voice, including polyphony steals. + stop(at) { + if (finished) return; + const stopAt = Math.max(ctx.currentTime, Math.min(at, end)); + node.gain.cancelAndHoldAtTime(stopAt); + node.gain.linearRampToValueAtTime(0, stopAt + 0.005); + for (const oscillator of oscillators) oscillator.stop(Math.min(end, stopAt + 0.005)); + }, + }; + }, { type: 'synth' }); + } + return Object.keys(voices); +} + +registerNotepat(); + +})() + +// STONE — @jeffrey / aesthetic.computer · pat.aesthetic.computer/stone +// Will: mute any complete $: block. All 12 parts are enabled; .n = voice color. +// Compact Wattajetta reinterpretation: rephrased melody, synthetic stone, no samples. +setcpm(132/4) +const bass = note("/2") +const tune = n("3 4 4 2 1 3 2 0 1 4 3 5 4 2 1 0 2").scale('E4:minor:pentatonic').palindrome().slow(17/8) +const color = perlin.slow(13).range(.15,.85) + +// Kick + sub: immediate, centered floor. +$: note("e1*4").s('ac_kick').n(.35).gain(.75) +$: bass.s('ac_sine').attack(.01).release(.1).gain(.3) + +// Swarm: offbeat gallop, filtered detuned saws + sub. +$: bass.transpose(12).struct("[~ x]*4").s('ac_swarm').n(.12) + .decay(.1).sustain(.05).release(.04).lpf(650).gain(.22) + +// Stone: 17-note phrase runs across bars; brightness, ring and pan drift. +$: tune.s('ac_stone').n(color).release(sine.slow(7.3).range(.1,1.2)) + .pan(sine.slow(13).range(.2,.8)).room(.25).gain(.3)._pianoroll() + +// Water: rotating five-note droplets, three hits against eight steps. +$: note("").euclid(3,8).s('ac_water').n(color) + .pan(sine.slow(17)).delay(.15).delaytime(.227).gain(.25) + +// Bowls: the bass two octaves up, long stone tails. +$: bass.transpose(24).s('ac_stone').n(.16).release(1.4).room(.4).gain(.12) + +// Marimba: a slower answer, with each phrase reversed on alternate turns. +$: n("0 3 2 4 1").scale('E3:minor:pentatonic').palindrome().slow(5/2) + .s('ac_marimba').n(color).pan(.3).room(.2).gain(.22) + +// Bloom: minor/suspended harmony, breathing on a different clock. +$: note("<[e3,g3,b3] [e3,a3,b3] [d3,g3,b3] [d3,a3,b3]>/4").s('ac_bloom') + .n(.1).attack(.04).release(.8).lpf(1400).gain(sine.slow(19).range(.06,.12)) + +// Notepat fifths: short syncopated triangle stabs. +$: note("e3 ~ [g3 a3] ~").s('ac_triangle_fifth').decay(.08).sustain(0).gain(.12) + +// Orbit: quiet feedback-FM counterline; seven notes against the stone's 17. +$: n("0 2 4 3 1 2 0").scale('E4:minor:pentatonic').slow(7/2).s('ac_orbit') + .n(color).release(.2).lpf(2300).pan(sine.slow(23)).gain(.09) + +// Glass: backbeat on 2 + 4; dry metallic strikes. +$: note("~ e4 ~ e4").s('ac_glass').n(.85).decay(.04).sustain(0).hpf(700).gain(.28) + +// Shakers: 5→7→9→11 hits, 18 bars each; the necklace rotates five steps/bar. +$: note('b6').euclidRot("<5 7 9 11>/18",16,"<0 5 10 15 4 9 14 3 8 13 2 7 12 1 6 11>") + .s('ac_glass').n(.9).attack(.001).decay(.018).sustain(0).release(.01).hpf(4500).gain(.1) diff --git a/ac-strudel/stone-paste.url b/ac-strudel/stone-paste.url new file mode 100644 index 0000000000..59b46f1204 --- /dev/null +++ b/ac-strudel/stone-paste.url @@ -0,0 +1 @@ +https://strudel.cc/#KCgpID0+IHsKLy8gQUMgT1MgTm90ZXBhdCB2b2ljZXMgZm9yIFN0cnVkZWwuIGh0dHBzOi8vYWVzdGhldGljLmNvbXB1dGVyCi8vIFJlY2lwZXM6IGZlZGFjL25hdGl2ZS9waWVjZXMvbm90ZXBhdC5tanMgKHRyaWFuZ2xlIFNoaWZ0IGFsdGVybmF0ZSBhbmQgY2hvcmRzKS4KLy8gVXNlcyBXZWIgQXVkaW8ncyBiYW5kLWxpbWl0ZWQgb3NjaWxsYXRvcnM7IHRoaXMgaXMgbm90IGEgcmVjb3JkaW5nIG9mIHRoZQovLyBuYXRpdmUgbWl4ZXIsIGl0cyBlY2hvLCBvciBpdHMgb3V0cHV0IHNhdHVyYXRpb24uCi8vIEdsYXNzLCBzd2FybSwgYmxvb20sIGFuZCBvcmJpdCBleHRlbmQgdGhvc2UgcmVjaXBlcyBhcyBuZXcgU3RydWRlbCBzb3VuZHMuCgpjb25zdCB2b2ljZXMgPSBPYmplY3QuZnJlZXplKHsKICBhY190cmlhbmdsZTogeyB3YXZlOiAndHJpYW5nbGUnLCByYXRpb3M6IFsxXSwgbGV2ZWxzOiBbMC41XSB9LAogIGFjX3RyaWFuZ2xlX2ZpZnRoOiB7IHdhdmU6ICd0cmlhbmdsZScsIHJhdGlvczogWzEsIDEuNV0sIGxldmVsczogWzAuNDIsIDAuMTJdIH0sCiAgYWNfc2luZTogeyB3YXZlOiAnc2luZScsIHJhdGlvczogWzFdLCBsZXZlbHM6IFswLjVdIH0sCiAgYWNfbWFqb3I6IHsgd2F2ZTogJ3NpbmUnLCByYXRpb3M6IFsxLCAyICoqICg0IC8gMTIpLCAyICoqICg3IC8gMTIpXSB9LAogIGFjX21pbm9yOiB7IHdhdmU6ICdzaW5lJywgcmF0aW9zOiBbMSwgMiAqKiAoMyAvIDEyKSwgMiAqKiAoNyAvIDEyKV0gfSwKICBhY19zdXMyOiB7IHdhdmU6ICdzaW5lJywgcmF0aW9zOiBbMSwgMiAqKiAoMiAvIDEyKSwgMiAqKiAoNyAvIDEyKV0gfSwKICBhY19nbGFzczogeyByaWNoOiAnZ2xhc3MnIH0sCiAgYWNfc3dhcm06IHsgcmljaDogJ3N3YXJtJyB9LAogIGFjX2Jsb29tOiB7IHJpY2g6ICdibG9vbScgfSwKICBhY19vcmJpdDogeyByaWNoOiAnb3JiaXQnIH0sCiAgYWNfbWFyaW1iYTogeyBtb2RhbDogdHJ1ZSB9LAogIGFjX3N0b25lOiB7IG1vZGFsOiAnc3RvbmUnIH0sCiAgYWNfd2F0ZXI6IHsgY2hpcnA6ICd3YXRlcicgfSwKICBhY19raWNrOiB7IGNoaXJwOiAna2ljaycgfSwKfSk7Cgpjb25zdCBib3VuZGVkID0gKHZhbHVlLCBmYWxsYmFjaywgbWluLCBtYXgpID0+IHsKICBjb25zdCBuID0gTnVtYmVyKHZhbHVlID8/IGZhbGxiYWNrKTsKICByZXR1cm4gTnVtYmVyLmlzRmluaXRlKG4pID8gTWF0aC5tYXgobWluLCBNYXRoLm1pbihtYXgsIG4pKSA6IGZhbGxiYWNrOwp9OwoKZnVuY3Rpb24gbWFyaW1iYVZvaWNlKGN0eCwgdGltZSwgdmFsdWUsIGZyZXF1ZW5jeSwgb25lbmRlZCwgc3RvbmUgPSBmYWxzZSkgewogIC8vIE1vZGFsIHJlZHVjdGlvbiBvZiBwb3AvbWFyaW1iYS9zeW50aHMvbWFyaW1iYS5tanMsIHJvc2V3b29kIHByZXNldC4KICAvLyBQcmVzZXJ2ZXMgaXRzIDE6NDo5LjIgcmF0aW9zIGFuZCBUNjAgZGVjYXkgcmVsYXRpb25zaGlwczsgb21pdHMgdGhlCiAgLy8gc291cmNlIHJlbmRlcmVyJ3MgbWFsbGV0IGNvbnZvbHV0aW9uIGFuZCByZXNvbmF0b3IgdHViZSBtb2RlbC4KICBjb25zdCBjb2xvciA9IGJvdW5kZWQodmFsdWUubiwgMC41LCAwLCAxKTsKICBjb25zdCBkdXJhdGlvbiA9IGJvdW5kZWQodmFsdWUuZHVyYXRpb24sIDAuNSwgMC4wMDEsIDM2MDApOwogIGNvbnN0IGF0dGFjayA9IE1hdGgubWluKGJvdW5kZWQodmFsdWUuYXR0YWNrLCAwLjAwMSwgMCwgNjApLCBkdXJhdGlvbik7CiAgY29uc3QgZGVjYXkgPSBNYXRoLm1pbihib3VuZGVkKHZhbHVlLmRlY2F5LCAwLCAwLCA2MCksIGR1cmF0aW9uIC0gYXR0YWNrKTsKICBjb25zdCBzdXN0YWluID0gYm91bmRlZCh2YWx1ZS5zdXN0YWluLCAxLCAwLCAxKTsKICBjb25zdCByZWxlYXNlID0gYm91bmRlZCh2YWx1ZS5yZWxlYXNlLCAwLjI1LCAwLCA2MCk7CiAgY29uc3QgZW5kID0gdGltZSArIGR1cmF0aW9uICsgcmVsZWFzZSArIDAuMDA1OwogIGNvbnN0IG5vZGUgPSBjdHguY3JlYXRlR2FpbigpOwogIG5vZGUuZ2Fpbi5zZXRWYWx1ZUF0VGltZShhdHRhY2sgPyAwIDogMSwgdGltZSk7CiAgaWYgKGF0dGFjaykgbm9kZS5nYWluLmxpbmVhclJhbXBUb1ZhbHVlQXRUaW1lKDEsIHRpbWUgKyBhdHRhY2spOwogIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZShzdXN0YWluLCB0aW1lICsgYXR0YWNrICsgZGVjYXkpOwogIG5vZGUuZ2Fpbi5zZXRWYWx1ZUF0VGltZShzdXN0YWluLCB0aW1lICsgZHVyYXRpb24pOwogIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZSgwLCBlbmQgLSAwLjAwNSk7CiAgLy8gU3RvbmUgaXMgYSBkZXNpZ25lZCBpbmhhcm1vbmljIGJhciBzdXJyb2dhdGUsIG5vdCB0aGUgb2ZmbGluZSBGRU0gbW9kZWwuCiAgY29uc3QgcmF0aW9zID0gc3RvbmUgPyBbMSwgMi43NTYsIDUuNDA0XSA6IFsxLCA0LCA5LjJdOwogIGNvbnN0IHQ2MCA9IHN0b25lID8gWzIuOCwgMS4xLCAwLjM4XSA6IFsxLjYsIDAuMzIsIDAuMDldOwogIGNvbnN0IGxldmVscyA9IFsxLCAwLjMyICogKDAuMyArIGNvbG9yICogMS40KSwgMC4xICogKDAuMyArIGNvbG9yICogMS40KV07CiAgY29uc3Qgc2NhbGUgPSAwLjQ4IC8gbGV2ZWxzLnJlZHVjZSgoYSwgYikgPT4gYSArIGIsIDApOwogIGNvbnN0IG9zY2lsbGF0b3JzID0gW10sIGdhaW5zID0gW107CiAgbGV0IHJlbWFpbmluZyA9IDMsIGZpbmlzaGVkID0gZmFsc2U7CiAgcmF0aW9zLmZvckVhY2goKHJhdGlvLCBpKSA9PiB7CiAgICBjb25zdCBvID0gY3R4LmNyZWF0ZU9zY2lsbGF0b3IoKSwgZyA9IGN0eC5jcmVhdGVHYWluKCk7CiAgICBvLmZyZXF1ZW5jeS52YWx1ZSA9IE1hdGgubWluKGZyZXF1ZW5jeSAqIHJhdGlvLCBjdHguc2FtcGxlUmF0ZSAqIDAuNDUpOwogICAgY29uc3QgYW1wbGl0dWRlID0gbGV2ZWxzW2ldICogc2NhbGU7CiAgICBjb25zdCByaW5nID0gdDYwW2ldICogKDAuOCArIGNvbG9yICogMC44KTsKICAgIGcuZ2Fpbi5zZXRWYWx1ZUF0VGltZShhbXBsaXR1ZGUsIHRpbWUpOwogICAgZy5nYWluLmV4cG9uZW50aWFsUmFtcFRvVmFsdWVBdFRpbWUoYW1wbGl0dWRlICogMC4wMDEsIHRpbWUgKyByaW5nKTsKICAgIGcuZ2Fpbi5leHBvbmVudGlhbFJhbXBUb1ZhbHVlQXRUaW1lKDAuMDAwMDAwMSwgdGltZSArIHJpbmcgKiAyKTsKICAgIG8uY29ubmVjdChnKS5jb25uZWN0KG5vZGUpOwogICAgby5vbmVuZGVkID0gKCkgPT4gewogICAgICBpZiAoLS1yZW1haW5pbmcgPT09IDAgJiYgIWZpbmlzaGVkKSB7CiAgICAgICAgZmluaXNoZWQgPSB0cnVlOwogICAgICAgIFsuLi5vc2NpbGxhdG9ycywgLi4uZ2FpbnMsIG5vZGVdLmZvckVhY2gobiA9PiBuLmRpc2Nvbm5lY3QoKSk7CiAgICAgICAgb25lbmRlZD8uKCk7CiAgICAgIH0KICAgIH07CiAgICBvc2NpbGxhdG9ycy5wdXNoKG8pOyBnYWlucy5wdXNoKGcpOwogICAgby5zdGFydCh0aW1lKTsgby5zdG9wKGVuZCk7CiAgfSk7CiAgcmV0dXJuIHsgbm9kZSwgc3RvcChhdCkgewogICAgaWYgKGZpbmlzaGVkKSByZXR1cm47CiAgICBjb25zdCBzdG9wQXQgPSBNYXRoLm1heChjdHguY3VycmVudFRpbWUsIE1hdGgubWluKGF0LCBlbmQpKTsKICAgIG5vZGUuZ2Fpbi5jYW5jZWxBbmRIb2xkQXRUaW1lKHN0b3BBdCk7CiAgICBub2RlLmdhaW4ubGluZWFyUmFtcFRvVmFsdWVBdFRpbWUoMCwgc3RvcEF0ICsgMC4wMDUpOwogICAgb3NjaWxsYXRvcnMuZm9yRWFjaChvID0+IG8uc3RvcChNYXRoLm1pbihlbmQsIHN0b3BBdCArIDAuMDA1KSkpOwogIH0gfTsKfQoKZnVuY3Rpb24gY2hpcnBWb2ljZShjdHgsIHRpbWUsIHZhbHVlLCBmcmVxdWVuY3ksIGtpbmQsIG9uZW5kZWQpIHsKICBjb25zdCBjb2xvciA9IGJvdW5kZWQodmFsdWUubiwgMC41LCAwLCAxKTsKICBjb25zdCBraWNrID0ga2luZCA9PT0gJ2tpY2snOwogIGNvbnN0IGR1cmF0aW9uID0gYm91bmRlZCh2YWx1ZS5kdXJhdGlvbiwgMC4yLCAwLjAwMSwgMzYwMCk7CiAgY29uc3QgcmVsZWFzZSA9IGJvdW5kZWQodmFsdWUucmVsZWFzZSwgMC4wOCwgMCwgNjApOwogIGNvbnN0IGVuZCA9IHRpbWUgKyBkdXJhdGlvbiArIHJlbGVhc2UgKyAwLjAwNTsKICBjb25zdCBub2RlID0gY3R4LmNyZWF0ZUdhaW4oKSwgb3NjaWxsYXRvciA9IGN0eC5jcmVhdGVPc2NpbGxhdG9yKCk7CiAgY29uc3QgYXR0YWNrID0gTWF0aC5taW4oYm91bmRlZCh2YWx1ZS5hdHRhY2ssIGtpY2sgPyAwLjAwNyA6IDAuMDAzLCAwLCA2MCksIGR1cmF0aW9uKTsKICBjb25zdCByaW5nID0ga2ljayA/IDAuMTIgKyBjb2xvciAqIDAuMiA6IDAuMDU1ICsgY29sb3IgKiAwLjE7CiAgbm9kZS5nYWluLnNldFZhbHVlQXRUaW1lKGF0dGFjayA/IDAgOiAwLjUsIHRpbWUpOwogIGlmIChhdHRhY2spIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZSgwLjUsIHRpbWUgKyBhdHRhY2spOwogIG5vZGUuZ2Fpbi5leHBvbmVudGlhbFJhbXBUb1ZhbHVlQXRUaW1lKDAuMDAwMDEsIE1hdGgubWluKGVuZCAtIDAuMDA1LCB0aW1lICsgYXR0YWNrICsgcmluZykpOwogIG5vZGUuZ2Fpbi5zZXRWYWx1ZUF0VGltZSgwLCBlbmQpOwogIG9zY2lsbGF0b3IuZnJlcXVlbmN5LnNldFZhbHVlQXRUaW1lKGtpY2sgPyAxMjYgKyBjb2xvciAqIDIwIDogZnJlcXVlbmN5ICogMC41NSwgdGltZSk7CiAgb3NjaWxsYXRvci5mcmVxdWVuY3kuZXhwb25lbnRpYWxSYW1wVG9WYWx1ZUF0VGltZShraWNrID8gNDMgOiBmcmVxdWVuY3ksCiAgICB0aW1lICsgTWF0aC5taW4oZHVyYXRpb24sIGtpY2sgPyAwLjA1NSArIGNvbG9yICogMC4wNCA6IDAuMDcpKTsKICBvc2NpbGxhdG9yLmNvbm5lY3Qobm9kZSk7CiAgbGV0IGZpbmlzaGVkID0gZmFsc2U7CiAgb3NjaWxsYXRvci5vbmVuZGVkID0gKCkgPT4gewogICAgaWYgKGZpbmlzaGVkKSByZXR1cm47CiAgICBmaW5pc2hlZCA9IHRydWU7IG9zY2lsbGF0b3IuZGlzY29ubmVjdCgpOyBub2RlLmRpc2Nvbm5lY3QoKTsgb25lbmRlZD8uKCk7CiAgfTsKICBvc2NpbGxhdG9yLnN0YXJ0KHRpbWUpOyBvc2NpbGxhdG9yLnN0b3AoZW5kKTsKICByZXR1cm4geyBub2RlLCBzdG9wKGF0KSB7CiAgICBpZiAoZmluaXNoZWQpIHJldHVybjsKICAgIGNvbnN0IHN0b3BBdCA9IE1hdGgubWF4KGN0eC5jdXJyZW50VGltZSwgTWF0aC5taW4oYXQsIGVuZCkpOwogICAgbm9kZS5nYWluLmNhbmNlbEFuZEhvbGRBdFRpbWUoc3RvcEF0KTsKICAgIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZSgwLCBzdG9wQXQgKyAwLjAwNSk7CiAgICBvc2NpbGxhdG9yLnN0b3AoTWF0aC5taW4oZW5kLCBzdG9wQXQgKyAwLjAwNSkpOwogIH0gfTsKfQoKZnVuY3Rpb24gcmljaFZvaWNlKGN0eCwgdGltZSwgdmFsdWUsIGZyZXF1ZW5jeSwga2luZCwgb25lbmRlZCwgY3BzID0gMC41KSB7CiAgLy8gbiBpcyBhIDAuLjEgdGltYnJlIG1hY3JvOiBGTSBkZXB0aCwgZGV0dW5lLCBhbmQgZmlsdGVyIG1vdGlvbi4KICBjb25zdCBjb2xvciA9IGJvdW5kZWQodmFsdWUubiwgMC41LCAwLCAxKTsKICBjb25zdCBkdXJhdGlvbiA9IGJvdW5kZWQodmFsdWUuZHVyYXRpb24sIDAuNSwgMC4wMDEsIDM2MDApOwogIGNvbnN0IGF0dGFjayA9IE1hdGgubWluKGJvdW5kZWQodmFsdWUuYXR0YWNrLCBraW5kID09PSAnYmxvb20nID8gMC4xOCA6IDAuMDA2LCAwLCA2MCksIGR1cmF0aW9uKTsKICBjb25zdCBkZWNheSA9IE1hdGgubWluKGJvdW5kZWQodmFsdWUuZGVjYXksIGtpbmQgPT09ICdnbGFzcycgPyAwLjMgOiAwLjEsIDAsIDYwKSwgZHVyYXRpb24gLSBhdHRhY2spOwogIGNvbnN0IHN1c3RhaW4gPSBib3VuZGVkKHZhbHVlLnN1c3RhaW4sIGtpbmQgPT09ICdnbGFzcycgPyAwLjI0IDogMC44LCAwLCAxKTsKICBjb25zdCByZWxlYXNlID0gYm91bmRlZCh2YWx1ZS5yZWxlYXNlLCBraW5kID09PSAnZ2xhc3MnID8gMC4zNSA6IDAuNiwgMCwgNjApOwogIGNvbnN0IGhvbGRFbmQgPSB0aW1lICsgZHVyYXRpb247CiAgY29uc3QgZW5kID0gaG9sZEVuZCArIHJlbGVhc2UgKyAwLjAwNTsKICBjb25zdCBub2RlID0gY3R4LmNyZWF0ZUdhaW4oKTsKICBjb25zdCBhbGxOb2RlcyA9IFtub2RlXSwgb3NjaWxsYXRvcnMgPSBbXTsKICBjb25zdCBrZWVwID0gb2JqZWN0ID0+IHsgYWxsTm9kZXMucHVzaChvYmplY3QpOyByZXR1cm4gb2JqZWN0OyB9OwogIGNvbnN0IGdhaW4gPSBsZXZlbCA9PiB7CiAgICBjb25zdCBnID0ga2VlcChjdHguY3JlYXRlR2FpbigpKTsgZy5nYWluLnZhbHVlID0gbGV2ZWw7IHJldHVybiBnOwogIH07CiAgY29uc3Qgb3NjID0gKHR5cGUsIGh6KSA9PiB7CiAgICBjb25zdCBvID0ga2VlcChjdHguY3JlYXRlT3NjaWxsYXRvcigpKTsKICAgIG8udHlwZSA9IHR5cGU7CiAgICBvLmZyZXF1ZW5jeS52YWx1ZSA9IE1hdGgubWF4KDAuMDUsIE1hdGgubWluKGh6LCBjdHguc2FtcGxlUmF0ZSAqIDAuNCkpOwogICAgb3NjaWxsYXRvcnMucHVzaChvKTsgcmV0dXJuIG87CiAgfTsKICBjb25zdCBmaWx0ZXIgPSBrZWVwKGN0eC5jcmVhdGVCaXF1YWRGaWx0ZXIoKSk7CiAgZmlsdGVyLnR5cGUgPSAnbG93cGFzcyc7CiAgZmlsdGVyLlEudmFsdWUgPSAwLjU7CiAgY29uc3QgY3V0b2ZmID0gTWF0aC5taW4oY3R4LnNhbXBsZVJhdGUgKiAwLjQsIGZyZXF1ZW5jeSAqICg1ICsgY29sb3IgKiAxNSkpOwogIGZpbHRlci5mcmVxdWVuY3kuc2V0VmFsdWVBdFRpbWUoY3V0b2ZmLCB0aW1lKTsKICBmaWx0ZXIuZnJlcXVlbmN5LmV4cG9uZW50aWFsUmFtcFRvVmFsdWVBdFRpbWUoCiAgICBNYXRoLm1heCg2MCwgY3V0b2ZmICogKGtpbmQgPT09ICdnbGFzcycgPyAwLjI0IDogMC41NSkpLCBob2xkRW5kLAogICk7CiAgZmlsdGVyLmNvbm5lY3Qobm9kZSk7CiAgbm9kZS5nYWluLnNldFZhbHVlQXRUaW1lKGF0dGFjayA/IDAgOiAxLCB0aW1lKTsKICBpZiAoYXR0YWNrKSBub2RlLmdhaW4ubGluZWFyUmFtcFRvVmFsdWVBdFRpbWUoMSwgdGltZSArIGF0dGFjayk7CiAgbm9kZS5nYWluLmxpbmVhclJhbXBUb1ZhbHVlQXRUaW1lKHN1c3RhaW4sIHRpbWUgKyBhdHRhY2sgKyBkZWNheSk7CiAgbm9kZS5nYWluLnNldFZhbHVlQXRUaW1lKHN1c3RhaW4sIGhvbGRFbmQpOwogIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZSgwLCBob2xkRW5kICsgcmVsZWFzZSk7CgogIGNvbnN0IGxheWVycyA9IGtpbmQgPT09ICdnbGFzcycKICAgID8gW1sxLCAwLjI0LCAnc2luZScsIDBdLCBbMS41LCAwLjEwLCAndHJpYW5nbGUnLCAwXSwgWzIuNzU2LCAwLjA3LCAnc2luZScsIDBdXQogICAgOiBraW5kID09PSAnc3dhcm0nCiAgICAgID8gW1sxLCAwLjEyLCAnc2F3dG9vdGgnLCAtMV0sIFsxLCAwLjEyLCAnc2F3dG9vdGgnLCAxXSwgWzEuNSwgMC4wOSwgJ3RyaWFuZ2xlJywgMC40XSwgWzAuNSwgMC4xMCwgJ3NpbmUnLCAwXV0KICAgICAgOiBbWzEsIDAuMTUsICd0cmlhbmdsZScsIC0xXSwgWzEsIDAuMTIsICd0cmlhbmdsZScsIDFdLCBbMS41LCAwLjA5LCAnc2luZScsIC0wLjVdLCBbMiwgMC4wNiwgJ3NpbmUnLCAwLjVdXTsKICBsYXllcnMuZm9yRWFjaCgoW3JhdGlvLCBsZXZlbCwgd2F2ZSwgc3ByZWFkXSwgaSkgPT4gewogICAgY29uc3QgY2FycmllciA9IG9zYyh3YXZlLCBmcmVxdWVuY3kgKiByYXRpbyk7CiAgICBjYXJyaWVyLmRldHVuZS52YWx1ZSA9IHNwcmVhZCAqICg0ICsgY29sb3IgKiAxOCk7CiAgICBjb25zdCBwYW4gPSBrZWVwKGN0eC5jcmVhdGVTdGVyZW9QYW5uZXIoKSk7CiAgICBwYW4ucGFuLnZhbHVlID0gc3ByZWFkICogMC40NTsKICAgIGNvbnN0IHB1bHNlID0gZ2FpbihraW5kID09PSAnb3JiaXQnID8gMC43MiA6IDEpOwogICAgY2Fycmllci5jb25uZWN0KGdhaW4obGV2ZWwpKS5jb25uZWN0KHB1bHNlKS5jb25uZWN0KHBhbikuY29ubmVjdChmaWx0ZXIpOwogICAgaWYgKGtpbmQgPT09ICdvcmJpdCcpIHsKICAgICAgLy8gQW1wbGl0dWRlIHB1bHNlcyBsb2NrZWQgdG8gdGhlIHNjaGVkdWxlcidzIGN5Y2xlIHJhdGUuCiAgICAgIG9zYygnc2luZScsIGJvdW5kZWQoY3BzLCAwLjUsIDAuMDEsIDIwKSAqIChpICsgMikpCiAgICAgICAgLmNvbm5lY3QoZ2FpbigwLjI4KSkuY29ubmVjdChwdWxzZS5nYWluKTsKICAgIH0KICAgIGlmIChraW5kICE9PSAnc3dhcm0nKSB7CiAgICAgIGNvbnN0IG1vZCA9IG9zYygnc2luZScsIGZyZXF1ZW5jeSAqIHJhdGlvICogKGtpbmQgPT09ICdnbGFzcycgPyAyLjc1NiA6IDEuNSkpOwogICAgICBjb25zdCBkZXB0aCA9IGdhaW4oZnJlcXVlbmN5ICogcmF0aW8gKiAoMC4wOCArIGNvbG9yICogKGtpbmQgPT09ICdnbGFzcycgPyAyLjQgOiAwLjYpKSk7CiAgICAgIG1vZC5jb25uZWN0KGRlcHRoKS5jb25uZWN0KGNhcnJpZXIuZnJlcXVlbmN5KTsKICAgICAgZGVwdGguZ2Fpbi5zZXRWYWx1ZUF0VGltZShkZXB0aC5nYWluLnZhbHVlLCB0aW1lKTsKICAgICAgZGVwdGguZ2Fpbi5leHBvbmVudGlhbFJhbXBUb1ZhbHVlQXRUaW1lKAogICAgICAgIE1hdGgubWF4KDAuMDAxLCBkZXB0aC5nYWluLnZhbHVlICogMC4wOCksIHRpbWUgKyBNYXRoLm1pbihkdXJhdGlvbiwgMC4zICsgY29sb3IpLAogICAgICApOwogICAgICBpZiAoa2luZCA9PT0gJ29yYml0JykgewogICAgICAgIC8vIENhcnJpZXIgLT4gZGVsYXkgLT4gbW9kdWxhdG9yIHBpdGNoIC0+IGNhcnJpZXIgcGl0Y2guIFRoZSBkZWxheQogICAgICAgIC8vIG1ha2VzIHRoZSBmZWVkYmFjayBjeWNsZSB2YWxpZCwgd2hpbGUgc2luZSBvdXRwdXRzIGJvdW5kIGl0cyBsZXZlbC4KICAgICAgICBjb25zdCBmZWVkYmFjayA9IGtlZXAoY3R4LmNyZWF0ZURlbGF5KDAuMDUpKTsKICAgICAgICBmZWVkYmFjay5kZWxheVRpbWUudmFsdWUgPSAwLjAwNCArIGkgKiAwLjAwMjsKICAgICAgICBjYXJyaWVyLmNvbm5lY3QoZmVlZGJhY2spLmNvbm5lY3QoZ2FpbihmcmVxdWVuY3kgKiAoMC4xNSArIGNvbG9yICogMS40KSkpCiAgICAgICAgICAuY29ubmVjdChtb2QuZnJlcXVlbmN5KTsKICAgICAgICBjb25zdCBzaGFwZSA9IG9zYygnc2luZScsIDAuMTMgKyBpICogMC4wNyk7CiAgICAgICAgc2hhcGUuY29ubmVjdChnYWluKGZyZXF1ZW5jeSAqIHJhdGlvICogY29sb3IgKiAwLjI0KSkuY29ubmVjdChkZXB0aC5nYWluKTsKICAgICAgfQogICAgfQogICAgY29uc3QgZHJpZnQgPSBvc2MoJ3NpbmUnLCAwLjE5ICsgaSAqIDAuMTEgKyBjb2xvciAqIDAuMzUpOwogICAgZHJpZnQuY29ubmVjdChnYWluKDIgKyBjb2xvciAqIDYpKS5jb25uZWN0KGNhcnJpZXIuZGV0dW5lKTsKICAgIGlmIChraW5kID09PSAnYmxvb20nIHx8IGtpbmQgPT09ICdvcmJpdCcpIGRyaWZ0LmNvbm5lY3QoZ2FpbigwLjMpKS5jb25uZWN0KHBhbi5wYW4pOwogIH0pOwogIGxldCByZW1haW5pbmcgPSBvc2NpbGxhdG9ycy5sZW5ndGgsIGZpbmlzaGVkID0gZmFsc2U7CiAgZm9yIChjb25zdCBvc2NpbGxhdG9yIG9mIG9zY2lsbGF0b3JzKSB7CiAgICBvc2NpbGxhdG9yLm9uZW5kZWQgPSAoKSA9PiB7CiAgICAgIGlmICgtLXJlbWFpbmluZyA9PT0gMCAmJiAhZmluaXNoZWQpIHsKICAgICAgICBmaW5pc2hlZCA9IHRydWU7CiAgICAgICAgYWxsTm9kZXMuZm9yRWFjaChuID0+IG4uZGlzY29ubmVjdCgpKTsKICAgICAgICBvbmVuZGVkPy4oKTsKICAgICAgfQogICAgfTsKICAgIG9zY2lsbGF0b3Iuc3RhcnQodGltZSk7CiAgICBvc2NpbGxhdG9yLnN0b3AoZW5kKTsKICB9CiAgcmV0dXJuIHsKICAgIG5vZGUsCiAgICBzdG9wKGF0KSB7CiAgICAgIGlmIChmaW5pc2hlZCkgcmV0dXJuOwogICAgICBjb25zdCBzdG9wQXQgPSBNYXRoLm1heChjdHguY3VycmVudFRpbWUsIE1hdGgubWluKGF0LCBlbmQpKTsKICAgICAgbm9kZS5nYWluLmNhbmNlbEFuZEhvbGRBdFRpbWUoc3RvcEF0KTsKICAgICAgbm9kZS5nYWluLmxpbmVhclJhbXBUb1ZhbHVlQXRUaW1lKDAsIHN0b3BBdCArIDAuMDA1KTsKICAgICAgZm9yIChjb25zdCBvIG9mIG9zY2lsbGF0b3JzKSBvLnN0b3AoTWF0aC5taW4oZW5kLCBzdG9wQXQgKyAwLjAwNSkpOwogICAgfSwKICB9Owp9CgpmdW5jdGlvbiByZWdpc3Rlck5vdGVwYXQoYXBpID0gZ2xvYmFsVGhpcykgewogIGNvbnN0IHsgcmVnaXN0ZXJTb3VuZCwgZ2V0QXVkaW9Db250ZXh0LCBnZXRGcmVxdWVuY3lGcm9tVmFsdWUgfSA9IGFwaTsKICBpZiAoIVtyZWdpc3RlclNvdW5kLCBnZXRBdWRpb0NvbnRleHQsIGdldEZyZXF1ZW5jeUZyb21WYWx1ZV0uZXZlcnkoZm4gPT4gdHlwZW9mIGZuID09PSAnZnVuY3Rpb24nKSkgewogICAgdGhyb3cgbmV3IEVycm9yKCdJbXBvcnQgTm90ZXBhdCBpbnNpZGUgU3RydWRlbCwgb3IgcGFzcyBpdHMgYXVkaW8gQVBJIHRvIHJlZ2lzdGVyTm90ZXBhdCgpLicpOwogIH0KICBmb3IgKGNvbnN0IFtuYW1lLCByZWNpcGVdIG9mIE9iamVjdC5lbnRyaWVzKHZvaWNlcykpIHsKICAgIHJlZ2lzdGVyU291bmQobmFtZSwgKHRpbWUsIHZhbHVlLCBvbmVuZGVkLCBjcHMpID0+IHsKICAgICAgY29uc3QgY3R4ID0gZ2V0QXVkaW9Db250ZXh0KCk7CiAgICAgIGNvbnN0IGZyZXF1ZW5jeSA9IGJvdW5kZWQoZ2V0RnJlcXVlbmN5RnJvbVZhbHVlKHZhbHVlKSwgMjIwLCAxLCBjdHguc2FtcGxlUmF0ZSAvIDIgLSAxKTsKICAgICAgaWYgKHJlY2lwZS5tb2RhbCkgcmV0dXJuIG1hcmltYmFWb2ljZShjdHgsIHRpbWUsIHZhbHVlLCBmcmVxdWVuY3ksIG9uZW5kZWQsIHJlY2lwZS5tb2RhbCA9PT0gJ3N0b25lJyk7CiAgICAgIGlmIChyZWNpcGUuY2hpcnApIHJldHVybiBjaGlycFZvaWNlKGN0eCwgdGltZSwgdmFsdWUsIGZyZXF1ZW5jeSwgcmVjaXBlLmNoaXJwLCBvbmVuZGVkKTsKICAgICAgaWYgKHJlY2lwZS5yaWNoKSByZXR1cm4gcmljaFZvaWNlKGN0eCwgdGltZSwgdmFsdWUsIGZyZXF1ZW5jeSwgcmVjaXBlLnJpY2gsIG9uZW5kZWQsIGNwcyk7CiAgICAgIGNvbnN0IGR1cmF0aW9uID0gYm91bmRlZCh2YWx1ZS5kdXJhdGlvbiwgMC41LCAwLjAwMSwgMzYwMCk7CiAgICAgIGNvbnN0IGF0dGFjayA9IE1hdGgubWluKGJvdW5kZWQodmFsdWUuYXR0YWNrLCAwLjAyLCAwLCA2MCksIGR1cmF0aW9uKTsKICAgICAgY29uc3QgZGVjYXkgPSBNYXRoLm1pbihib3VuZGVkKHZhbHVlLmRlY2F5LCAwLCAwLCA2MCksIGR1cmF0aW9uIC0gYXR0YWNrKTsKICAgICAgY29uc3Qgc3VzdGFpbiA9IGJvdW5kZWQodmFsdWUuc3VzdGFpbiwgMSwgMCwgMSk7CiAgICAgIGNvbnN0IHJlbGVhc2UgPSBib3VuZGVkKHZhbHVlLnJlbGVhc2UsIDAuMSwgMCwgNjApOwogICAgICBjb25zdCBob2xkRW5kID0gdGltZSArIGR1cmF0aW9uOwogICAgICBjb25zdCBlbmQgPSBob2xkRW5kICsgcmVsZWFzZSArIDAuMDA1OwogICAgICBjb25zdCBub2RlID0gY3R4LmNyZWF0ZUdhaW4oKTsKICAgICAgbm9kZS5nYWluLnNldFZhbHVlQXRUaW1lKGF0dGFjayA/IDAgOiAxLCB0aW1lKTsKICAgICAgaWYgKGF0dGFjaykgbm9kZS5nYWluLmxpbmVhclJhbXBUb1ZhbHVlQXRUaW1lKDEsIHRpbWUgKyBhdHRhY2spOwogICAgICBub2RlLmdhaW4ubGluZWFyUmFtcFRvVmFsdWVBdFRpbWUoc3VzdGFpbiwgdGltZSArIGF0dGFjayArIGRlY2F5KTsKICAgICAgbm9kZS5nYWluLnNldFZhbHVlQXRUaW1lKHN1c3RhaW4sIGhvbGRFbmQpOwogICAgICBub2RlLmdhaW4ubGluZWFyUmFtcFRvVmFsdWVBdFRpbWUoMCwgaG9sZEVuZCArIHJlbGVhc2UpOwoKICAgICAgY29uc3Qgb3NjaWxsYXRvcnMgPSBbXTsKICAgICAgbGV0IHJlbWFpbmluZyA9IHJlY2lwZS5yYXRpb3MubGVuZ3RoOwogICAgICBsZXQgZmluaXNoZWQgPSBmYWxzZTsKICAgICAgZm9yIChsZXQgaSA9IDA7IGkgPCByZWNpcGUucmF0aW9zLmxlbmd0aDsgaSsrKSB7CiAgICAgICAgY29uc3Qgb3NjaWxsYXRvciA9IGN0eC5jcmVhdGVPc2NpbGxhdG9yKCk7CiAgICAgICAgb3NjaWxsYXRvci50eXBlID0gcmVjaXBlLndhdmU7CiAgICAgICAgb3NjaWxsYXRvci5mcmVxdWVuY3kudmFsdWUgPSBNYXRoLm1pbihmcmVxdWVuY3kgKiByZWNpcGUucmF0aW9zW2ldLCBjdHguc2FtcGxlUmF0ZSAvIDIgLSAxKTsKICAgICAgICBjb25zdCBnYWluID0gY3R4LmNyZWF0ZUdhaW4oKTsKICAgICAgICAvLyBQcmVzZXJ2ZSB0aGUgbmF0aXZlIHRyaWFuZ2xlIGxheWVyIGJhbGFuY2UuIENob3JkcyBzaGFyZSBoZWFkcm9vbS4KICAgICAgICBnYWluLmdhaW4udmFsdWUgPSAocmVjaXBlLmxldmVscz8uW2ldID8/IDAuNSAvIHJlY2lwZS5yYXRpb3MubGVuZ3RoKQogICAgICAgICAgKiAocmVjaXBlLndhdmUgPT09ICd0cmlhbmdsZScgPyAtMSA6IDEpOwogICAgICAgIG9zY2lsbGF0b3IuY29ubmVjdChnYWluKS5jb25uZWN0KG5vZGUpOwogICAgICAgIG9zY2lsbGF0b3Iub25lbmRlZCA9ICgpID0+IHsKICAgICAgICAgIG9zY2lsbGF0b3IuZGlzY29ubmVjdCgpOwogICAgICAgICAgZ2Fpbi5kaXNjb25uZWN0KCk7CiAgICAgICAgICBpZiAoLS1yZW1haW5pbmcgPT09IDAgJiYgIWZpbmlzaGVkKSB7CiAgICAgICAgICAgIGZpbmlzaGVkID0gdHJ1ZTsKICAgICAgICAgICAgbm9kZS5kaXNjb25uZWN0KCk7CiAgICAgICAgICAgIG9uZW5kZWQ/LigpOwogICAgICAgICAgfQogICAgICAgIH07CiAgICAgICAgb3NjaWxsYXRvci5zdGFydCh0aW1lKTsKICAgICAgICBvc2NpbGxhdG9yLnN0b3AoZW5kKTsKICAgICAgICBvc2NpbGxhdG9ycy5wdXNoKG9zY2lsbGF0b3IpOwogICAgICB9CiAgICAgIHJldHVybiB7CiAgICAgICAgbm9kZSwKICAgICAgICAvLyBTdHJ1ZGVsIGNhbGxzIHRoaXMgd2hlbiByZXRpcmluZyBhIHZvaWNlLCBpbmNsdWRpbmcgcG9seXBob255IHN0ZWFscy4KICAgICAgICBzdG9wKGF0KSB7CiAgICAgICAgICBpZiAoZmluaXNoZWQpIHJldHVybjsKICAgICAgICAgIGNvbnN0IHN0b3BBdCA9IE1hdGgubWF4KGN0eC5jdXJyZW50VGltZSwgTWF0aC5taW4oYXQsIGVuZCkpOwogICAgICAgICAgbm9kZS5nYWluLmNhbmNlbEFuZEhvbGRBdFRpbWUoc3RvcEF0KTsKICAgICAgICAgIG5vZGUuZ2Fpbi5saW5lYXJSYW1wVG9WYWx1ZUF0VGltZSgwLCBzdG9wQXQgKyAwLjAwNSk7CiAgICAgICAgICBmb3IgKGNvbnN0IG9zY2lsbGF0b3Igb2Ygb3NjaWxsYXRvcnMpIG9zY2lsbGF0b3Iuc3RvcChNYXRoLm1pbihlbmQsIHN0b3BBdCArIDAuMDA1KSk7CiAgICAgICAgfSwKICAgICAgfTsKICAgIH0sIHsgdHlwZTogJ3N5bnRoJyB9KTsKICB9CiAgcmV0dXJuIE9iamVjdC5rZXlzKHZvaWNlcyk7Cn0KCnJlZ2lzdGVyTm90ZXBhdCgpOwoKfSkoKQoKLy8gU1RPTkUg4oCUIEBqZWZmcmV5IC8gYWVzdGhldGljLmNvbXB1dGVyIMK3IHBhdC5hZXN0aGV0aWMuY29tcHV0ZXIvc3RvbmUKLy8gV2lsbDogbXV0ZSBhbnkgY29tcGxldGUgJDogYmxvY2suIEFsbCAxMiBwYXJ0cyBhcmUgZW5hYmxlZDsgLm4gPSB2b2ljZSBjb2xvci4KLy8gQ29tcGFjdCBXYXR0YWpldHRhIHJlaW50ZXJwcmV0YXRpb246IHJlcGhyYXNlZCBtZWxvZHksIHN5bnRoZXRpYyBzdG9uZSwgbm8gc2FtcGxlcy4Kc2V0Y3BtKDEzMi80KQpjb25zdCBiYXNzID0gbm90ZSgiPGUxIGcxIGExIGQyIGUxIGcxIGExIGQyIGUxPi8yIikKY29uc3QgdHVuZSA9IG4oIjMgNCA0IDIgMSAzIDIgMCAxIDQgMyA1IDQgMiAxIDAgMiIpLnNjYWxlKCdFNDptaW5vcjpwZW50YXRvbmljJykucGFsaW5kcm9tZSgpLnNsb3coMTcvOCkKY29uc3QgY29sb3IgPSBwZXJsaW4uc2xvdygxMykucmFuZ2UoLjE1LC44NSkKCi8vIEtpY2sgKyBzdWI6IGltbWVkaWF0ZSwgY2VudGVyZWQgZmxvb3IuCiQ6IG5vdGUoImUxKjQiKS5zKCdhY19raWNrJykubiguMzUpLmdhaW4oLjc1KQokOiBiYXNzLnMoJ2FjX3NpbmUnKS5hdHRhY2soLjAxKS5yZWxlYXNlKC4xKS5nYWluKC4zKQoKLy8gU3dhcm06IG9mZmJlYXQgZ2FsbG9wLCBmaWx0ZXJlZCBkZXR1bmVkIHNhd3MgKyBzdWIuCiQ6IGJhc3MudHJhbnNwb3NlKDEyKS5zdHJ1Y3QoIlt+IHhdKjQiKS5zKCdhY19zd2FybScpLm4oLjEyKQogIC5kZWNheSguMSkuc3VzdGFpbiguMDUpLnJlbGVhc2UoLjA0KS5scGYoNjUwKS5nYWluKC4yMikKCi8vIFN0b25lOiAxNy1ub3RlIHBocmFzZSBydW5zIGFjcm9zcyBiYXJzOyBicmlnaHRuZXNzLCByaW5nIGFuZCBwYW4gZHJpZnQuCiQ6IHR1bmUucygnYWNfc3RvbmUnKS5uKGNvbG9yKS5yZWxlYXNlKHNpbmUuc2xvdyg3LjMpLnJhbmdlKC4xLDEuMikpCiAgLnBhbihzaW5lLnNsb3coMTMpLnJhbmdlKC4yLC44KSkucm9vbSguMjUpLmdhaW4oLjMpLl9waWFub3JvbGwoKQoKLy8gV2F0ZXI6IHJvdGF0aW5nIGZpdmUtbm90ZSBkcm9wbGV0cywgdGhyZWUgaGl0cyBhZ2FpbnN0IGVpZ2h0IHN0ZXBzLgokOiBub3RlKCI8ZTMgZzMgYTMgYjMgZDQ+IikuZXVjbGlkKDMsOCkucygnYWNfd2F0ZXInKS5uKGNvbG9yKQogIC5wYW4oc2luZS5zbG93KDE3KSkuZGVsYXkoLjE1KS5kZWxheXRpbWUoLjIyNykuZ2FpbiguMjUpCgovLyBCb3dsczogdGhlIGJhc3MgdHdvIG9jdGF2ZXMgdXAsIGxvbmcgc3RvbmUgdGFpbHMuCiQ6IGJhc3MudHJhbnNwb3NlKDI0KS5zKCdhY19zdG9uZScpLm4oLjE2KS5yZWxlYXNlKDEuNCkucm9vbSguNCkuZ2FpbiguMTIpCgovLyBNYXJpbWJhOiBhIHNsb3dlciBhbnN3ZXIsIHdpdGggZWFjaCBwaHJhc2UgcmV2ZXJzZWQgb24gYWx0ZXJuYXRlIHR1cm5zLgokOiBuKCIwIDMgMiA0IDEiKS5zY2FsZSgnRTM6bWlub3I6cGVudGF0b25pYycpLnBhbGluZHJvbWUoKS5zbG93KDUvMikKICAucygnYWNfbWFyaW1iYScpLm4oY29sb3IpLnBhbiguMykucm9vbSguMikuZ2FpbiguMjIpCgovLyBCbG9vbTogbWlub3Ivc3VzcGVuZGVkIGhhcm1vbnksIGJyZWF0aGluZyBvbiBhIGRpZmZlcmVudCBjbG9jay4KJDogbm90ZSgiPFtlMyxnMyxiM10gW2UzLGEzLGIzXSBbZDMsZzMsYjNdIFtkMyxhMyxiM10+LzQiKS5zKCdhY19ibG9vbScpCiAgLm4oLjEpLmF0dGFjayguMDQpLnJlbGVhc2UoLjgpLmxwZigxNDAwKS5nYWluKHNpbmUuc2xvdygxOSkucmFuZ2UoLjA2LC4xMikpCgovLyBOb3RlcGF0IGZpZnRoczogc2hvcnQgc3luY29wYXRlZCB0cmlhbmdsZSBzdGFicy4KJDogbm90ZSgiZTMgfiBbZzMgYTNdIH4iKS5zKCdhY190cmlhbmdsZV9maWZ0aCcpLmRlY2F5KC4wOCkuc3VzdGFpbigwKS5nYWluKC4xMikKCi8vIE9yYml0OiBxdWlldCBmZWVkYmFjay1GTSBjb3VudGVybGluZTsgc2V2ZW4gbm90ZXMgYWdhaW5zdCB0aGUgc3RvbmUncyAxNy4KJDogbigiMCAyIDQgMyAxIDIgMCIpLnNjYWxlKCdFNDptaW5vcjpwZW50YXRvbmljJykuc2xvdyg3LzIpLnMoJ2FjX29yYml0JykKICAubihjb2xvcikucmVsZWFzZSguMikubHBmKDIzMDApLnBhbihzaW5lLnNsb3coMjMpKS5nYWluKC4wOSkKCi8vIEdsYXNzOiBiYWNrYmVhdCBvbiAyICsgNDsgZHJ5IG1ldGFsbGljIHN0cmlrZXMuCiQ6IG5vdGUoIn4gZTQgfiBlNCIpLnMoJ2FjX2dsYXNzJykubiguODUpLmRlY2F5KC4wNCkuc3VzdGFpbigwKS5ocGYoNzAwKS5nYWluKC4yOCkKCi8vIFNoYWtlcnM6IDXihpI34oaSOeKGkjExIGhpdHMsIDE4IGJhcnMgZWFjaDsgdGhlIG5lY2tsYWNlIHJvdGF0ZXMgZml2ZSBzdGVwcy9iYXIuCiQ6IG5vdGUoJ2I2JykuZXVjbGlkUm90KCI8NSA3IDkgMTE+LzE4IiwxNiwiPDAgNSAxMCAxNSA0IDkgMTQgMyA4IDEzIDIgNyAxMiAxIDYgMTE+IikKICAucygnYWNfZ2xhc3MnKS5uKC45KS5hdHRhY2soLjAwMSkuZGVjYXkoLjAxOCkuc3VzdGFpbigwKS5yZWxlYXNlKC4wMSkuaHBmKDQ1MDApLmdhaW4oLjEpCg== diff --git a/ac-strudel/stone-score.strudel b/ac-strudel/stone-score.strudel new file mode 100644 index 0000000000..ab6655fe0d --- /dev/null +++ b/ac-strudel/stone-score.strudel @@ -0,0 +1,88 @@ +// STONE WATTAJETTA — @jeffrey / aesthetic.computer +// https://pat.aesthetic.computer/stone — for Will +// Instrumental reduction of pop/wattajetta/bin/render-wattajetta.mjs: +// original pentatonic cursor, E/G/A/D roots, four stone mutations, water gallop. +// 72-bar edit at 132 BPM; the current club renderer stretches this to 144 bars. +// Synth stone approximates the bells; no FEM audio, vocals or found FX here. +await import('https://pat.aesthetic.computer/s') +setcpm(132/4) + +// SCORE — one cycle = one 4/4 bar. The cursor never resets between bars. +// Change moves or pool to rewrite the tune; keep first strikes for an instant start. +const pool = ['e4', 'g4', 'a4', 'b4', 'd5', 'e5'] +const moves = [1, 1, 0, 2, -1, 1, -2, 1, 0, 1, 2, -1, 0, -2, 1, 1, -1] +let cursor = 2, direction = 1, step = 0 +const lines = [], roots = [], water = [], necklaces = [] +for (let bar = 0; bar < 72; bar++) { + const mutation = Math.floor(bar / 18) + roots.push(['e1', 'g1', 'a1', 'd2'][Math.floor((bar % 18) / 2) % 4]) + const line = [] + for (let eighth = 0; eighth < 8; eighth++) { + if (step > 0 && (step % 11 === 0 || step % 29 === 0)) direction *= -1 + cursor += moves[(step + mutation * 3) % moves.length] * direction + while (cursor < 0 || cursor >= pool.length) { + if (cursor < 0) cursor = -cursor + if (cursor >= pool.length) cursor = 2 * (pool.length - 1) - cursor + direction *= -1 + } + // Sparse opening, fuller later; every bar speaks on its downbeat. + line.push(eighth === 0 || (step * 7) % 11 < [5, 7, 8, 10][mutation] ? pool[cursor] : '~') + step++ + } + lines.push('[' + line.join(' ') + ']') + // Rising drips on the source's 1.75 / 3.25 / 3.75 beat positions. + const drips = Array(16).fill('~') + for (const slot of [7, 13, 15]) drips[slot] = ['e3', 'g3', 'a3', 'b3', 'd4'][(bar + slot) % 5] + if (bar === 0) drips[0] = 'e3' + water.push('[' + drips.join(' ') + ']') + // E(5/7/9/11,16), rotated five steps per bar: a changing percussion necklace. + const hits = [5, 7, 9, 11][mutation] + necklaces.push('[' + Array.from({length:16}, (_,i) => + (((i + bar * 5) % 16) * hits) % 16 < hits ? 'b6' : '~').join(' ') + ']') +} +const tune = mini(lines.join(' ')).note().slow(72) +const bass = mini('<' + roots.join(' ') + '>').note() +const drips = mini(water.join(' ')).note().slow(72) +const shaker = mini(necklaces.join(' ')).note().slow(72) + +// KICK — continuous floor; ac_kick sweeps a sine from ~126 Hz to 43 Hz. +// .n(0..1) changes pitch attack + decay. All parts below are active. +$: note("e1*4").s('ac_kick').n(.35).gain(.85).release(.04) + +// SUB — the source's two-bar root sequence, centered beneath the moving field. +// A quiet octave adds definition on smaller speakers. +$: bass.s('ac_sine').attack(.01).release(.08).gain(.32).lpf(190) +$: bass.transpose(12).s('ac_sine').attack(.015).release(.06).gain(.1).lpf(260) + +// GALLOP — four offbeat sine stabs; the sub already sounds on beat one. +$: bass.transpose(12).struct("[~ x]*4").s('ac_sine') + .attack(.005).decay(.1).sustain(.05).release(.035).gain(.5) + +// STONE LINE — asymmetric cursor; 18 bars per mutation, 72 before repeating. +// .n = strike brightness/ring. Long and short breaths drift past bar boundaries. +$: tune.s('ac_stone').n(perlin.slow(13).range(.15,.85)) + .release(sine.slow(7.3).range(.08,1.3)).gain(.38) + .pan(sine.slow(13).range(.2,.8)).room(.25)._pianoroll() + +// WATER — upward 0.55x-to-1x pitch chirps, from the renderer's bloop gesture. +// .n changes droplet length; delay and pan move independently of the stone. +$: drips.s('ac_water').n(perlin.slow(11)).gain(.32) + .pan(sine.slow(17)).delay(.16).delaytime(.227).release(.05) + +// BOWLS — lower stone bodies under each pair of bars. Different breathing clock. +$: bass.transpose(24).s('ac_stone').n(.16).attack(.004) + .release(1.4).gain(sine.slow(19).range(.1,.2)).room(.4) + +// BACKBEAT / SHAKERS — synthetic metallic percussion, no sample downloads. +// Backbeat on 2 & 4; the rotating necklace is audible from the opening. +$: note("~ e4 ~ e4").s('ac_glass').n(.85) + .attack(.001).decay(.04).sustain(0).release(.025).gain(.38).hpf(700) +$: shaker.s('ac_glass').n(.9).attack(.001).decay(.018).sustain(0) + .release(.012).gain(.12).hpf(4500).pan(sine.slow(23).range(.25,.75)) + +// Will: mute any complete $: block with /* ... */; leave the SCORE setup intact. +// Every part is enabled. The bass and kick start immediately; backbeat/gallop +// enter on their first rhythmic slot. No long intro or silent probability gate. +// Notes repeat after 72 bars; 7.3/11/13/17/19/23-cycle motion keeps changing. +// More voices + controls: https://pat.aesthetic.computer/source +// Standalone copy/paste: https://pat.aesthetic.computer/stone/paste diff --git a/ac-strudel/stone.strudel b/ac-strudel/stone.strudel new file mode 100644 index 0000000000..3563ed0ddd --- /dev/null +++ b/ac-strudel/stone.strudel @@ -0,0 +1,49 @@ +// STONE — @jeffrey / aesthetic.computer · pat.aesthetic.computer/stone +// Will: mute any complete $: block. All 12 parts are enabled; .n = voice color. +// Compact Wattajetta reinterpretation: rephrased melody, synthetic stone, no samples. +await import('https://pat.aesthetic.computer/s') +setcpm(132/4) +const bass = note("/2") +const tune = n("3 4 4 2 1 3 2 0 1 4 3 5 4 2 1 0 2").scale('E4:minor:pentatonic').palindrome().slow(17/8) +const color = perlin.slow(13).range(.15,.85) + +// Kick + sub: immediate, centered floor. +$: note("e1*4").s('ac_kick').n(.35).gain(.75) +$: bass.s('ac_sine').attack(.01).release(.1).gain(.3) + +// Swarm: offbeat gallop, filtered detuned saws + sub. +$: bass.transpose(12).struct("[~ x]*4").s('ac_swarm').n(.12) + .decay(.1).sustain(.05).release(.04).lpf(650).gain(.22) + +// Stone: 17-note phrase runs across bars; brightness, ring and pan drift. +$: tune.s('ac_stone').n(color).release(sine.slow(7.3).range(.1,1.2)) + .pan(sine.slow(13).range(.2,.8)).room(.25).gain(.3)._pianoroll() + +// Water: rotating five-note droplets, three hits against eight steps. +$: note("").euclid(3,8).s('ac_water').n(color) + .pan(sine.slow(17)).delay(.15).delaytime(.227).gain(.25) + +// Bowls: the bass two octaves up, long stone tails. +$: bass.transpose(24).s('ac_stone').n(.16).release(1.4).room(.4).gain(.12) + +// Marimba: a slower answer, with each phrase reversed on alternate turns. +$: n("0 3 2 4 1").scale('E3:minor:pentatonic').palindrome().slow(5/2) + .s('ac_marimba').n(color).pan(.3).room(.2).gain(.22) + +// Bloom: minor/suspended harmony, breathing on a different clock. +$: note("<[e3,g3,b3] [e3,a3,b3] [d3,g3,b3] [d3,a3,b3]>/4").s('ac_bloom') + .n(.1).attack(.04).release(.8).lpf(1400).gain(sine.slow(19).range(.06,.12)) + +// Notepat fifths: short syncopated triangle stabs. +$: note("e3 ~ [g3 a3] ~").s('ac_triangle_fifth').decay(.08).sustain(0).gain(.12) + +// Orbit: quiet feedback-FM counterline; seven notes against the stone's 17. +$: n("0 2 4 3 1 2 0").scale('E4:minor:pentatonic').slow(7/2).s('ac_orbit') + .n(color).release(.2).lpf(2300).pan(sine.slow(23)).gain(.09) + +// Glass: backbeat on 2 + 4; dry metallic strikes. +$: note("~ e4 ~ e4").s('ac_glass').n(.85).decay(.04).sustain(0).hpf(700).gain(.28) + +// Shakers: 5→7→9→11 hits, 18 bars each; the necklace rotates five steps/bar. +$: note('b6').euclidRot("<5 7 9 11>/18",16,"<0 5 10 15 4 9 14 3 8 13 2 7 12 1 6 11>") + .s('ac_glass').n(.9).attack(.001).decay(.018).sustain(0).release(.01).hpf(4500).gain(.1) diff --git a/ac-strudel/stone.url b/ac-strudel/stone.url new file mode 100644 index 0000000000..b3121fac67 --- /dev/null +++ b/ac-strudel/stone.url @@ -0,0 +1 @@ +https://strudel.cc/#Ly8gU1RPTkUg4oCUIEBqZWZmcmV5IC8gYWVzdGhldGljLmNvbXB1dGVyIMK3IHBhdC5hZXN0aGV0aWMuY29tcHV0ZXIvc3RvbmUKLy8gV2lsbDogbXV0ZSBhbnkgY29tcGxldGUgJDogYmxvY2suIEFsbCAxMiBwYXJ0cyBhcmUgZW5hYmxlZDsgLm4gPSB2b2ljZSBjb2xvci4KLy8gQ29tcGFjdCBXYXR0YWpldHRhIHJlaW50ZXJwcmV0YXRpb246IHJlcGhyYXNlZCBtZWxvZHksIHN5bnRoZXRpYyBzdG9uZSwgbm8gc2FtcGxlcy4KYXdhaXQgaW1wb3J0KCdodHRwczovL3BhdC5hZXN0aGV0aWMuY29tcHV0ZXIvcycpCnNldGNwbSgxMzIvNCkKY29uc3QgYmFzcyA9IG5vdGUoIjxlMSBnMSBhMSBkMiBlMSBnMSBhMSBkMiBlMT4vMiIpCmNvbnN0IHR1bmUgPSBuKCIzIDQgNCAyIDEgMyAyIDAgMSA0IDMgNSA0IDIgMSAwIDIiKS5zY2FsZSgnRTQ6bWlub3I6cGVudGF0b25pYycpLnBhbGluZHJvbWUoKS5zbG93KDE3LzgpCmNvbnN0IGNvbG9yID0gcGVybGluLnNsb3coMTMpLnJhbmdlKC4xNSwuODUpCgovLyBLaWNrICsgc3ViOiBpbW1lZGlhdGUsIGNlbnRlcmVkIGZsb29yLgokOiBub3RlKCJlMSo0IikucygnYWNfa2ljaycpLm4oLjM1KS5nYWluKC43NSkKJDogYmFzcy5zKCdhY19zaW5lJykuYXR0YWNrKC4wMSkucmVsZWFzZSguMSkuZ2FpbiguMykKCi8vIFN3YXJtOiBvZmZiZWF0IGdhbGxvcCwgZmlsdGVyZWQgZGV0dW5lZCBzYXdzICsgc3ViLgokOiBiYXNzLnRyYW5zcG9zZSgxMikuc3RydWN0KCJbfiB4XSo0IikucygnYWNfc3dhcm0nKS5uKC4xMikKICAuZGVjYXkoLjEpLnN1c3RhaW4oLjA1KS5yZWxlYXNlKC4wNCkubHBmKDY1MCkuZ2FpbiguMjIpCgovLyBTdG9uZTogMTctbm90ZSBwaHJhc2UgcnVucyBhY3Jvc3MgYmFyczsgYnJpZ2h0bmVzcywgcmluZyBhbmQgcGFuIGRyaWZ0LgokOiB0dW5lLnMoJ2FjX3N0b25lJykubihjb2xvcikucmVsZWFzZShzaW5lLnNsb3coNy4zKS5yYW5nZSguMSwxLjIpKQogIC5wYW4oc2luZS5zbG93KDEzKS5yYW5nZSguMiwuOCkpLnJvb20oLjI1KS5nYWluKC4zKS5fcGlhbm9yb2xsKCkKCi8vIFdhdGVyOiByb3RhdGluZyBmaXZlLW5vdGUgZHJvcGxldHMsIHRocmVlIGhpdHMgYWdhaW5zdCBlaWdodCBzdGVwcy4KJDogbm90ZSgiPGUzIGczIGEzIGIzIGQ0PiIpLmV1Y2xpZCgzLDgpLnMoJ2FjX3dhdGVyJykubihjb2xvcikKICAucGFuKHNpbmUuc2xvdygxNykpLmRlbGF5KC4xNSkuZGVsYXl0aW1lKC4yMjcpLmdhaW4oLjI1KQoKLy8gQm93bHM6IHRoZSBiYXNzIHR3byBvY3RhdmVzIHVwLCBsb25nIHN0b25lIHRhaWxzLgokOiBiYXNzLnRyYW5zcG9zZSgyNCkucygnYWNfc3RvbmUnKS5uKC4xNikucmVsZWFzZSgxLjQpLnJvb20oLjQpLmdhaW4oLjEyKQoKLy8gTWFyaW1iYTogYSBzbG93ZXIgYW5zd2VyLCB3aXRoIGVhY2ggcGhyYXNlIHJldmVyc2VkIG9uIGFsdGVybmF0ZSB0dXJucy4KJDogbigiMCAzIDIgNCAxIikuc2NhbGUoJ0UzOm1pbm9yOnBlbnRhdG9uaWMnKS5wYWxpbmRyb21lKCkuc2xvdyg1LzIpCiAgLnMoJ2FjX21hcmltYmEnKS5uKGNvbG9yKS5wYW4oLjMpLnJvb20oLjIpLmdhaW4oLjIyKQoKLy8gQmxvb206IG1pbm9yL3N1c3BlbmRlZCBoYXJtb255LCBicmVhdGhpbmcgb24gYSBkaWZmZXJlbnQgY2xvY2suCiQ6IG5vdGUoIjxbZTMsZzMsYjNdIFtlMyxhMyxiM10gW2QzLGczLGIzXSBbZDMsYTMsYjNdPi80IikucygnYWNfYmxvb20nKQogIC5uKC4xKS5hdHRhY2soLjA0KS5yZWxlYXNlKC44KS5scGYoMTQwMCkuZ2FpbihzaW5lLnNsb3coMTkpLnJhbmdlKC4wNiwuMTIpKQoKLy8gTm90ZXBhdCBmaWZ0aHM6IHNob3J0IHN5bmNvcGF0ZWQgdHJpYW5nbGUgc3RhYnMuCiQ6IG5vdGUoImUzIH4gW2czIGEzXSB+IikucygnYWNfdHJpYW5nbGVfZmlmdGgnKS5kZWNheSguMDgpLnN1c3RhaW4oMCkuZ2FpbiguMTIpCgovLyBPcmJpdDogcXVpZXQgZmVlZGJhY2stRk0gY291bnRlcmxpbmU7IHNldmVuIG5vdGVzIGFnYWluc3QgdGhlIHN0b25lJ3MgMTcuCiQ6IG4oIjAgMiA0IDMgMSAyIDAiKS5zY2FsZSgnRTQ6bWlub3I6cGVudGF0b25pYycpLnNsb3coNy8yKS5zKCdhY19vcmJpdCcpCiAgLm4oY29sb3IpLnJlbGVhc2UoLjIpLmxwZigyMzAwKS5wYW4oc2luZS5zbG93KDIzKSkuZ2FpbiguMDkpCgovLyBHbGFzczogYmFja2JlYXQgb24gMiArIDQ7IGRyeSBtZXRhbGxpYyBzdHJpa2VzLgokOiBub3RlKCJ+IGU0IH4gZTQiKS5zKCdhY19nbGFzcycpLm4oLjg1KS5kZWNheSguMDQpLnN1c3RhaW4oMCkuaHBmKDcwMCkuZ2FpbiguMjgpCgovLyBTaGFrZXJzOiA14oaSN+KGkjnihpIxMSBoaXRzLCAxOCBiYXJzIGVhY2g7IHRoZSBuZWNrbGFjZSByb3RhdGVzIGZpdmUgc3RlcHMvYmFyLgokOiBub3RlKCdiNicpLmV1Y2xpZFJvdCgiPDUgNyA5IDExPi8xOCIsMTYsIjwwIDUgMTAgMTUgNCA5IDE0IDMgOCAxMyAyIDcgMTIgMSA2IDExPiIpCiAgLnMoJ2FjX2dsYXNzJykubiguOSkuYXR0YWNrKC4wMDEpLmRlY2F5KC4wMTgpLnN1c3RhaW4oMCkucmVsZWFzZSguMDEpLmhwZig0NTAwKS5nYWluKC4xKQo= diff --git a/ac-strudel/verify-stone.mjs b/ac-strudel/verify-stone.mjs new file mode 100644 index 0000000000..619d84dafd --- /dev/null +++ b/ac-strudel/verify-stone.mjs @@ -0,0 +1,79 @@ +import { chromium } from 'playwright'; +import { readFile } from 'node:fs/promises'; +import assert from 'node:assert/strict'; +const read = name => readFile(new URL(name, import.meta.url), 'utf8'); +const source = await read('stone.strudel'); +const previous = await read('stone-score.strudel'); +assert(Buffer.byteLength(source) < Buffer.byteLength(previous) * .6); +const blockCode = Array.from(source.matchAll(/^\$:\s*([\s\S]*?)(?=\n\n|\n\$:|(?![\s\S]))/gm), m=>m[1]); +assert.equal(blockCode.length,12); +// Match Strudel's quoted mini-notation transformation for query-only inspection. +const compile = code => code.replace(/"([^"\n]*)"/g, (_,pattern)=>'mini('+JSON.stringify(pattern)+')'); +const setup = compile(source.slice(source.indexOf('const bass'),source.indexOf('// Kick'))); +const blocks = blockCode.map(code=>compile(code.replace('._pianoroll()',''))); +const names = ['ac_kick','ac_sine','ac_swarm','ac_stone','ac_water','ac_marimba', + 'ac_bloom','ac_triangle_fifth','ac_orbit','ac_glass']; +const browser = await chromium.launch({channel:'chrome',headless:true}); +try { + for (const paste of [false,true]) { + const page=await browser.newPage(); + page.setDefaultTimeout(60000); + page.setDefaultNavigationTimeout(60000); + const errors=[]; + page.on('pageerror',e=>errors.push(e.message)); + page.on('console',m=>{if(process.env.DEBUG)console.log(m.type(),m.text());if(m.type()==='error'||/getTrigger.*error|\[eval\].*error|Can't do arithmetic/i.test(m.text()))errors.push(m.text());}); + if(!process.env.LIVE)await page.route('https://pat.aesthetic.computer/s',async r=>r.fulfill({ + body:await read('notepat.mjs'),contentType:'text/javascript',headers:{'Access-Control-Allow-Origin':'*'}, + })); + await page.goto((await read(paste?'stone-paste.url':'stone.url')).trim(),{waitUntil:'domcontentloaded'}); + // CodeMirror only renders visible lines; the paste starts with the module. + await page.waitForFunction(()=>document.querySelector('.cm-content')?.innerText.includes('aesthetic.computer')); + await page.evaluate(()=>{ + globalThis.__patStarts=[]; + let original=globalThis.registerSound; + const observe=(name,trigger,...rest)=>original(name,(time,value,...args)=>{ + __patStarts.push({name,time,note:value.note,gain:value.gain});return trigger(time,value,...args); + },...rest); + Object.defineProperty(globalThis,'registerSound',{configurable:true,get:()=>observe,set:fn=>{if(fn!==observe)original=fn;}}); + }); + await page.getByRole('button',{name:'play',exact:true}).click(); + await page.waitForFunction(()=>typeof globalThis.getSound?.('ac_kick')?.onTrigger==='function'); + await page.waitForTimeout(4500); + assert.deepEqual(errors,[]); + const starts=await page.evaluate(()=>__patStarts); + for(const name of names)assert(starts.some(e=>e.name===name),name+' failed to trigger'); + const opening=['ac_kick','ac_sine','ac_stone','ac_water','ac_marimba','ac_bloom','ac_triangle_fifth','ac_orbit'] + .map(name=>starts.find(e=>e.name===name)); + assert(Math.max(...opening.map(e=>e.time))-Math.min(...opening.map(e=>e.time))<.01); + const stats=await page.evaluate(({setup,blocks})=>{ + const patterns=Function(setup+';return ['+blocks.join(',')+']')(); + return patterns.map(p=>({ + counts:[0,18,36,54].map(b=>p.queryArc(b,b+1).filter(h=>h.hasOnset()).length), + events:p.queryArc(0,72).filter(h=>h.hasOnset()).map(h=>({ + at:+h.whole.begin,end:+h.whole.end,...h.value, + })), + colors:[0,18,36,54,72].map(b=>p.queryArc(b,b+1)[0]?.value.n), + })); + },{setup,blocks}); + assert.deepEqual(stats.at(-1).counts,[5,7,9,11]); + assert.equal(stats[0].events.length,288,'Four-on-floor across 72 bars'); + for (const [bar,note] of [[0,'e1'],[2,'g1'],[4,'a1'],[6,'d2']]) assert(stats[1].events.some(e=>e.at===bar&&e.note===note)); + assert.equal(stats[2].events.length,288,'Four gallops in every bar'); + assert(new Set(stats[3].colors).size>3,'Stone timbre must keep evolving'); + const voices=new Set(); + for(const [i,part] of stats.entries()){ + assert(part.events.length>0 && part.events.length<1500,'Part '+i+' has invalid event density'); + assert(part.counts[0]>0,'Every part must enter in the first bar'); + for(const event of part.events){ + assert(Number.isFinite(event.at)&&event.end>event.at); + assert(Number.isFinite(event.gain)&&event.gain>0&&event.gain<=.75, JSON.stringify({part:i,event})); + assert(Number.isFinite(event.note)||/^[a-g][#b]?-?\d$/i.test(event.note),'Invalid note '+event.note); + voices.add(event.s); + } + } + assert.equal(voices.size,10); + console.log('stone',paste?'paste':'import',': 12 parts / 10 voices, immediate opening, 72-bar floor and density changes, evolving timbre, valid pitches'); + await page.getByRole('button',{name:'stop',exact:true}).click(); + await page.close(); + } +}finally{await browser.close();} diff --git a/ac-strudel/verify-tracks.mjs b/ac-strudel/verify-tracks.mjs new file mode 100644 index 0000000000..d80c6877a5 --- /dev/null +++ b/ac-strudel/verify-tracks.mjs @@ -0,0 +1,61 @@ +import { chromium } from 'playwright'; +import { readFile } from 'node:fs/promises'; +import assert from 'node:assert/strict'; +const read = name => readFile(new URL(name, import.meta.url), 'utf8'); +const reference = JSON.parse(await read('marimbaba-score.json')); +const expected = reference.events.flatMap(event => event.pitches.map(note => ({ + note, at: event.at / 3, end: (event.at + event.beats) / 3, +}))).sort((a,b) => a.at-b.at || a.note.localeCompare(b.note)); +const browser = await chromium.launch({channel:'chrome',headless:true}); +try { + for (const name of ['marimbaba', 'marimbaba-orbit']) { + for (const paste of [false,true]) { + const page = await browser.newPage(); + const errors = []; + page.on('pageerror', error => errors.push(error.message)); + page.on('console', message => { + if (message.type() === 'error' || /getTrigger.*error|\[eval\].*error/i.test(message.text())) errors.push(message.text()); + }); + if (!process.env.LIVE) await page.route('https://pat.aesthetic.computer/s', async route => route.fulfill({ + body: await read('notepat.mjs'),contentType:'text/javascript',headers:{'Access-Control-Allow-Origin':'*'}, + })); + const source = await read(name + (paste ? '-paste' : '') + '.strudel'); + if (paste) assert(!source.includes('await import(')); + await page.goto((await read(name+(paste?'-paste':'')+'.url')).trim(),{waitUntil:'domcontentloaded'}); + await page.waitForFunction(()=>document.querySelector('.cm-content')?.innerText.includes('ac_marimba')); + await page.evaluate(()=>{ + globalThis.__patStarts=[]; + let original=globalThis.registerSound; + const observe=(name,trigger,...rest)=>original(name,(time,value,...args)=>{ + __patStarts.push({name,time,note:value.note,gain:value.gain});return trigger(time,value,...args); + },...rest); + Object.defineProperty(globalThis,'registerSound',{configurable:true,get:()=>observe,set:fn=>{if(fn!==observe) original=fn;}}); + }); + await page.getByRole('button',{name:'play',exact:true}).click(); + await page.waitForFunction(()=>typeof globalThis.getSound?.('ac_marimba')?.onTrigger==='function'); + await page.waitForTimeout(1500); + assert.deepEqual(errors,[]); + const starts=await page.evaluate(()=>__patStarts); + const names=name.endsWith('orbit')?['ac_marimba','ac_swarm','ac_bloom','ac_orbit']:['ac_marimba','ac_sine','ac_bloom']; + const opening=names.map(n=>starts.find(e=>e.name===n)); + assert(opening.every(Boolean),JSON.stringify({name,starts})); + assert(Math.max(...opening.map(e=>e.time))-Math.min(...opening.map(e=>e.time))<.01); + assert.equal(opening[0].note,'c5'); + const expression=source.match(/const melody = ([\s\S]*?)\n\n\/\/ SINGER/)[1]; + const events=await page.evaluate(expression=>{ + const melody=Function('return '+expression)(); + return melody.queryArc(0,24).map(h=>({note:h.value.note,at:+h.whole.begin,end:+h.whole.end})); + },expression); + events.sort((a,b)=>a.at-b.at||a.note.localeCompare(b.note)); + assert.equal(events.length,expected.length); + events.forEach((event,i)=>{ + assert.equal(event.note,expected[i].note); + assert(Math.abs(event.at-expected[i].at)<1e-8); + assert(Math.abs(event.end-expected[i].end)<1e-8); + }); + console.log(name,paste?'standalone':'import',': all layers start together; all',events.length,'score notes retain pitch, onset, and duration'); + await page.getByRole('button',{name:'stop',exact:true}).click(); + await page.close(); + } + } +} finally { await browser.close(); } diff --git a/ac-strudel/verify.mjs b/ac-strudel/verify.mjs new file mode 100644 index 0000000000..cfb06bb424 --- /dev/null +++ b/ac-strudel/verify.mjs @@ -0,0 +1,222 @@ +import { chromium } from "playwright"; +import { fileURLToPath } from "node:url"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import fs from "node:fs"; +import { runInNewContext } from "node:vm"; +import assert from "node:assert/strict"; +const root = fileURLToPath(new URL("./", import.meta.url)); +const output = fs.mkdtempSync(join(tmpdir(), "ac-strudel-check-")); +const exampleSource = fs.readFileSync(root + "example.strudel", "utf8"); +const generator = exampleSource.slice(exampleSource.indexOf("const epoch"), exampleSource.indexOf("setcpm(")); +const scoreAt = now => runInNewContext(generator + ";JSON.stringify({root, melody, sparks, harmony})", { Date: { now: () => now } }); +assert.equal(scoreAt(1791080400000), scoreAt(1791080400001)); +assert.notEqual(scoreAt(1791080400000), scoreAt(1791081000000)); +for (let epoch = 2985130; epoch < 2985230; epoch++) { + const score = JSON.parse(scoreAt(epoch * 600000)); + assert.equal(Number(score.melody.split(' ')[0]), score.root + 24); + assert.equal(Number(score.sparks.split(' ')[0]), score.root + 31); +} +assert(!fs.readFileSync(root + "notepat-paste.strudel", "utf8").includes("await import(")); +(async () => { + const browser = await chromium.launch({ channel: "chrome", headless: true }); + try { + for (const mode of process.env.MODE + ? [process.env.MODE] + : ["example", "standalone"]) { + const page = await browser.newPage(); + const errors = []; + const logs = []; + page.on("pageerror", (e) => errors.push(e.message)); + page.on("console", (m) => { + if (m.type() === "error" || /getTrigger.*error|\[eval\].*error/i.test(m.text())) errors.push(m.text()); + else logs.push(m.text()); + }); + if (!process.env.LIVE) + await page.route( + "https://pat.aesthetic.computer/s", + (r) => + r.fulfill({ + body: fs.readFileSync(root + "notepat.mjs", "utf8"), + contentType: "text/javascript", + headers: { "Access-Control-Allow-Origin": "*" }, + }), + ); + await page.goto(fs.readFileSync(root + mode + ".url", "utf8").trim(), { + waitUntil: "domcontentloaded", + }); + await page.waitForFunction(() => + document + .querySelector(".cm-content") + ?.innerText.includes("ac_glass"), + ); + const code = await page.locator(".cm-content").innerText(); + if (!code.includes("ac_glass") || code.includes("tr909")) + throw new Error("Wrong code in editor"); + await page.evaluate(() => { + globalThis.__patStarts = []; + let original = globalThis.registerSound; + const observe = (name, trigger, ...rest) => original(name, (time, value, ...args) => { + globalThis.__patStarts.push({name, time, gain: value.gain}); + return trigger(time, value, ...args); + }, ...rest); + Object.defineProperty(globalThis, 'registerSound', { + configurable: true, get: () => observe, set: fn => { if (fn !== observe) original = fn; }, + }); + }); + await page.getByRole("button", { name: "play", exact: true }).click(); + await page.waitForFunction(() => + typeof globalThis.getSound?.("ac_glass")?.onTrigger === "function", + ); + await page.waitForTimeout(2200); + const state = await page.evaluate(() => ({ + voice: typeof getSound("ac_glass")?.onTrigger, + audio: getAudioContext().state, + })); + if ( + state.voice !== "function" || + state.audio !== "running" || + errors.length + ) + throw new Error(JSON.stringify({ state, errors })); + const starts = await page.evaluate(() => globalThis.__patStarts); + const opening = ['ac_orbit', 'ac_glass', 'ac_bloom', 'ac_swarm'].map(name => starts.find(event => event.name === name)); + assert(opening.every(Boolean), 'Every layer must start immediately: ' + JSON.stringify(starts)); + assert(Math.max(...opening.map(e => e.time)) - Math.min(...opening.map(e => e.time)) < 0.01, + 'All four layers must share the opening beat'); + assert(opening.every(e => e.gain >= 0.15), 'Opening layers must have audible gain'); + console.log('Opening: all four layers start together', opening.map(e => e.name)); + if (mode === "example") { + const evolution = await page.evaluate(() => { + const motion = note('c3').n(perlin.slow(11).range(.12, .95)); + return [0, 16, 48].map(cycle => motion.queryArc(cycle, cycle + 1)[0].value.n); + }); + assert(evolution.every(Number.isFinite)); + assert(new Set(evolution).size > 1, "Timbre must evolve during playback"); + const render = await page.evaluate(async () => { + const { registerNotepat, voices } = + await import("https://pat.aesthetic.computer/s"); + const results = []; + for (const name of Object.keys(voices)) { + const ctx = new OfflineAudioContext(1, 44100, 44100); + const sounds = {}; + let ended = 0; + registerNotepat({ + registerSound: (n, f) => (sounds[n] = f), + getAudioContext: () => ctx, + getFrequencyFromValue, + }); + const handle = sounds[name]( + 0, + { note: "a3", duration: 0.4, attack: 0.02, release: 0.1 }, + () => ended++, + ); + handle.node.connect(ctx.destination); + const data = (await ctx.startRendering()).getChannelData(0); + let peak = 0, + tail = 0, + energy = 0; + for (let i = 0; i < data.length; i++) { + if (!Number.isFinite(data[i])) throw new Error("Nonfinite audio"); + peak = Math.max(peak, Math.abs(data[i])); + energy += data[i] ** 2; + if (i > 24000) tail = Math.max(tail, Math.abs(data[i])); + } + const magnitude = (f) => { + let re = 0, + im = 0; + const start = 4410, + N = 8820; + for (let i = 0; i < N; i++) { + re += data[start + i] * Math.cos((2 * Math.PI * f * i) / 44100); + im += data[start + i] * Math.sin((2 * Math.PI * f * i) / 44100); + } + return (2 * Math.hypot(re, im)) / N; + }; + if (peak < 0.05 || peak > 0.56 || tail > 1e-6 || ended !== 1) + throw new Error(JSON.stringify({ name, peak, tail, ended })); + const root = magnitude(220), + fifth = magnitude(330); + if ( + name === "ac_triangle_fifth" && + !(root > 0.3 && fifth > 0.08 && fifth < 0.12) + ) + throw new Error("Incorrect triangle/fifth levels"); + results.push({ + name, + peak: +peak.toFixed(4), + rms: +Math.sqrt(energy / data.length).toFixed(4), + root: +root.toFixed(4), + fifth: +fifth.toFixed(4), + ended, + }); + } + const ctx = new OfflineAudioContext(1, 22050, 44100), + sounds = {}; + let ended = 0; + registerNotepat({ + registerSound: (n, f) => (sounds[n] = f), + getAudioContext: () => ctx, + getFrequencyFromValue, + }); + const h = sounds.ac_triangle_fifth( + 0, + { freq: 220, duration: 0.4 }, + () => ended++, + ); + h.node.connect(ctx.destination); + h.stop(0.1); + h.stop(0.1); + const data = (await ctx.startRendering()).getChannelData(0); + if (data.slice(5000).some((x) => Math.abs(x) > 1e-6) || ended !== 1) + throw new Error("Voice stop/cleanup failed"); + for (const name of ['ac_glass', 'ac_swarm', 'ac_bloom', 'ac_orbit', 'ac_marimba', 'ac_stone', 'ac_water', 'ac_kick']) { + const takes = []; + for (const color of [0, 1]) { + const ctx = new OfflineAudioContext(2, 22050, 44100); + const sounds = {}; + let ended = 0; + registerNotepat({ + registerSound: (n, f) => sounds[n] = f, + getAudioContext: () => ctx, + getFrequencyFromValue, + }); + const handle = sounds[name](0, { freq: 220, n: color, duration: 0.4 }, () => ended++); + handle.node.connect(ctx.destination); + handle.stop(0.2); + handle.stop(0.2); + const audio = await ctx.startRendering(); + for (let channel = 0; channel < 2; channel++) { + const data = audio.getChannelData(channel); + if (data.some(x => !Number.isFinite(x) || Math.abs(x) > 0.56) + || data.slice(10000).some(x => Math.abs(x) > 1e-6)) { + throw new Error(name + ': invalid audio or leaking tail'); + } + } + if (ended !== 1) throw new Error(name + ': cleanup did not run exactly once'); + takes.push(audio.getChannelData(0)); + } + const difference = Math.sqrt(takes[0].slice(0, 8820).reduce( + (sum, sample, i) => sum + (sample - takes[1][i]) ** 2, 0, + ) / 8820); + if (difference < 0.005) throw new Error(name + ': timbre macro has no audible-scale effect'); + } + return results; + }); + console.log("Offline audio:", JSON.stringify(render)); + } + console.log( + mode, + JSON.stringify({ state, errors, loadedCode: code.length }), + ); + await page.screenshot({ path: join(output, mode + ".png") }); + await page.getByRole("button", { name: "stop", exact: true }).click(); + await page.close(); + } + } finally { + await browser.close(); + } +})().catch((e) => { + console.error(e); + process.exitCode = 1; +}); diff --git a/ac-strudel/worker.mjs b/ac-strudel/worker.mjs new file mode 100644 index 0000000000..edc6eb848d --- /dev/null +++ b/ac-strudel/worker.mjs @@ -0,0 +1,51 @@ +import synth from './synth.txt'; +import example from './example.txt'; +import paste from './paste.txt'; +import marimbaba from './marimbaba.txt'; +import marimbabaPaste from './marimbaba-paste.txt'; +import marimbabaOrbit from './marimbaba-orbit.txt'; +import marimbabaOrbitPaste from './marimbaba-orbit-paste.txt'; +import stone from './stone.txt'; +import stonePaste from './stone-paste.txt'; + +// Dedicated AC host: no user data, storage, or third-party API calls. +export default { + fetch(request) { + const path = new URL(request.url).pathname; + const headers = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS', + 'Cache-Control': 'no-cache', + 'X-Content-Type-Options': 'nosniff', + }; + if (request.method === 'OPTIONS') return new Response(null, { status: 204, headers }); + if (!['GET', 'HEAD'].includes(request.method)) { + return new Response('Method not allowed', { status: 405, headers: { ...headers, Allow: 'GET, HEAD, OPTIONS' } }); + } + const demos = { '/': example, '/demo': example, '/marimbaba': marimbaba, '/marimbaba-orbit': marimbabaOrbit, '/stone': stone, '/stonwattajetta': stone }; + if (demos[path]) { + // The human shares this short URL; Strudel receives its native source hash. + return new Response(null, { + status: 302, + headers: { ...headers, Location: 'https://strudel.cc/#' + btoa(String.fromCharCode(...new TextEncoder().encode(demos[path]))) }, + }); + } + const files = { + '/stone/source': [stone, 'text/plain; charset=utf-8'], + '/stone/paste': [stonePaste, 'text/plain; charset=utf-8'], + '/s': [synth, 'text/javascript; charset=utf-8'], + '/synth.mjs': [synth, 'text/javascript; charset=utf-8'], + '/paste': [paste, 'text/plain; charset=utf-8'], + '/source': [example, 'text/plain; charset=utf-8'], + '/marimbaba/source': [marimbaba, 'text/plain; charset=utf-8'], + '/marimbaba/paste': [marimbabaPaste, 'text/plain; charset=utf-8'], + '/marimbaba-orbit/source': [marimbabaOrbit, 'text/plain; charset=utf-8'], + '/marimbaba-orbit/paste': [marimbabaOrbitPaste, 'text/plain; charset=utf-8'], + }; + const file = files[path]; + if (!file) return new Response('Not found', { status: 404, headers }); + return new Response(request.method === 'HEAD' ? null : file[0], { + headers: { ...headers, 'Content-Type': file[1] }, + }); + }, +}; diff --git a/lith/DNS.md b/lith/DNS.md index 7661ff7809..5a376b01c5 100644 --- a/lith/DNS.md +++ b/lith/DNS.md @@ -21,6 +21,10 @@ The primary frontend origin is `209.38.133.33`. ## Other Live Exceptions +- `pat.aesthetic.computer` is a Workers custom domain for `ac-strudel-notepat` + (added 2026-10-03). `/` opens the Strudel demo, `/s` serves the synth module, + and `/paste` serves standalone source. Deploy from `ac-strudel/deploy.mjs`; + this host does not route through lith. - `aesthetic.computer` keeps its non-lith service records for PDS, session, silo, oven, spaces/CDN, Auth0, Shopify, Stripe, and Cloudflare Worker endpoints. - `prompt.ac` keeps `*.prompt.ac -> 100::` for the Worker wildcard. - `prompt.ac` zone ruleset "prompt.ac bare-domain auto-redirect" (dynamic