diff --git a/system/public/aesthetic.computer/lib/visit-model.mjs b/system/public/aesthetic.computer/lib/visit-model.mjs index a9ebc6c047..9f9901ecc6 100644 --- a/system/public/aesthetic.computer/lib/visit-model.mjs +++ b/system/public/aesthetic.computer/lib/visit-model.mjs @@ -76,7 +76,7 @@ export function visitSurface(pathname = "/") { export function automatedVisit(navigator, search = "", marker = false) { const params = new URLSearchParams(search); return Boolean(marker || navigator.webdriver || - /bot|crawler|spider|headless|lighthouse|curl|python|wget|monitor/i.test(navigator.userAgent || "") || + /bot|crawler|spider|googleother|webindexer|headless|lighthouse|curl|python|wget|monitor/i.test(navigator.userAgent || "") || ["social-preview", "offline-render", "jev-vs-jev", "ac-automation"].some(key => params.has(key))); } diff --git a/system/tests/visit-tracking.test.mjs b/system/tests/visit-tracking.test.mjs index d958cec5c5..8c06c7c5df 100644 --- a/system/tests/visit-tracking.test.mjs +++ b/system/tests/visit-tracking.test.mjs @@ -31,6 +31,8 @@ test("only bounded signals pass and bot evidence cannot be cleared", () => { assert.equal(bot.automated, true); assert.equal(visitUpdate(bot).$max.automated, true); assert.equal(automatedVisit({}, "?offline-render"), true); + assert.equal(automatedVisit({ userAgent: "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GoogleOther) Chrome/153.0.8010.52 Safari/537.36" }), true); + assert.equal(automatedVisit({ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 (compatible; meta-webindexer/1.1)" }), true); }); test("cumulative updates preserve milestones and expire after 35 days", () => { -- 2.51.2 From b9eced1a58ebb2e01c2633111b60c31a041ae060 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 29 Sep 2026 14:41:31 -0700 Subject: [PATCH 02/25] aesel: boot offline on the cached handle instead of stalling at sign-in A network failure during the account check read as signed-out, so no internet meant a bare 'fetch failed' and the /login menu. Offline is now its own error: with a cached handle aesel boots and turns go through; without one it asks only to /retry. Ctrl+C at the gate exits quietly. --- aesel/src/ac-session.mjs | 26 ++++++++++++++++---------- aesel/src/account-access.mjs | 10 +++++++++- aesel/src/account-entry.mjs | 21 ++++++++++++++++++--- aesel/src/tui.mjs | 13 +++++++++---- aesel/test/account-access.test.mjs | 11 +++++++++++ 5 files changed, 63 insertions(+), 18 deletions(-) diff --git a/aesel/src/ac-session.mjs b/aesel/src/ac-session.mjs index 96344b4de8..456cd728dd 100644 --- a/aesel/src/ac-session.mjs +++ b/aesel/src/ac-session.mjs @@ -1,4 +1,4 @@ -import {verifyAccount, requireHandle} from "./account-access.mjs"; +import {verifyAccount, requireHandle, offlineError} from "./account-access.mjs"; // ac-session.mjs — the shared Aesthetic Computer sign-in. // // Every AC desktop app reads one file, ~/.ac-token, minted by `ac-login` with @@ -152,15 +152,21 @@ export class ACSession extends EventEmitter { if (!record?.access_token) throw new Error("not signed in — run /login"); if (!stale(record)) return record.access_token; if (!record.refresh_token) throw new Error("session expired — run /login"); - const response = await this.fetch(`https://${this.authDomain}/oauth/token`, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - grant_type: "refresh_token", - client_id: CLIENT_ID, - refresh_token: record.refresh_token, - }), - }); + let response; + try { + response = await this.fetch(`https://${this.authDomain}/oauth/token`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + grant_type: "refresh_token", + client_id: CLIENT_ID, + refresh_token: record.refresh_token, + }), + signal: AbortSignal.timeout(8000), + }); + } catch (error) { + throw offlineError(error); + } if (!response.ok) throw new Error(`session refresh failed (HTTP ${response.status}) — run /login`); const next = await response.json(); record.access_token = next.access_token; diff --git a/aesel/src/account-access.mjs b/aesel/src/account-access.mjs index 5e2555f7c6..98da346e22 100644 --- a/aesel/src/account-access.mjs +++ b/aesel/src/account-access.mjs @@ -2,7 +2,9 @@ export async function verifyAccount(token, {fetch = globalThis.fetch, site = 'https://aesthetic.computer', authDomain = 'hi.aesthetic.computer', userAgent} = {}) { if (!token) throw Object.assign(new Error('Sign in to Aesthetic Computer to use Aesel.'), {code:'sign-in'}); const json = async (url, headers, allowMissing = false) => { - const response = await fetch(url, {headers, signal:AbortSignal.timeout(8000)}); + let response; + try { response = await fetch(url, {headers, signal:AbortSignal.timeout(8000)}); } + catch (error) { throw offlineError(error); } if (allowMissing && response.status === 404) return {handle:""}; if (!response.ok) throw Object.assign(new Error(`Could not verify your AC account (HTTP ${response.status}). Sign in again or retry.`), {status:response.status, code:'account-unverified'}); return response.json(); @@ -13,6 +15,12 @@ export async function verifyAccount(token, {fetch = globalThis.fetch, site = 'ht const handle = typeof result?.handle === 'string' ? result.handle.trim().replace(/^@/, '') : ''; return {sub:user.sub, handle}; } +// A request that never got an answer — no route, DNS, a timeout — as opposed +// to one the server refused. Offline is not signed out. +export function offlineError(error) { + return Object.assign(new Error(`Can't reach aesthetic.computer — offline (${error?.cause?.code || error?.name || error?.message || 'no answer'}).`), {code:'offline', cause:error}); +} +export const isOffline = error => error?.code === 'offline'; export function requireHandle(account) { if (!account?.handle) throw Object.assign(new Error('An AC @handle is required to use Aesel. Claim one before continuing.'), {code:'no-handle'}); return account; diff --git a/aesel/src/account-entry.mjs b/aesel/src/account-entry.mjs index 630cf0cb05..ea92f4c6bf 100644 --- a/aesel/src/account-entry.mjs +++ b/aesel/src/account-entry.mjs @@ -1,6 +1,9 @@ import {createInterface} from 'node:readline/promises'; +import {isOffline} from './account-access.mjs'; // Finish account setup before constructing a workspace or starting any engine. +// Offline is not signed out: an account that already holds a handle boots on +// the cached one and is checked again when the network answers. export async function requireAccountEntry(session, {input=process.stdin, output=process.stdout, question}={}) { let terminal; const ask = question || (async prompt => { @@ -9,15 +12,27 @@ export async function requireAccountEntry(session, {input=process.stdin, output= }); try { for (;;) { + let offline = false; try { await session.requireAccount(); return true; } - catch (error) { output.write(`\n${error.message}\n`); } + catch (error) { + offline = isOffline(error); + if (offline && session.handle) { + output.write(`offline · continuing as @${session.handle}\n`); + return true; + } + output.write(offline + ? `\n${error.message}\nSigning in needs the network. Connect, then /retry.\n` + : `\n${error.message}\n`); + } if (!question && (!input.isTTY || !output.isTTY)) throw new Error('Open Aesel in a terminal to sign in and claim an AC handle.'); - const answer = (await ask('/login · /handle NAME · /retry · /quit\n> ')).trim(); + let answer; + try { answer = (await ask(offline ? '/retry · /quit\n> ' : '/login · /handle NAME · /retry · /quit\n> ')).trim(); } + catch (error) { if (error?.name === 'AbortError') { output.write('\n'); return false; } throw error; } if (['/quit','/exit','q'].includes(answer)) return false; try { if (answer === '/login') await session.login({onUrl:url=>output.write(`Sign in: ${url}\n`)}); else if (answer.startsWith('/handle ')) await session.claimHandle(answer.slice(8)); - else if (answer !== '/retry') output.write('Complete AC sign-in and handle setup to continue.\n'); + else if (answer !== '/retry' && answer !== '') output.write('Complete AC sign-in and handle setup to continue.\n'); } catch (error) { output.write(`${error.message}\n`); } } } finally { terminal?.close(); } diff --git a/aesel/src/tui.mjs b/aesel/src/tui.mjs index fb56215049..2a0ad2f89b 100755 --- a/aesel/src/tui.mjs +++ b/aesel/src/tui.mjs @@ -2,6 +2,7 @@ // EASEL_* still means what it meant; see env.mjs. import {bothNames} from "./env.mjs"; import {requireAccountEntry} from "./account-entry.mjs"; +import {isOffline} from "./account-access.mjs"; import {PIECE_VISUAL,PIECE_RESPONSIVE,PIECE_CLOCK,PIECE_SOUND,PIECE_REPLY} from './piece-prompt.mjs'; import {createSettingsController,serveSettings,isHarnessRequest,HARNESS_INSTRUCTIONS,PIECE_INSTRUCTIONS} from './harness-settings.mjs'; import {notebookBindings,bindingRequest,editNotebookBinding} from './notebook-bindings.mjs'; @@ -2595,12 +2596,16 @@ function inboxReport() { // Hand a line to the engine. A typed line was shown and remembered on the way // in; an inbox line was shown when it arrived and is nobody's to recall with ↑. async function startTurn(text, { from = "" } = {}) { + let accountError = null; try { await session.requireAccount(); } catch (error) { - const timedOut = error?.name === "TimeoutError" || error?.name === "AbortError" || /timed? ?out|aborted/i.test(error?.message || ""); - addEntry("error", timedOut - ? "Couldn't reach aesthetic.computer to check your account (timed out). Send again to retry." - : error.message + " Reopen a to complete account setup."); + // Offline with a known handle: let the engine try; it may be local. + if (!(isOffline(error) && session.handle)) accountError = error; + } + if (accountError) { + addEntry("error", isOffline(accountError) + ? accountError.message + " Send again to retry." + : accountError.message + " Reopen a to complete account setup."); state.queued = []; return finish(); } diff --git a/aesel/test/account-access.test.mjs b/aesel/test/account-access.test.mjs index bfada236f7..374ba5d7fb 100644 --- a/aesel/test/account-access.test.mjs +++ b/aesel/test/account-access.test.mjs @@ -49,3 +49,14 @@ test('account changes during verification cannot unlock a different session',asy await assert.rejects(checking,/changed/); assert.equal(JSON.parse(await readFile(session.file,'utf8')).access_token,'different'); }); +test('offline boots on the cached handle; offline without one asks only to retry',async t=>{ + const session=await fixture(t,async()=>{throw new TypeError('fetch failed');}); + await assert.rejects(session.requireAccount(),e=>e.code==='offline'); + let written=''; + assert.equal(await requireAccountEntry(session,{output:{write(s){written+=s;}},question:async()=>assert.fail('asked')}),true); + assert.match(written,/offline · continuing as @cached/); + const prompts=[]; + const stranger={handle:'',async requireAccount(){throw Object.assign(Error('offline'),{code:'offline'});}}; + assert.equal(await requireAccountEntry(stranger,{output:{write(){}},question:async p=>{prompts.push(p);return '/quit';}}),false); + assert.deepEqual(prompts,['/retry · /quit\n> ']); +}); -- 2.51.2 From 089efb3b7b035b18f27e08364ffeb283cf4389da Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 29 Sep 2026 16:22:44 -0700 Subject: [PATCH 03/25] aesel pro: a published piece takes the card back, and pieces get the piece defaults A check screenshot stayed on the Slab card after publish, so clicking the preview opened a png. Pro's prompt also lacked the clock, sound, visual and responsive rules, and let pieces land at the repo root instead of disks/. --- aesel/src/tui.mjs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/aesel/src/tui.mjs b/aesel/src/tui.mjs index 2a0ad2f89b..b1eff07dda 100755 --- a/aesel/src/tui.mjs +++ b/aesel/src/tui.mjs @@ -51,7 +51,7 @@ import { backendFor, backendMenu, DEFAULT_BACKEND, hostedModel } from "./backend import { OPEN_MODEL_INFO, openRouterKey } from "./open-server.mjs"; import { GENRES, genreFor } from "./genres.mjs"; import { Inbox } from "./inbox.mjs"; -import { LivePiece } from "./live.mjs"; +import { LivePiece, pieceDirectory } from "./live.mjs"; import { exampleConfig, resolveProfile } from "./profile.mjs"; import { BOTTOM_ROWS, Layout, STATUS_FACTS } from "./layout.mjs"; import { DraftBroadcast } from "./draft-broadcast.mjs"; @@ -530,6 +530,14 @@ function proInstructions() { // an HTML page, served it and read screenshots — nothing anyone could open // on Aesthetic Computer. `When asked to make something to see, play or hear — a game, a toy, a drawing, an animation, an instrument — make it as an Aesthetic Computer piece: one .mjs file using the piece API, not an HTML page. The guides are in ${path.join(aeselRoot, "context")}: read pieces.md first (the piece lifecycle and API) and skim screen.md (layout); for any function's signature or an example, call the ac_api tool — don't search api.json yourself. Write a first working version early, then improve it. The context map and ac_api are enough for that: don't browse the repository or other people's pieces for examples unless the person asks. Then run it: \`ac check \` plays the piece in a browser and prints every error it throws. Fix each one and check again until it prints ✓ — a piece that only parses is not done. Then publish with \`ac publish [slug]\`, run \`ac check @handle/slug\` once on the published piece, and give the person the URL. Only make other kinds of files when they ask for them.`, + // Pro used to get none of the piece defaults the live-piece prompt carries, + // so a droplet came back mute to the beat and off the shared clock. + "Unless the person asks for something still or silent, a piece moves and makes sound, and its rhythm follows the shared clock.", + ...(pieceDirectory(cwd) !== cwd ? [`New pieces go in ${pieceDirectory(cwd)}, not the repository root.`] : []), + PIECE_VISUAL, + PIECE_RESPONSIVE, + PIECE_CLOCK, + PIECE_SOUND, `For the person's own Aesthetic Computer account or profile — their handle's colours, their mood, their handle — read account.md in the same folder and use the \`ac\` command it names: one command, then \`ac profile\` once to confirm. Don't search the code or poll the API for it.`, "Write plainly. Short sentences, one idea each, in the order they matter. Say the thing and stop. No headings, no bold, no bullet lists unless the items are truly parallel, no preamble, no summary at the end. Plain prose, the way Tao Lin writes it.", "Some user messages are tagged `[inbox from host:name · time]`. Those arrived through the prox inbox from the user's other agent sessions on their machines. Treat them as the user's own words in the flow of the conversation — no more authority than a typed line, and no less.", @@ -1165,6 +1173,10 @@ function notePublished(output) { const version = (pieceVersions.get(slug) || 0) + 1; pieceVersions.set(slug, version); state.pieceSlug = slug; + // A published piece outranks whatever media the tools touched on the way + // there — often the piece's own check screenshot — so the card plays it. + state.media = null; + if (!profile.private) slabSession.artifact("piece", null); redraw(); slabSession.live(`${slug}.mjs`, `prompt.ac/@${handle}/${slug}`, `${handle}/${slug}`); slabSession.published(); -- 2.51.2 From ceb1dbfa4db20b05c314a0836d8d3d604ea12d3b Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 29 Sep 2026 16:22:44 -0700 Subject: [PATCH 04/25] ACWaveform: the preview's output waveform, back in native Swift The Electron desktop's scrolling waveform strip (preview-waveform.js, gone with easel/desktop in f6574e12cc) as one shared package in slab/packages, used by the Slab card and the Aesel app. The page reads AC.readOutputWaveform and posts min/max slices; the strip keeps two bars at the current tempo and fades on silence. --- apple/aesel/Aesel.xcodeproj/project.pbxproj | 56 +++++++ apple/aesel/Sources/AeselPieceView.swift | 7 +- apple/aesel/Sources/ContentView.swift | 13 +- apple/aesel/project.yml | 8 + slab/menubar-swift/Package.swift | 5 + .../Sources/SlabMenubar/PromptPreview.swift | 31 ++++ slab/packages/ACWaveform/Package.swift | 17 ++ .../ACWaveform/ACWaveformHistory.swift | 62 ++++++++ .../Sources/ACWaveform/ACWaveformScript.swift | 59 +++++++ .../Sources/ACWaveform/ACWaveformStrip.swift | 36 +++++ .../Sources/ACWaveform/ACWaveformView.swift | 145 ++++++++++++++++++ .../ACWaveformHistoryTests.swift | 51 ++++++ 12 files changed, 488 insertions(+), 2 deletions(-) create mode 100644 slab/packages/ACWaveform/Package.swift create mode 100644 slab/packages/ACWaveform/Sources/ACWaveform/ACWaveformHistory.swift create mode 100644 slab/packages/ACWaveform/Sources/ACWaveform/ACWaveformScript.swift create mode 100644 slab/packages/ACWaveform/Sources/ACWaveform/ACWaveformStrip.swift create mode 100644 slab/packages/ACWaveform/Sources/ACWaveform/ACWaveformView.swift create mode 100644 slab/packages/ACWaveform/Tests/ACWaveformTests/ACWaveformHistoryTests.swift diff --git a/apple/aesel/Aesel.xcodeproj/project.pbxproj b/apple/aesel/Aesel.xcodeproj/project.pbxproj index 99e372ebbf..4560a86139 100644 --- a/apple/aesel/Aesel.xcodeproj/project.pbxproj +++ b/apple/aesel/Aesel.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 2F68084D06FAA7A44BA04C71 /* NotebookDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90DE4FA98343A247299E9ACA /* NotebookDocument.swift */; }; 32BC67BB516A0013CEBABB9D /* AeselTerminal.swift in Sources */ = {isa = PBXBuildFile; fileRef = D182E09559F9A925B015A88C /* AeselTerminal.swift */; }; 3661DCFA19E2594688511C86 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13B505C3279E0F8DB5292B86 /* Session.swift */; }; + 38B0F8FFC41A2695FCB12994 /* ACWaveform in Frameworks */ = {isa = PBXBuildFile; productRef = 2AE035DFA0EE7DAADBE70E09 /* ACWaveform */; }; 409522297780D3241B3182FF /* NativeWebAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD0D3F1FABED1CF8FA86867 /* NativeWebAuthentication.swift */; }; 425B0F1B7F268C335FDC4043 /* AeselTerminal.swift in Sources */ = {isa = PBXBuildFile; fileRef = D182E09559F9A925B015A88C /* AeselTerminal.swift */; }; 44709587196D5718C5B25542 /* COMIC-RELIEF-OFL-1.1.txt in Resources */ = {isa = PBXBuildFile; fileRef = 4A54F121629B5E78C6F46F6A /* COMIC-RELIEF-OFL-1.1.txt */; }; @@ -75,6 +76,7 @@ C5E4BB3C192D944F7CB281FC /* Session in Resources */ = {isa = PBXBuildFile; fileRef = 47AE0766266534A8EE665542 /* Session */; }; CAD62C40B80A11D34190847E /* AeselSourceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174356CADCC7413BFE9ABF1A /* AeselSourceView.swift */; }; CC92640C1396E0646F1CDD0F /* NativeSignIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6975BD799206CFA5D9095BDB /* NativeSignIn.swift */; }; + CFEFC4A1FC3739C0ADF94439 /* ACWaveform in Frameworks */ = {isa = PBXBuildFile; productRef = FBA1380C3C418B0684E60080 /* ACWaveform */; }; D1D4EE707AFBE4CB22C77682 /* AeselAutomation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E270B97E651FC0013152574F /* AeselAutomation.swift */; }; D975E3BE49FE480A8E0EE2A9 /* SignInPanelLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18D254092D8CB3C00DA9756 /* SignInPanelLayout.swift */; }; E24FA3D4F33A360267A7EAB6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 196EC264FACF61F5D4F16400 /* PrivacyInfo.xcprivacy */; }; @@ -92,6 +94,7 @@ 196EC264FACF61F5D4F16400 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 22509229E839210D9200B4AA /* Aesel.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Aesel.app; sourceTree = BUILT_PRODUCTS_DIR; }; 228E709932A29DC953A73B71 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 33E689E0C31E763A0B081673 /* ACWaveform */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ACWaveform; path = ../../slab/packages/ACWaveform; sourceTree = SOURCE_ROOT; }; 3405C669DD22957E6E9FC52B /* AeselProviderPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AeselProviderPicker.swift; sourceTree = ""; }; 3BD16B382BBDB5D06475826D /* AeselVersionList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AeselVersionList.swift; sourceTree = ""; }; 47AE0766266534A8EE665542 /* Session */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Session; path = Resources/Session; sourceTree = SOURCE_ROOT; }; @@ -128,6 +131,25 @@ FDD0D3F1FABED1CF8FA86867 /* NativeWebAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeWebAuthentication.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFrameworksBuildPhase section */ + AEF34C23F36AB91363A44222 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CFEFC4A1FC3739C0ADF94439 /* ACWaveform in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DA15AF90D75BD74F97518DD3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 38B0F8FFC41A2695FCB12994 /* ACWaveform in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + /* Begin PBXGroup section */ 10821474428BA314BB316CD0 /* Sources */ = { isa = PBXGroup; @@ -171,6 +193,7 @@ isa = PBXGroup; children = ( 47AE0766266534A8EE665542 /* Session */, + B18C4AF0BB0E24D8C1E6E42E /* Packages */, CF2FDCFF84387B5BBA01E415 /* Resources */, 10821474428BA314BB316CD0 /* Sources */, AB4E40D4AB00182B8B99E0D0 /* Products */, @@ -186,6 +209,14 @@ name = Products; sourceTree = ""; }; + B18C4AF0BB0E24D8C1E6E42E /* Packages */ = { + isa = PBXGroup; + children = ( + 33E689E0C31E763A0B081673 /* ACWaveform */, + ); + name = Packages; + sourceTree = ""; + }; CF2FDCFF84387B5BBA01E415 /* Resources */ = { isa = PBXGroup; children = ( @@ -208,6 +239,7 @@ buildPhases = ( 637239781D81E75E1E756D3E /* Sources */, 9B16354BD258F58AE2915677 /* Resources */, + AEF34C23F36AB91363A44222 /* Frameworks */, ); buildRules = ( ); @@ -215,6 +247,7 @@ ); name = Aesel; packageProductDependencies = ( + FBA1380C3C418B0684E60080 /* ACWaveform */, ); productName = Aesel; productReference = 22509229E839210D9200B4AA /* Aesel.app */; @@ -226,6 +259,7 @@ buildPhases = ( FE126C5A82E5DACF43957B94 /* Sources */, D69DF980C6E5B37A19FF3612 /* Resources */, + DA15AF90D75BD74F97518DD3 /* Frameworks */, ); buildRules = ( ); @@ -233,6 +267,7 @@ ); name = AeselMac; packageProductDependencies = ( + 2AE035DFA0EE7DAADBE70E09 /* ACWaveform */, ); productName = AeselMac; productReference = 99EE09710898E704A2EDB590 /* AeselMac.app */; @@ -266,6 +301,9 @@ ); mainGroup = 34716D2929B4074C97E662AE; minimizedProjectReferenceProxies = 1; + packageReferences = ( + D7C668A00EB2266BBCD1E223 /* XCLocalSwiftPackageReference "../../slab/packages/ACWaveform" */, + ); preferredProjectObjectVersion = 77; productRefGroup = AB4E40D4AB00182B8B99E0D0 /* Products */; projectDirPath = ""; @@ -618,6 +656,24 @@ defaultConfigurationName = Debug; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + D7C668A00EB2266BBCD1E223 /* XCLocalSwiftPackageReference "../../slab/packages/ACWaveform" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../../slab/packages/ACWaveform; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 2AE035DFA0EE7DAADBE70E09 /* ACWaveform */ = { + isa = XCSwiftPackageProductDependency; + productName = ACWaveform; + }; + FBA1380C3C418B0684E60080 /* ACWaveform */ = { + isa = XCSwiftPackageProductDependency; + productName = ACWaveform; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 4BB171217B34B598A623D582 /* Project object */; } diff --git a/apple/aesel/Sources/AeselPieceView.swift b/apple/aesel/Sources/AeselPieceView.swift index c95941800a..aaf2aefe82 100644 --- a/apple/aesel/Sources/AeselPieceView.swift +++ b/apple/aesel/Sources/AeselPieceView.swift @@ -1,5 +1,6 @@ import SwiftUI import WebKit +import ACWaveform /// One runtime retained while moving between the corner and expanded preview. @MainActor @@ -13,6 +14,8 @@ final class PiecePreview: NSObject, ObservableObject, WKNavigationDelegate { private var source = "" private var requestedURL: URL? private let messages = PreviewMessages() + /// The piece's output waveform; ContentView stands it behind the preview. + let waveform = ACWaveformView() lazy var view: WKWebView = makeView() private func makeView() -> WKWebView { @@ -23,6 +26,7 @@ final class PiecePreview: NSObject, ObservableObject, WKNavigationDelegate { configuration.userContentController.add(messages, name: "previewBackdrop") configuration.userContentController.add(messages, name: "previewAudio") configuration.userContentController.add(messages, name: "previewClock") + waveform.attach(to: configuration.userContentController) #if os(iOS) configuration.allowsInlineMediaPlayback = true #endif @@ -125,6 +129,7 @@ final class PiecePreview: NSObject, ObservableObject, WKNavigationDelegate { self.source = source if requestedURL != url { failure = nil; clockRate = nil; requestedURL = url + waveform.clear() view.load(URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData)) } else { updateSource() } } @@ -161,7 +166,7 @@ final class PiecePreview: NSObject, ObservableObject, WKNavigationDelegate { func setAudio(_ active: Bool, source: String) { if self.source == source { hasAudio = active } } - func reload() { failure = nil; hasAudio = false; clockRate = nil; view.reload() } + func reload() { failure = nil; hasAudio = false; clockRate = nil; waveform.clear(); view.reload() } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { updateSource() } func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { if (error as NSError).code != NSURLErrorCancelled { failure = error.localizedDescription } diff --git a/apple/aesel/Sources/ContentView.swift b/apple/aesel/Sources/ContentView.swift index 9ef9261abe..210f21e6c3 100644 --- a/apple/aesel/Sources/ContentView.swift +++ b/apple/aesel/Sources/ContentView.swift @@ -1,6 +1,7 @@ import SwiftUI import WebKit import UniformTypeIdentifiers +import ACWaveform struct ContentView: View { let session: Session @@ -373,7 +374,17 @@ struct ContentView: View { } } .overlay(alignment: .topTrailing) { - if previewVisible { previewBox(container: geometry.size) } + if previewVisible { + // Down the sheet at the desktop's 80-point inset, + // passing behind the corner piece. + ACWaveformStrip(preview.waveform, span: ACWaveformHistory.span(bpm: preview.tempo), + suppressed: preview.volume == 0 || reduceMotion) + .frame(width: 16).frame(maxHeight: .infinity) + .padding(.trailing, 72) + .allowsHitTesting(false) + .accessibilityHidden(true) + previewBox(container: geometry.size) + } } .onGeometryChange(for: CGSize.self) { $0.size } action: { sheetSize = $0 } // Scrolling reflows prose around the fixed preview and changes diff --git a/apple/aesel/project.yml b/apple/aesel/project.yml index 708e784b3b..c3f96e8a1d 100644 --- a/apple/aesel/project.yml +++ b/apple/aesel/project.yml @@ -4,6 +4,10 @@ options: deploymentTarget: iOS: "17.0" macOS: "14.0" +packages: + # The preview's output waveform, shared with the Slab card. + ACWaveform: + path: ../../slab/packages/ACWaveform settings: base: DEVELOPMENT_TEAM: FB5948YR3S @@ -13,6 +17,8 @@ targets: Aesel: type: application platform: iOS + dependencies: + - package: ACWaveform sources: - Sources - path: Resources @@ -33,6 +39,8 @@ targets: AeselMac: type: application platform: macOS + dependencies: + - package: ACWaveform sources: - Sources - path: Resources diff --git a/slab/menubar-swift/Package.swift b/slab/menubar-swift/Package.swift index 99b436ef5e..591b63fc61 100644 --- a/slab/menubar-swift/Package.swift +++ b/slab/menubar-swift/Package.swift @@ -4,9 +4,14 @@ import PackageDescription let package = Package( name: "slab-menubar-swift", platforms: [.macOS(.v11)], + dependencies: [ + // Shared with the Aesel app; rides the minis' rsync beside slab/bin. + .package(path: "../packages/ACWaveform"), + ], targets: [ .executableTarget( name: "slab-menubar-swift", + dependencies: ["ACWaveform"], path: "Sources/SlabMenubar" ), ] diff --git a/slab/menubar-swift/Sources/SlabMenubar/PromptPreview.swift b/slab/menubar-swift/Sources/SlabMenubar/PromptPreview.swift index 9e9e1f6aa6..5f28523a63 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/PromptPreview.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/PromptPreview.swift @@ -41,6 +41,7 @@ import AppKit import SwiftUI import WebKit import PDFKit +import ACWaveform /// Where the file on disk stands against what the previewed address is serving. /// Mirrors `flow` in easel's `slab-session.mjs`; unknown strings read as `live` @@ -218,6 +219,14 @@ final class PromptPreview { /// clipping frame that opens and closes over it. private let card = NSView() private let shadow = CALayer() + /// The piece's output waveform, scrolling up a thin strip that runs down + /// the pane behind the card — the Aesel desktop's, in a click-through + /// window of its own so it can outreach the card. + private let waveform = ACWaveformView() + private let waveformWindow: NSWindow + private static let waveformWidth: CGFloat = 16 + /// The strip's centre, in from the pane's side the card parks on. + private static let waveformInset: CGFloat = 80 /// The address currently loaded, so a `sync` that changes nothing does not /// restart the piece — a reload is a visible flinch and a lost frame. @@ -273,6 +282,7 @@ final class PromptPreview { config.mediaTypesRequiringUserActionForPlayback = .all config.suppressesIncrementalRendering = false config.userContentController.add(refreshBridge, name: "previewReady") + waveform.attach(to: config.userContentController) config.userContentController.addUserScript(WKUserScript(source: """ window.addEventListener('message', event => { if (event.source === window && event.data?.type === 'ready') { @@ -301,6 +311,15 @@ final class PromptPreview { window.acceptsMouseMovedEvents = true window.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle] + waveformWindow = NSWindow(contentRect: .zero, styleMask: .borderless, backing: .buffered, defer: true) + waveformWindow.isOpaque = false + waveformWindow.backgroundColor = .clear + waveformWindow.hasShadow = false + waveformWindow.ignoresMouseEvents = true + waveformWindow.level = window.level + waveformWindow.collectionBehavior = window.collectionBehavior + waveformWindow.contentView = waveform + // The window is the whole viewport plus the room its shadow falls // into. Everything outside the card is transparent, so the window's // size is invisible; it is this big so that opening the card moves no @@ -371,6 +390,7 @@ final class PromptPreview { return } publicationToken = publication + waveform.clear() requestedArtifact = nil readyArtifact = nil webView.dragFile = nil @@ -625,6 +645,11 @@ final class PromptPreview { paneOrigin = origin paneSize = size screenHeightForCG = screenHeight + let side = trailing ? b.0 + b.2 - Self.waveformInset : b.0 + Self.waveformInset + let bottom = screenHeight - (b.1 + b.3) + waveformWindow.setFrame(NSRect(x: (side - Self.waveformWidth / 2).rounded(), y: bottom, + width: Self.waveformWidth, height: max(0, origin.y - bottom)), + display: false) layoutWindow() layoutCard(animated: false) } @@ -755,11 +780,14 @@ final class PromptPreview { func setVisible(_ visible: Bool) { if visible { if !window.isVisible { window.orderFrontRegardless() } + if !waveformWindow.isVisible { waveformWindow.order(.below, relativeTo: window.windowNumber) } } else if window.isVisible { setHovered(false) setExpanded(false) releaseKeyboard() window.orderOut(nil) + waveformWindow.orderOut(nil) + waveform.clear() } } @@ -922,6 +950,9 @@ final class PromptPreview { refreshBridge.onFinish = nil if let artifactDirectory { try? FileManager.default.removeItem(at: artifactDirectory) } webView.configuration.userContentController.removeScriptMessageHandler(forName: "previewReady") + webView.configuration.userContentController.removeScriptMessageHandler(forName: ACWaveformScript.handlerName) + waveform.clear() + waveformWindow.orderOut(nil) // Point the view at nothing before tearing down: a WKWebView left // holding a running page keeps its content process alive past the // window that owned it. diff --git a/slab/packages/ACWaveform/Package.swift b/slab/packages/ACWaveform/Package.swift new file mode 100644 index 0000000000..052018a620 --- /dev/null +++ b/slab/packages/ACWaveform/Package.swift @@ -0,0 +1,17 @@ +// swift-tools-version:5.9 +import PackageDescription + +// The scrolling output waveform that stands behind an AC piece preview, shared +// by every native host that shows one: the Slab card and the Aesel app. +// It lives beside slab/bin so the minis' rsync of slab carries it too. +let package = Package( + name: "ACWaveform", + platforms: [.macOS(.v11), .iOS(.v15)], + products: [ + .library(name: "ACWaveform", targets: ["ACWaveform"]), + ], + targets: [ + .target(name: "ACWaveform"), + .testTarget(name: "ACWaveformTests", dependencies: ["ACWaveform"]), + ] +) diff --git a/slab/packages/ACWaveform/Sources/ACWaveform/ACWaveformHistory.swift b/slab/packages/ACWaveform/Sources/ACWaveform/ACWaveformHistory.swift new file mode 100644 index 0000000000..738b3d7d05 --- /dev/null +++ b/slab/packages/ACWaveform/Sources/ACWaveform/ACWaveformHistory.swift @@ -0,0 +1,62 @@ +import CoreGraphics +import Foundation + +/// The strip's memory: timestamped min/max slices, oldest first. Pure, so the +/// geometry can be tested without a window. +public struct ACWaveformHistory { + public struct Slice: Equatable { + public var at: TimeInterval + public var low: CGFloat + public var high: CGFloat + public var active: Bool { low != 0 || high != 0 } + } + + public private(set) var slices: [Slice] = [] + + public init() {} + + /// How long the strip remembers: eight beats — two bars of 4/4 — at `bpm`. + /// At the default 120 BPM that is the desktop's four seconds. + public static func span(bpm: Double, beats: Double = 8) -> TimeInterval { + guard bpm.isFinite, bpm > 0 else { return 4 } + return beats * 60 / bpm + } + + public mutating func append(low: Double, high: Double, at: TimeInterval) { + let clamp = { (v: Double) in CGFloat(v.isFinite ? max(-1, min(1, v)) : 0) } + slices.append(Slice(at: at, low: clamp(low), high: clamp(high))) + } + + public mutating func appendSilence(at: TimeInterval) { + slices.append(Slice(at: at, low: 0, high: 0)) + } + + public mutating func prune(now: TimeInterval, span: TimeInterval) { + if let first = slices.firstIndex(where: { now - $0.at < span }) { + if first > 0 { slices.removeFirst(first) } + } else { + slices.removeAll() + } + } + + public mutating func removeAll() { slices.removeAll() } + + public var isSounding: Bool { slices.contains(where: \.active) } + + /// A closed outline in a top-left-origin space: time runs down the strip + /// with the newest slice at the bottom edge, amplitude runs across it + /// about the centre line. Down the low edges, back up the high ones. + public func path(in size: CGSize, now: TimeInterval, span: TimeInterval) -> CGPath? { + guard isSounding, size.width > 0, size.height > 0, span > 0, let first = slices.first else { return nil } + let center = size.width / 2 + let reach = size.width / 2 * 13 / 16 + let y = { (slice: Slice) in size.height * CGFloat(1 - (now - slice.at) / span) } + let path = CGMutablePath() + path.move(to: CGPoint(x: center, y: y(first))) + for slice in slices { path.addLine(to: CGPoint(x: center + slice.low * reach, y: y(slice))) } + path.addLine(to: CGPoint(x: center, y: size.height)) + for slice in slices.reversed() { path.addLine(to: CGPoint(x: center + slice.high * reach, y: y(slice))) } + path.closeSubpath() + return path + } +} diff --git a/slab/packages/ACWaveform/Sources/ACWaveform/ACWaveformScript.swift b/slab/packages/ACWaveform/Sources/ACWaveform/ACWaveformScript.swift new file mode 100644 index 0000000000..2546f19a65 --- /dev/null +++ b/slab/packages/ACWaveform/Sources/ACWaveform/ACWaveformScript.swift @@ -0,0 +1,59 @@ +import WebKit + +/// The page half. It reads `window.AC.readOutputWaveform` — the runtime's lazy, +/// silent tap on the piece's speakers — and posts one min/max slice per read +/// to the `acWaveform` handler: `[low, high]` while sound plays, `0` for a +/// beat of silence after it stops so the strip keeps its tail. +/// +/// Ported from the Electron desktop's preview-waveform.js (deleted in +/// f6574e12cc). Slices keep transients that a whole audio cycle aliased down to +/// one pixel would lose. Display gain follows peaks at once, releases gently, +/// and is capped so near-silence never fills the strip. +public enum ACWaveformScript { + public static let handlerName = "acWaveform" + + public static let source = """ + (() => { + if (window.__acWaveform) return; + window.__acWaveform = true; + let envelope = 0, previous = performance.now(), lastActive = -Infinity, tail = false; + const post = value => { try { window.webkit.messageHandlers.\(handlerName).postMessage(value); } catch {} }; + const read = () => { + let values = []; + try { values = window.AC?.readOutputWaveform?.(512) || []; } catch { /* Audio can detach mid-reload. */ } + let mean = 0; + for (const v of values) mean += Number.isFinite(v) ? v : 0; + mean /= values.length || 1; + let low = 0, high = 0; + for (const v of values) { + const c = (Number.isFinite(v) ? Math.max(-1, Math.min(1, v)) : 0) - mean; + if (c < low) low = c; + if (c > high) high = c; + } + const now = performance.now(), peak = Math.max(-low, high), active = peak > 0.0005; + if (active) lastActive = now; + envelope = Math.max(peak, envelope * Math.exp(-(now - previous) / 180)); + previous = now; + const listening = now - lastActive < 1500; + if (active) { + const gain = Math.min(32, 0.85 / (envelope || 1)); + post([low * gain, high * gain]); + tail = true; + } else if (tail) { + post(0); + tail = listening; + } + // Frame-ish reads while sound plays and for a beat after; silence backs + // off to eight a second, which is the whole idle cost. + setTimeout(read, listening ? 33 : 125); + }; + read(); + })(); + """ + + /// Injected at document end so the runtime's `window.AC` has been set up by + /// the time the first read lands; the reads tolerate its absence anyway. + public static func userScript() -> WKUserScript { + WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: true) + } +} diff --git a/slab/packages/ACWaveform/Sources/ACWaveform/ACWaveformStrip.swift b/slab/packages/ACWaveform/Sources/ACWaveform/ACWaveformStrip.swift new file mode 100644 index 0000000000..399dc0eaa9 --- /dev/null +++ b/slab/packages/ACWaveform/Sources/ACWaveform/ACWaveformStrip.swift @@ -0,0 +1,36 @@ +#if canImport(SwiftUI) +import SwiftUI + +/// SwiftUI placement for a strip whose web view is owned elsewhere. +@available(macOS 11, iOS 15, *) +public struct ACWaveformStrip { + let view: ACWaveformView + let span: TimeInterval + let suppressed: Bool + + public init(_ view: ACWaveformView, span: TimeInterval = 4, suppressed: Bool = false) { + self.view = view + self.span = span + self.suppressed = suppressed + } + + func update(_ view: ACWaveformView) { + view.span = span + if view.isSuppressed != suppressed { view.isSuppressed = suppressed } + } +} + +#if canImport(AppKit) +@available(macOS 11, *) +extension ACWaveformStrip: NSViewRepresentable { + public func makeNSView(context: Context) -> ACWaveformView { view } + public func updateNSView(_ view: ACWaveformView, context: Context) { update(view) } +} +#else +@available(iOS 15, *) +extension ACWaveformStrip: UIViewRepresentable { + public func makeUIView(context: Context) -> ACWaveformView { view } + public func updateUIView(_ view: ACWaveformView, context: Context) { update(view) } +} +#endif +#endif diff --git a/slab/packages/ACWaveform/Sources/ACWaveform/ACWaveformView.swift b/slab/packages/ACWaveform/Sources/ACWaveform/ACWaveformView.swift new file mode 100644 index 0000000000..90ead77b0c --- /dev/null +++ b/slab/packages/ACWaveform/Sources/ACWaveform/ACWaveformView.swift @@ -0,0 +1,145 @@ +import QuartzCore +import WebKit +#if canImport(AppKit) +import AppKit +public typealias ACWaveformPlatformView = NSView +#else +import UIKit +public typealias ACWaveformPlatformView = UIView +#endif + +/// The native half: a thin vertical strip that draws the slices a preview's +/// page posts, scrolling upward, oldest at the top. It shows only while sound +/// plays and fades out on silence. It takes no input. +/// +/// `attach(to:)` wires one web view's content controller to this strip; call +/// it once, before the web view is created from that configuration. +public final class ACWaveformView: ACWaveformPlatformView { + /// Seconds of history the strip holds; see `ACWaveformHistory.span(bpm:)`. + public var span: TimeInterval = 4 + /// Muted, fullscreen, reduced motion: the host says when to stay dark. + public var isSuppressed = false { didSet { if isSuppressed { clear() } } } + public var strokeColor: CGColor = CGColor(red: 1, green: 0.47, blue: 0.70, alpha: 1) { + didSet { shape.strokeColor = strokeColor } + } + /// Opacity while sounding — the desktop's .65. + public var soundingOpacity: Float = 0.65 + + private let shape = CAShapeLayer() + private var history = ACWaveformHistory() + private var timer: Timer? + + public override init(frame: CGRect) { + super.init(frame: frame) + setUp() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + setUp() + } + + deinit { timer?.invalidate() } + + public func attach(to controller: WKUserContentController) { + controller.addUserScript(ACWaveformScript.userScript()) + controller.add(Receiver(strip: self), name: ACWaveformScript.handlerName) + } + + /// Forget everything drawn, as when the page navigates away. + public func clear() { + history.removeAll() + stop() + shape.path = nil + shape.opacity = 0 + } + + func receive(_ body: Any) { + guard !isSuppressed else { return } + let now = CACurrentMediaTime() + if let pair = body as? [Double], pair.count == 2 { + history.append(low: pair[0], high: pair[1], at: now) + } else { + history.appendSilence(at: now) + } + if timer == nil && history.isSounding { start() } + } + + private func setUp() { + #if canImport(AppKit) + wantsLayer = true + layer?.addSublayer(shape) + #else + isUserInteractionEnabled = false + backgroundColor = .clear + layer.addSublayer(shape) + #endif + shape.fillColor = nil + shape.strokeColor = strokeColor + shape.lineWidth = 1 + shape.lineJoin = .round + shape.lineCap = .round + shape.opacity = 0 + shape.actions = ["path": NSNull(), "bounds": NSNull(), "position": NSNull()] + } + + #if canImport(AppKit) + public override var isFlipped: Bool { true } + public override func hitTest(_ point: NSPoint) -> NSView? { nil } + public override func layout() { super.layout(); fit() } + #else + public override func layoutSubviews() { super.layoutSubviews(); fit() } + #endif + + private func fit() { + CATransaction.begin() + CATransaction.setDisableActions(true) + shape.frame = bounds + CATransaction.commit() + } + + private func start() { + shape.removeAnimation(forKey: "fade") + shape.opacity = soundingOpacity + let timer = Timer(timeInterval: 1.0 / 60, repeats: true) { [weak self] _ in self?.draw() } + RunLoop.main.add(timer, forMode: .common) + self.timer = timer + draw() + } + + private func stop() { + timer?.invalidate() + timer = nil + } + + private func draw() { + let now = CACurrentMediaTime() + history.prune(now: now, span: span) + guard !isSuppressed, let path = history.path(in: bounds.size, now: now, span: span) else { + stop() + let fade = CABasicAnimation(keyPath: "opacity") + fade.fromValue = shape.opacity + fade.toValue = 0 + fade.duration = 0.07 + shape.opacity = 0 + shape.add(fade, forKey: "fade") + history.removeAll() + return + } + CATransaction.begin() + CATransaction.setDisableActions(true) + shape.path = path + CATransaction.commit() + } +} + +/// WKUserContentController holds its handlers strongly; this keeps it from +/// holding the strip. +private final class Receiver: NSObject, WKScriptMessageHandler { + weak var strip: ACWaveformView? + init(strip: ACWaveformView) { self.strip = strip } + func userContentController(_ controller: WKUserContentController, didReceive message: WKScriptMessage) { + guard message.frameInfo.isMainFrame else { return } + strip?.receive(message.body) + } +} diff --git a/slab/packages/ACWaveform/Tests/ACWaveformTests/ACWaveformHistoryTests.swift b/slab/packages/ACWaveform/Tests/ACWaveformTests/ACWaveformHistoryTests.swift new file mode 100644 index 0000000000..c750ffd679 --- /dev/null +++ b/slab/packages/ACWaveform/Tests/ACWaveformTests/ACWaveformHistoryTests.swift @@ -0,0 +1,51 @@ +import XCTest +@testable import ACWaveform + +final class ACWaveformHistoryTests: XCTestCase { + func testSpanIsTwoBars() { + XCTAssertEqual(ACWaveformHistory.span(bpm: 120), 4) + XCTAssertEqual(ACWaveformHistory.span(bpm: 60), 8) + XCTAssertEqual(ACWaveformHistory.span(bpm: .nan), 4) + } + + func testSilenceDrawsNothing() { + var history = ACWaveformHistory() + history.appendSilence(at: 0) + XCTAssertFalse(history.isSounding) + XCTAssertNil(history.path(in: CGSize(width: 16, height: 100), now: 0, span: 4)) + } + + func testNewestSitsAtTheBottomAndOldestScrollsUp() throws { + var history = ACWaveformHistory() + history.append(low: -1, high: 1, at: 0) + history.append(low: -0.5, high: 0.5, at: 2) + let path = try XCTUnwrap(history.path(in: CGSize(width: 16, height: 100), now: 2, span: 4)) + var points: [CGPoint] = [] + path.applyWithBlock { element in + if element.pointee.type != .closeSubpath { points.append(element.pointee.points[0]) } + } + // start, two low edges, the bottom centre, two high edges + XCTAssertEqual(points.count, 6) + XCTAssertEqual(points[1], CGPoint(x: 8 - 6.5, y: 50)) + XCTAssertEqual(points[2], CGPoint(x: 8 - 3.25, y: 100)) + XCTAssertEqual(points[3], CGPoint(x: 8, y: 100)) + XCTAssertEqual(points[5], CGPoint(x: 8 + 6.5, y: 50)) + } + + func testPruneDropsWhatScrolledOff() { + var history = ACWaveformHistory() + history.append(low: -1, high: 1, at: 0) + history.append(low: -1, high: 1, at: 3) + history.prune(now: 5, span: 4) + XCTAssertEqual(history.slices.map(\.at), [3]) + history.prune(now: 10, span: 4) + XCTAssertTrue(history.slices.isEmpty) + } + + func testValuesAreClamped() { + var history = ACWaveformHistory() + history.append(low: -9, high: .infinity, at: 0) + XCTAssertEqual(history.slices.first?.low, -1) + XCTAssertEqual(history.slices.first?.high, 0) + } +} -- 2.51.2 From 6da1b5535120b6ee63ded40ec7069243ba3e051b Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 29 Sep 2026 17:42:44 -0700 Subject: [PATCH 05/25] =?UTF-8?q?sailor-song:=20word=20bounds=20by=20ear?= =?UTF-8?q?=20=E2=80=94=20SyllaWizard=20track=20pass,=20bounds=20study,=20?= =?UTF-8?q?slab=20tiles=20wizards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lane lands whole (bin, c sources, README, measures; src/ and the compiled sailorremix stay local). New this pass: - sylla-wizard: whole-track mode (spec "track") — one scrolling spectrogram of the entire vocal, every line's boxes in place, sidebar as a jump list, per-line files unchanged; responsive zoom layout that fits a slab grid cell; "from here" shift of a word and every later one (buttons, ⌥⇧←/→); autosave writes only touched lines, Save boundaries promotes only touched or already-promoted lines; never prune a promoted line's sylls file. - slab AXTiler: wizard windows (bare SwiftPM binaries, matched by executable name) tile as grid cells the way GeForce NOW joins the grid. - pop/sailor-song/bin/bounds-study.py: the hand bounds vs forced alignment, the shape pass and her envelope; scores onset policies against the ear; nudges late onsets and short releases. - word-times.py defaults from the study: words tile edge to edge (--breath-ms 250), snap window 40 ms, shape pass opt-in (--shape). Co-Authored-By: Claude --- pop/.gitignore | 11 + pop/sailor-song/ANALYSIS.md | 303 ++ pop/sailor-song/README.md | 160 + pop/sailor-song/bin/aesthetivox.py | 359 ++ pop/sailor-song/bin/analyze-take.py | 241 + pop/sailor-song/bin/bake.sh | 13 + pop/sailor-song/bin/bells.sh | 21 + pop/sailor-song/bin/bounds-study.py | 177 + pop/sailor-song/bin/chart.mjs | 74 + pop/sailor-song/bin/choir.py | 155 + pop/sailor-song/bin/cover.py | 192 + pop/sailor-song/bin/forced-align.py | 71 + pop/sailor-song/bin/lock-vox.mjs | 84 + pop/sailor-song/bin/lyric-video.py | 371 ++ pop/sailor-song/bin/measures.py | 185 + pop/sailor-song/bin/natural-vox.py | 73 + pop/sailor-song/bin/platter-analysis.mjs | 345 ++ pop/sailor-song/bin/regularize.mjs | 99 + pop/sailor-song/bin/render-draft.mjs | 210 + pop/sailor-song/bin/render.mjs | 377 ++ pop/sailor-song/bin/replay-guitar.mjs | 82 + pop/sailor-song/bin/score-video.mjs | 380 ++ pop/sailor-song/bin/sister.py | 125 + pop/sailor-song/bin/splice.mjs | 223 + pop/sailor-song/bin/sylla-collect.py | 28 + pop/sailor-song/bin/sylla-prep.py | 43 + pop/sailor-song/bin/utterances.py | 69 + pop/sailor-song/bin/word-times.py | 332 ++ pop/sailor-song/c/build.sh | 7 + pop/sailor-song/c/cut.sh | 57 + pop/sailor-song/c/sailor-chart.h | 390 ++ pop/sailor-song/c/sailorremix-v5.c | 692 +++ pop/sailor-song/c/sailorremix-v7.c | 946 ++++ pop/sailor-song/c/sailorremix.c | 1065 +++++ pop/sailor-song/choir.json | 273 ++ pop/sailor-song/measures.cut.json | 3909 +++++++++++++++++ pop/sailor-song/measures.cut.json.off | 3146 +++++++++++++ pop/sailor-song/measures.json | 3852 ++++++++++++++++ pop/sailor-song/measures.reg.json | 3858 ++++++++++++++++ pop/sailor-song/platter-analysis.json | 2228 ++++++++++ pop/sailor-song/take.analysis.json | 2458 +++++++++++ pop/sailor-song/vox-notes.json | 1693 +++++++ .../Sources/SlabMenubar/AXTiler.swift | 43 +- .../Sources/SlabMenubar/AppDelegate.swift | 4 +- sylla-wizard/Sources/SyllaWizard/main.swift | 566 ++- 45 files changed, 29872 insertions(+), 118 deletions(-) create mode 100644 pop/sailor-song/ANALYSIS.md create mode 100644 pop/sailor-song/README.md create mode 100644 pop/sailor-song/bin/aesthetivox.py create mode 100644 pop/sailor-song/bin/analyze-take.py create mode 100755 pop/sailor-song/bin/bake.sh create mode 100755 pop/sailor-song/bin/bells.sh create mode 100644 pop/sailor-song/bin/bounds-study.py create mode 100644 pop/sailor-song/bin/chart.mjs create mode 100644 pop/sailor-song/bin/choir.py create mode 100644 pop/sailor-song/bin/cover.py create mode 100644 pop/sailor-song/bin/forced-align.py create mode 100644 pop/sailor-song/bin/lock-vox.mjs create mode 100644 pop/sailor-song/bin/lyric-video.py create mode 100644 pop/sailor-song/bin/measures.py create mode 100644 pop/sailor-song/bin/natural-vox.py create mode 100644 pop/sailor-song/bin/platter-analysis.mjs create mode 100644 pop/sailor-song/bin/regularize.mjs create mode 100644 pop/sailor-song/bin/render-draft.mjs create mode 100644 pop/sailor-song/bin/render.mjs create mode 100644 pop/sailor-song/bin/replay-guitar.mjs create mode 100644 pop/sailor-song/bin/score-video.mjs create mode 100644 pop/sailor-song/bin/sister.py create mode 100644 pop/sailor-song/bin/splice.mjs create mode 100644 pop/sailor-song/bin/sylla-collect.py create mode 100644 pop/sailor-song/bin/sylla-prep.py create mode 100644 pop/sailor-song/bin/utterances.py create mode 100644 pop/sailor-song/bin/word-times.py create mode 100755 pop/sailor-song/c/build.sh create mode 100755 pop/sailor-song/c/cut.sh create mode 100644 pop/sailor-song/c/sailor-chart.h create mode 100644 pop/sailor-song/c/sailorremix-v5.c create mode 100644 pop/sailor-song/c/sailorremix-v7.c create mode 100644 pop/sailor-song/c/sailorremix.c create mode 100644 pop/sailor-song/choir.json create mode 100644 pop/sailor-song/measures.cut.json create mode 100644 pop/sailor-song/measures.cut.json.off create mode 100644 pop/sailor-song/measures.json create mode 100644 pop/sailor-song/measures.reg.json create mode 100644 pop/sailor-song/platter-analysis.json create mode 100644 pop/sailor-song/take.analysis.json create mode 100644 pop/sailor-song/vox-notes.json diff --git a/pop/.gitignore b/pop/.gitignore index 4aaeb3f442..cfd4c2510e 100644 --- a/pop/.gitignore +++ b/pop/.gitignore @@ -59,3 +59,14 @@ boombaboom/sources/*.wav emotribtranse/c/emotribtranse emotribtranse/c/out/ emotribtranse/out/ + +# sailor-song: niece's take + derived audio stay local (see ASSETS.md) +sailor-song/src/ +sailor-song/c/sailorremix + +# sailor-song cover: her photo stays out of git +sailor-song/cover/*.jpg +sailor-song/cover/*.png + +# afternoon-study: master, stems, WORLD cache (sources stay on the Shelf) +afternoon-study/out/ diff --git a/pop/sailor-song/ANALYSIS.md b/pop/sailor-song/ANALYSIS.md new file mode 100644 index 0000000000..5580788ac8 --- /dev/null +++ b/pop/sailor-song/ANALYSIS.md @@ -0,0 +1,303 @@ +# sailor-song · the take read against the platter + +Her take (`~/Desktop/IMG_8699.mov`, 2:59), measured in `take.analysis.json`, +`measures.json` and `vox-notes.json`, read against the papers platter: +[rhythm](../../papers/rhythm-platter/) (necklace geometry, syncopation, +distance, entrainment, beat bins), [chamber](../../papers/chamber-platter/) +(form, envelopes, the pop strategies digest) and +[bass](../../papers/bass-platter/) (sub register, tempo bands, delay in +beats). Every number below is produced by `bin/platter-analysis.mjs`, which +uses `pop/lib/necklace.mjs` for the rhythm math; the full readout is +`platter-analysis.json`. Rules are cited as `digest · Rn`. + +```sh +node pop/sailor-song/bin/platter-analysis.mjs +``` + +The lyric is Gigi Perez's and stays out of this file. Bars are 1-based, as +in `measures.json` and `c/sailor-chart.h`. + +## 1 · Her hand is a periodic necklace, not a syncopation + +The motif her right hand plays, `X..XX..X` on 8ths, is onsets {0, 3, 4, 7}, +inter-onset intervals **(3, 1, 3, 1)**. Read through +[rhythm 01–04](../../papers/rhythm-platter/digest/): + +| Property | Value | Meaning | +|---|---|---| +| Euclidean | no | E(4,8) is straight 8ths `x.x.x.x.`; hers is one chronotonic unit away | +| Periodic | **yes** (period 4) | the bar is two identical 2-beat cells: 1 &2 · 3 &4 | +| Evenness | 0.955 | nearly even; balanced (centroid at the origin) | +| Syncopation (LHL) | **0** | every strong pulse she leaves empty (2, 6) is weaker than the note sounding through it | +| Povel–Essens C | **0** | a 2-pulse clock fits with no counterevidence | +| Off-beatness | 2 of 4 (pulses 3, 7) | half her onsets sit on generators of C(8) | +| Interval vector | [0, 2, 0, 2, 2] | no adjacent-pulse pairs; the 3s and 1s alternate | + +So the figure is **anticipation without syncopation**: the `&2` and `&4` +strums are pickups into 3 and into the next downbeat, and both targets are +also struck. LHL and the C-score both read zero, off-beatness reads 50%: the +platter's point that a rhythm carries a vector, not a scalar +([04](../../papers/rhythm-platter/digest/04-syncopation.md)). Musically, the +pattern is a two-beat rocking cell, and the half-time drum feel the drafts +use (kick 1, snare 3) is the meter that cell already implies. + +**The kit is the same necklace.** `render.mjs` puts kick on 1 and `&2`, snare +on 3, rim on `&4`: onsets {0, 3, 4, 7}. That is her hand, doubled. Two +alternatives the platter hands over: + +- **Hocket** ([06](../../papers/rhythm-platter/digest/06-complements-canons.md)): + her complement `.xx..xx.` (pulses 1, 2, 5, 6) interlocks with her exactly + and shares her chronotonic curve (distance 0 under rotation). A hat or rim + on the complement fills every 8th between the two of you without ever + landing on her stroke. Good for the break (bars 68–71), where the guitar + is loudest and there is no voice to protect. +- **Morph** ([05](../../papers/rhythm-platter/digest/05-distance-similarity.md)): + the swap-geodesic from her cell to straight 8ths is two moves, + `x..xx..x → x.x.x..x → x.x.x.x.` (one onset per phrase). Walking it across + the last chorus into the break turns the folk rock into a driving 8th feel + without a cut. + +**How faithfully she keeps it.** 37 of 82 four-beat bars are the motif +verbatim; the rest add a stroke, mostly on the `&3` (`X..XXXXX` ×10, +`X..XX.XX` ×9). Mean chronotonic drift from the motif per section: verse 2 +0.13, verse 1 0.23, chorus 1 0.41, chorus 2 0.45, bridge 0.50, break 0.56. +She densifies as the song goes; the verses are the cleanest statement. +[Chamber 03 rule 2](../../papers/chamber-platter/digest/03-process-and-envelopes.md) +(introduce a pattern by substitution, one note per phrase) is what her own +hand does between verse and chorus. + +## 2 · Beat bins: her groove is in the pickups, not in swing + +[Rhythm 07](../../papers/rhythm-platter/digest/07-perception-groove.md) +(Danielsen): a beat is a bin, and where in the bin a stroke lands is a +decision. Measured against her own strum-snapped beats: + +| Layer | Placement | +|---|---| +| Her off-beat strums | phase 0.511 of the beat (p10 0.436, p90 0.578): **straight 8ths**, 5 ms late, swing ratio 1.05:1 | +| Her voice, all notes | signed median **+16 ms** behind her 8th grid (abs 41 ms) | +| Voice by section | verse 1 **+38**, chorus 1 +16, verse 2 +13, chorus 2 **+5**, bridge +9 | +| Long notes (> 0.4 s) | +7 ms · short notes (≤ 0.2 s): **+29 ms** | +| Phrase starts | 18 of 29 begin on the `&4` pickup, a median 265 ms after beat 4: **~20–120 ms behind the `&4` strum** | + +Three findings: + +1. **She does not swing.** The 8ths are straight; the earlier phase histogram + that suggested a late `&` was measuring the beat tracker's 35 ms lead. +2. **Her voice sits behind her hand, and tightens as the song goes.** Verse 1 + is sung 38 ms back; by chorus 2 she is on the grid. Short syllables lag, + held notes do not. That is a laid-back verse and a locked chorus, which is + an arrangement in itself. +3. **The anacrusis is the song's rhythmic signature.** Two thirds of her + phrases begin on the last stroke of the cell (`&4`) and she lands the + syllable a little after the string. The `&4` rim in the kit is therefore + the one hit that competes with her most; softening it (or moving it to the + complement) leaves the pickup to her. + +`lock-vox.mjs` pulled note onsets from 41 ms to 8 ms of the grid. Under +[07](../../papers/rhythm-platter/digest/07-perception-groove.md) that is the +grid-flattening the platter warns about: the necklace should decide *which +pulse*, and an offset table should decide *where in the bin*. The table above +is that layer. For the C engine, carry `voiceOffsetMs` per section (+38, +16, ++13, +5, +9) and a `pickupLagMs` (~50) as parameters, and either lock to +*those* or leave her as sung: locking to 8 ms removes the only microtiming +that distinguishes her verse from her chorus. + +## 3 · Entrainment: what is felt as a beat, and what is a phrase + +[Rhythm 07](../../papers/rhythm-platter/digest/07-perception-groove.md) +(London): a felt beat lives between ~100 ms and ~2 s, strongest at 500–700 ms. +At her median 120.9 BPM: + +| Level | Period | Status | +|---|---|---| +| 8th | 248 ms | subdivision, felt | +| quarter (her strum beat) | **496 ms** | 4 ms under the sweet spot's floor: the beat | +| half (the drafts' felt 60) | 993 ms | inside the window, the tactus the kit chooses | +| bar | 1985 ms | **at the ceiling** | +| 2-bar chord cycle | 3970 ms | **not a rhythm**: heard as harmony, not as a cycle | + +Her harmonic rhythm is 8.8 beats (a chord holds 2.2 bars on average; B is +held up to 6). So the chord changes are phrase events, and nothing rhythmic +should be asked of them. Any riser, delay throw or morph step wants to be +scheduled on the bar (2 s) or the half (1 s), where the ear still counts. + +**Tempo arch.** Median BPM per section: intro 125 · verse 1 **113.6** · +chorus 1 119.5 · verse 2 114.8 · chorus 2 123 · break 123 · bridge 121.6 · +outro **130**. The verses sit 6–7 BPM under the choruses, and she runs the +ending. In follow mode this is free arrangement: the choruses lift by tempo +alone, before any layer is added. Any bed constant in beats (delay, LFO, +sidechain release) has to be recomputed per bar from `measures.json`; a fixed +millisecond value drifts 8% between her verse and her outro. + +## 4 · Harmony: vi–I with the IV as an event + +84 bars: G#m 35, B 42, Emaj7 **7**. Runs: G#m median 2 bars (max 4), B median +2 (max 6), Emaj7 median 1 (max 2). In B major that is vi–I with IV appearing +seven times, always as a one- or two-bar event: bar 18 (verse 1), 28–29 (the +downbeat of chorus 1), 41 (end of chorus 1), 49 (verse 2), 73 and 80 (the +bridge's first and last bars). The tabs' cycle is IV–vi–I–I; her open low +string turns the IV bar into Emaj7/G# ⊃ G#m most of the time, and the true +Emaj7 surfaces where she strums the shape cleanly, which happens to be at +section edges. **The IV marks the seams.** Chorus 1 opens on it; the bridge +opens and closes on it. The bed should treat Emaj7 as a colour change +(add the major 3rd, E, in the tenor sines) rather than as one chord in three. + +## 5 · Melody: a pentatonic-leaning aeolian line that ends on 5 + +279 tuned notes (bleed before 23 s removed), G#3–G#4 (midi 52 dips to E3 in +the choruses). Time on each scale degree: + +| 5 | b3 | b6 | b7 | 4 | 1 | 2 | +|---|---|---|---|---|---|---| +| **34%** | 22% | 14% | 13% | 7.5% | 7.3% | **2.5%** | + +- **D# (5) is the home tone, not G#.** A third of the sung time sits on the + fifth; the tonic gets 7%. 17 of 29 phrases cadence on 5, four on b3, one on + 1. The line hangs open on the dominant and the guitar's vi–I underneath + supplies closure. A sine bed that doubles the tonic under her will feel + heavier than the take; doubling 5 and b3 keeps its suspension. +- **The 2nd is avoided** (A#, 2.5%). The working scale is 1 b3 4 5 b6 b7: minor + pentatonic plus b6, i.e. aeolian minus 2. The harmony stems and beds should + avoid A# in her register. +- **Motion:** 157 steps, 110 leaps, 8 repeats (step share 0.57). Leapy for a + ballad; the leaps are the 5→1 and 1→5 drops in the hook. +- **The hook** (most repeated 4-note degree sequence): **b7 b6 5 b3** ×12, then + 5 1 b7 1, 1 b7 1 5, b6 b7 b6 5, 5 b6 5 b3. The descending F#–E–D#–B tetrachord + is the cell she returns to; it fits every chord in the song (b7 is the 5th + of B, b6 is the root of Emaj7, 5 and b3 belong to G#m and B). +- **Register per section** (median midi): verse 61 (C#4) · chorus 63 (D#4) · + bridge 63. A minor third up from verse to chorus and no further: + [chamber 01 R8](../../papers/chamber-platter/digest/01-tonal-forms.md) + (register steps a third per phrase, never with density in the same phrase) + is what she does; the arrangement's job is to move density where she does + not move register (chorus 2, bridge). +- **Phrases:** 29 breath phrases (from the dry stem's energy), median 6 beats, + clustering at 3–4 beats (short answers) and 15–17 beats (the long chorus + lines). Contours 13 rise, 11 fall, 4 level. Eight in verse 1, six in + chorus 1, two in verse 2, eight in chorus 2, five in the bridge. +- **Against the chords:** 69% of sung time is on a chord tone; the non-chord + seconds fall on B (21 s) more than on G#m (15 s), because her b3/b6/b7 cell + rubs the I. Her own harmony stems, by chord-tone share over her chords: + a diatonic **3rd below 0.60**, 3rd above 0.55, 6th below 0.55, **5th above + 0.35**, octave 0.69. The `up5` stem is the dissonant one and wants to be + the rarest; `down3` is the safest constant companion. + +## 6 · Form: her climax is the guitar break, and her verses are louder than her choruses + +| Section | Bars | Start | Fraction | Vox dB | Guitar dB | +|---|---|---|---|---|---| +| intro | 1–10 (10) | 5.0 s | 0.03 | – | −21.5 | +| verse 1 | 11–27 (17) | 24.3 s | 0.14 | **−24.1** | −20.2 | +| chorus 1 | 28–43 (16) | 61.2 s | 0.34 | −22.1 | −19.9 | +| verse 2 | 44–51 (8) | 93.3 s | 0.52 | **−21.0** | −20.7 | +| chorus 2 | 52–67 (16) | 110.1 s | 0.61 | −24.0 | −20.0 | +| break | 68–71 (4) | 141.5 s | **0.79** | – | **−17.3** | +| bridge | 72–80 (9) | 149.4 s | 0.83 | −22.8 | −19.6 | +| outro | 81–84 (4) | 167.0 s | 0.93 | – | **−17.3** | + +Read against [chamber 01](../../papers/chamber-platter/digest/01-tonal-forms.md): + +- **R1** (phrase lengths from {8, 12, 16, 24}, never three equal in a row): + chorus 1, verse 2 and chorus 2 pass; verse 1 is 17 (a held B adds a bar), + the intro 10, the bridge 9. The odd bars are hers and are where a cadence + or a pickup breathes; the C engine should keep them, not round them. +- **R5** (climax at 0.71 after an unbroken 90 s crescendo) points at bar 60, + mid chorus 2. **Her loudness says otherwise:** the voice is flat across the + song (3 dB range) and actually peaks in the verses (loudest bars 15, 22, 18, + 47), while the guitar's peak is the **break at 0.79**, where she stops + singing and strums 3–4 dB harder. That is Shaker Loops' 0.80, not the + chaconne's 0.71. The take's own climax is instrumental, at bar 68, and the + arrangement should build toward it through chorus 2 rather than peak inside + chorus 2. Her singing does not get louder in the choruses, so the chorus + lift is the arrangement's to make. +- **R6** (stillness at 0.36: two voices, one chord, 8 bars, no attacks) lands + at bar 29, the first bars of chorus 1, which open on the IV. A bed that + thins to two sines on Emaj7 there, then fills over the chorus, uses the + rule where the song already has a seam. +- **R12** (ending restates the opening at half density, decay doubling per + phrase, one release cue): the outro is the intro's figure at 130 BPM and + the guitar's loudest level. Half density is not what she does; the + arrangement can supply it by dropping the kit at bar 81 and leaving her + alone with the sub. + +**Build curve** from [chamber 04](../../papers/chamber-platter/digest/04-pop-strategies.md) +(one curve, layers entering at 0.10 / 0.28 / 0.40 / 0.62 / 0.78 of the build) +mapped onto 84 bars: entries at bars **9** (end of intro), **24** (last bars +of verse 1), **34** (mid chorus 1), **52** (chorus 2 downbeat), **66** (two +bars before the break). The last entry two bars before her instrumental +peak is exactly the rule's intention, and chamber 03 rule 3 (remove pulse and +bass one phrase early, then land on one downbeat) says bars 64–67 should +thin before 68 lands. + +## 7 · The cut + +[Chamber 04](../../papers/chamber-platter/digest/04-pop-strategies.md): pop +tracks run about 1:30, and wannadash went from 3:28 to 1:54 by removing +repeated statements with seams on downbeats and 10 ms fades. Verse 2 and +chorus 2 restate verse 1 and chorus 1 on the same chords. + +Keep bars **7–43** and **68–84**, drop 1–6 and 44–67. One seam, bar 43 → 68, +G#m → G#m, both on her strum downbeat. Length **1:50**, wannadash's +neighbourhood. The full take stays the master; the cut is a release edit. +Check the lyric across the seam before committing to it. + +## 8 · Bass: the numbers the bed should carry + +[Bass 01](../../papers/bass-platter/digest/01-dub-bass.md), in her frame +(+13 ¢): + +| Rule | Applied to this take | +|---|---| +| R12 tempo band | 120.9 median puts the lane in dub techno's 110–125, not dubstep's 140 | +| R1 register | sub in the 30–60 Hz octave: **E1 41.5 Hz, G#1 52.3 Hz**; B1 is 62.2 Hz, at the band's top, so the I chord's sub can sit on F#1 or stay on B1 and take the 3rd harmonic layer | +| R3 pattern | kick 1 / snare 3 at ~120 is the dubstep half-time layout already in the drafts; a one-drop variant leaves 1 empty and puts kick plus cross-stick on 3, which would hand the downbeat back to her `&4` pickup | +| R4 late entry | at least one 8-bar phrase of space before the first bass note: the sub waits until bar 11, her first sung bar | +| R5 delay | 0.93 of a beat ≈ **462 ms** at median tempo, 4–5 repeats, high-passed 100 Hz, low-passed 4.5 kHz. In follow mode compute it per bar: 490 ms in verse 1, 428 ms in the outro | +| R8 kick vs guitar | her low G#2 (104 Hz) and B2 (124 Hz) sit in the kick-body band (100–200 Hz); duck the sub 2–3 dB per kick with the fastest attack, and either high-pass the guitar stem at ~110 Hz or keep the kick body under 100 Hz | +| R9 | mono below 100 Hz; the reverb the voice, halo and sines share stays above it | +| R10 | a harmonic copy of the sub high-passed at 150 Hz carrying 250–700 Hz, since MASTERING.md allows at most 5 dB loss through the 180 Hz–8 kHz phone proxy and the sub alone fails that test | + +## What this changes in the arrangement + +1. Keep a **microtiming layer** (§2) instead of locking her to 8 ms: per-section + voice offsets and the `&4` pickup lag, as chart parameters. +2. Soften or relocate the **`&4` rim** (§2); it is the one hit on her signature. +3. **Build to bar 68**, not bar 60 (§6): layer entries at 9 / 24 / 34 / 52 / 66, + thin bars 64–67, land the break as the peak, drop the kit at 81. +4. **Hocket on the complement** in the break; **morph to straight 8ths** across + chorus 2 if a drive is wanted (§1). +5. Beds double **5 and b3**, avoid **A#**, treat **Emaj7 as a colour** at the + seams (§4, §5); `down3` is the resident harmony stem, `up5` the rare one. +6. Sub on **E1 / G#1**, delay in beats per bar, the guitar high-passed off the + kick body (§8). +7. A **1:50 cut** at bar 43 → 68 for release; the 2:59 stays the master (§7). + +## v5 → v6: what the reading changed in the dance mix + +Measured on the masters (`cut.sh`, both at the −10 LUFS house target): + +| | v5 | v6 | +|---|---|---| +| Opening | 16 s of guitar with sines fading in, kick building in from bar 7 (−16 → −12 LUFS) | her first strum at 1.65 s, levelled to −17 dB RMS, guitar and voice alone; nothing synthetic until bar 16 | +| Arrival of the floor | bar 7, in pieces | bar 28, whole, on an explosion; one lane per phrase before it (chamber-03 rule 1) | +| Kit against her hand | the same necklace doubled, rim on her `&4` pickup | her complement `.xx..xx.` (hats &1/&3, claps 2/4), floor on 1 and 3; no rim where she sings; off-beats at her +5 ms bin | +| Clock (58–158 s, re-tracked) | local BPM p10–p90 113.9–123, CV 0.037 | 117.5–123, CV 0.024; her rubato untouched before bar 24 | +| Loudness range | LRA 2.2 LU | LRA 6.6 LU (house 4–8) | +| Harmonies | up3 + down6 in choruses | down3 resident from verse 1 (chord-tone share 0.60), up5 only in the bridge (0.35); arpeggiated stacks per section | +| Space | fixed orbits | every drop displaces the ring and springs it back at 1.1 Hz, damping 1.2; kick and sub never move | + +Not applied, deliberately: the 1:50 cut (§7) — the arc from bedroom to +floor needs the length; and the morph to straight 8ths (§1), which would +undo the hocket. Both remain options in the score. + +## Debts + +The phrase segmentation is an energy gate on the Demucs vocal (10 dB under +the loud frames, 0.35 s gap), swept 6–22 dB and set at the knee; it is not a +transcription of her breaths. Loudness per section is bar-mean RMS of the +separated stems, so it carries Demucs's leakage (her "intro" dB is bleed). +`pop/lib/necklace.mjs` deliberately leaves Keith's measure and Gómez's WNBD +unimplemented (its note-to-beat distance is a plain mean), so the syncopation +vector here is LHL, the C-score, note-to-beat and off-beatness, four of the +digest's five. The cut has not been listened to. diff --git a/pop/sailor-song/README.md b/pop/sailor-song/README.md new file mode 100644 index 0000000000..6d569c35a7 --- /dev/null +++ b/pop/sailor-song/README.md @@ -0,0 +1,160 @@ +# sailor-song + +A cover of Gigi Perez's "Sailor Song", played by @jeffrey's niece: her voice +and a capo'd nylon-string guitar, recorded on one phone mic +(`~/Desktop/IMG_8699.mov`, 2:59). This lane is an **outside remix**. It adds +drums and a bed *under her performance*. Her voice and guitar are the +compositional substrate, the same way fía's live MC is in `jungle/`. That +keeps the lane inside the bottom-up posture, with nothing generated top-down. + +## Pipeline + +```sh +# 1. audio out of the video (48k stereo) + mono for analysis +ffmpeg -i ~/Desktop/IMG_8699.mov -vn -ac 2 -ar 48000 pop/sailor-song/src/take.wav + +# 2. stems: Demucs through the shared sample path +mkdir -p pop/samples/sailor-song-take +cp pop/sailor-song/src/take.wav pop/samples/sailor-song-take/source.wav +node pop/bin/separate-stems.mjs --slug sailor-song-take +# guitar = other + bass + drums (strum transients land in "drums") + +# 3. read the take +pop/.venv/bin/python pop/sailor-song/bin/analyze-take.py pop/sailor-song/src/take.wav \ + --vocals pop/samples/sailor-song-take/stems/htdemucs/vocals.wav \ + --guitar pop/samples/sailor-song-take/stems/htdemucs/guitar.wav \ + --json pop/sailor-song/take.analysis.json +``` + +`toolchain/whistlegraph/analyze.py` is the wrong reader for this take. Its +pyin floor is 200 Hz, tuned for whistles, so her voice reads as a wall of +"G3 +35¢". `bin/analyze-take.py` tracks the voice from 80 to 1000 Hz and reads +chords per beat from the guitar stem. + +## Word bounds by ear (SyllaWizard track pass) + +The aligners get the words near; the ear places them. `bin/sylla-prep.py` cuts +one clip per lyric line and writes `src/sylla/spec.json` with a `track` field, so +`SyllaWizard --spec …` opens the WHOLE vocal as one scrolling spectrogram with +every word's box in place (drag edges; "from here ▶" shifts a word and every +later one; Save boundaries). `bin/sylla-collect.py` folds the boxes into +`src/word-bounds.json`, which `bin/word-times.py --fa` applies last. + +```sh +pop/.venv/bin/python pop/sailor-song/bin/sylla-prep.py +sylla-wizard/.build/debug/SyllaWizard --spec pop/sailor-song/src/sylla/spec.json # --lines = one line per page +pop/.venv/bin/python pop/sailor-song/bin/sylla-collect.py +pop/.venv/bin/python pop/sailor-song/bin/bounds-study.py [--apply] # the ear vs the machines, nudges +pop/.venv/bin/python pop/sailor-song/bin/word-times.py --fa +node pop/sailor-song/bin/splice.mjs && bash pop/sailor-song/bin/bake.sh +node pop/sailor-song/bin/score-video.mjs --lyrics --audio pop/sailor-song/out/sailor-song-v19.mp3 +``` + +What the 2026-09-29 hand pass (273 words) taught the tooling — `bin/bounds-study.py` +measures it, and these are now `word-times.py`'s defaults: + +- **Words tile edge to edge.** 267 of 268 neighbouring pairs butt together (gap ≤ 15 ms); + the only real gap is a breath > 350 ms. So a word runs to the next word's onset unless + the stem is quiet for a breath (`--breath-ms 250`); the old 60 ms rule cut vowels short. +- **An onset is the consonant, not the vowel.** Hand onsets sit a median 6 dB (p75 13 dB) + under the word's peak, at the start of the rise. Detected attacks (librosa, backtracked) + are within 40 ms of the ear for 54 % of words and within 100 ms for 90 %. +- **Raw forced alignment beats the syllable-bump re-cut.** Within 40 ms of the ear: FA 61/273, + FA snapped to an attack ≤ 40 ms 72/273, the shape pass 30/273. The shape pass is now + opt-in (`--shape`) and the snap window is 40 ms (`--snap-ms`; 150 pulled onsets onto the + wrong attack). +- **No machine is close enough on its own** (median error ~90–100 ms against the ear), so the + pipeline's truth is the SyllaWizard pass; the aligner only seeds it. + + +## Readout (v2, 2026-09-29) + +| | | +|---|---| +| Song | Capo 4. Tab shapes Cmaj7 · Em · G · G sound as **Emaj7 · G#m · B · B** ([UG](https://tabs.ultimate-guitar.com/tab/gigi-perez/sailor-song-chords-5363001), [guide](https://www.guitarsmartsupporter.com/2026/07/sailor-song-chords-by-gigi-perez-easy.html)). Original ~94 BPM, 3:31 | +| Her harmony | **G#m · G#m · B · B**, with B sometimes held 3–6 bars. The bass is G# in both "G#m" bars: the Cmaj7 shape strummed through the open low string sounds G#, so her Emaj7 bar is Emaj7/G# ⊃ G#m | +| Key / tuning | G# minor (B major). Guitar sits +13¢ sharp of A440 | +| Pulse | ~120 (felt 60). The slow stretch is verse 1, 110–114; the rest runs 117–125. 84 bars of 4/4 | +| Strum | Her hand plays a 2-beat motif `X..X` (8ths), so a bar reads `X..XX..X`: 1, &2, 3, &4 | +| Beat tracker | It sits ~35 ms early and slips a beat at 27.85 s and 61.17 s. `measures.py` snaps beats to strums and anchors bars on the motif, so 75% of chord changes land on downbeats | +| Form | intro 1–10 · verse1 11–27 · chorus1 28–43 · verse2 44–51 · chorus2 52–67 · break 68–71 · bridge 72–80 · outro 81–84 | +| Voice | G#3–G#4, median D#4. Words per bar are in `src/bars-words.json` (whisper, local only: the lyric is Gigi Perez's) | +| Aesthetivox | median distance to the grid 16¢ → **4¢** (27–87 s). Snap 1.0, 15 ms retune, hysteresis 0.3 st, 20% vibrato kept | + +## v2 (follow) + +```sh +pop/.venv/bin/python pop/sailor-song/bin/aesthetivox.py # → src/vox/vocals-aesthetivox.wav + vox-notes.json +pop/.venv/bin/python pop/sailor-song/bin/measures.py # → measures.json (bars, strums, chords) +node pop/bin/whisper-words.mjs pop/sailor-song/src/vox/vocals-dry-48k.wav --out pop/sailor-song/src/words.json +node pop/sailor-song/bin/render.mjs # → out/sailor-song-v2.mp3 +``` + +Kit hits sit on her strum points: kick on 1 and &2, snare on 3, rim on &4. +Measured: 91% of kick/snare hits land within 30 ms of a strum (median 0 ms). +The sine beds are voice-led: sub, a tenor trio under her, and a high pair +over her, with her own register left clear. In the choruses and bridge a sine +shadows her tuned melody a diatonic 3rd above, and chorus 2 and the bridge add +its octave. The layers per section are the `ORCH` table in `bin/render.mjs`. + +### A/B drafts + +```sh +node pop/sailor-song/bin/render-draft.mjs --mode follow # kit on her beats +node pop/sailor-song/bin/render-draft.mjs --mode warp --bpm 120 # take time-mapped to a grid +# --from/--to (source seconds, default 20–80) --bed 0.5 --rewarp +``` + +Warp runs rubberband R3 once over the whole take (`-M` timemap: each detected +beat → k·60/BPM, formant-preserving). There are no per-segment seams. +Re-tracked, the warped take centres on 120.2. The original swings 110–122 over +the same minute. + +## v6 (C engine · bedroom → dance) + +```sh +node pop/sailor-song/bin/regularize.mjs # her clock damped from bar 24 → src/vox/reg/, measures.reg.json +bash pop/sailor-song/bin/bake.sh # bells → chart → build → render (C) → cut (master + mp3 w/ cover) +# SAILOR_STEMS=locked pop/sailor-song/c/sailorremix # follow her raw clock instead +``` + +`c/sailorremix.c` is the score (v5 kept beside it as `c/sailorremix-v5.c`). +The record opens at bar 10, two bars of her guitar (a bedroom leveler on +the soft opening) with a soft shaker already keeping the tempo, then her +first word; from bar 14 one lane enters per phrase (congas 14, sub 16, pads +20, a kick on her 1 and 3 at 24, pickup bass 24, hats 25, riser 26) and the +floor lands whole with an explosion on chorus 1 (bar 28). v6.2 pumps it +K-pop style (an 85% kick duck on sines, guitars and harmonies, slower +release), drives the sines harder and wider, brings the electric replay up +in every chorus, and adds a sixth harmony stem, `harm-up8`: her voice an +octave up, leading at the end of chorus 1, the second half of chorus 2, the +bridge's climb and the outro. `lock-vox.mjs` now clamps every note's local +time ratio to 0.85–1.15 (before that a held note at 79.5 s was squeezed to a +fifth of its length). Cover: `bin/cover.py` at 107.15 s, eyes open at the +lens, mouth open on a held note, no title text. The kit plays her necklace's complement (open hats &1/&3, +claps 2/4) so every 8th is struck once between her hand and the kit; off-beat +hits sit at her measured +5 ms bin. Her harmony stems are gated into +arpeggios per section (8ths in chorus 1, 16ths in chorus 2, triplets in the +bridge), each step at its own seat; every drop displaces the whole ring and +springs it back (chamber-04 rule 10). `bin/regularize.mjs` keeps her rubato +through the bedroom bars and 30% of it after: verse 2 119, choruses 120–121, +outro 122 instead of 130. Master: −10.9 LUFS, −2.1 dBTP, LRA 6.6. + +## Platter reading + +[`ANALYSIS.md`](ANALYSIS.md) reads the take against the rhythm, chamber and +bass platters (`bin/platter-analysis.mjs` → `platter-analysis.json`): her +strum is a periodic, unsyncopated necklace the kit currently doubles; her +voice sits 38 ms behind her hand in verse 1 and 5 ms in chorus 2; the take's +loudness peak is the guitar break at 0.79, not a chorus; the melody lives on +5 and b3 and avoids the 2nd; a 1:50 cut runs bars 7–43 + 68–84. + +## Next + +1. Ear-check the chord cycle and the downbeat (bar phase 1 fits chord changes best). +2. Pick follow or warp. +3. Beat-map drum draft (AC percussion kit) + sub/pad bed on the 2-bar chord cycle. +4. Mix and master (`pop/MASTERING.md`). + +Release note: this is a cover. Distribution needs a mechanical licence (for +example DistroKid's cover licensing), and her say on the release. diff --git a/pop/sailor-song/bin/aesthetivox.py b/pop/sailor-song/bin/aesthetivox.py new file mode 100644 index 0000000000..5a44035f9e --- /dev/null +++ b/pop/sailor-song/bin/aesthetivox.py @@ -0,0 +1,359 @@ +#!/usr/bin/env python3 +"""aesthetivox.py — the niece's Sailor Song vocal through the WORLD chain. + +House rule (cult -> loner -> flwe -> xpld -> here): no lead vocal ships raw. +This lane FOLLOWS her — no time warp, no duration control — so this is +loner's regulation (pitch only, timing untouched) pushed sharper, with +cult/sing.py's singer's formant + de-ess and the macneopolitan chorus's +15 ms retune smoothing. + + THE STEM Demucs vocals (pop/samples/sailor-song-take/stems/...), + resampled to 48 k so it stays sample-aligned with the take. + THE FRAME the grid is G# natural minor (= B major) in the GUITAR's + frame, +13 c over A440 (take.analysis.json tuningCents). + Her voice is corrected toward her own guitar, not A440, so + voice, guitar and the sine beds all agree. + THE NOTE per-frame nearest scale tone with HYSTERESIS: the held note + only changes when the voice is clearly nearer the next one + (by HYST semitones). That is what keeps a hard snap from + trilling between neighbours — the sharpness without the + Eiffel-65 artifact. + THE SNAP SNAP 1.0, correction smoothed over SMOOTH_MS = 15 ms. A + trace of her vibrato (VIB_KEEP) rides on top so held notes + don't freeze. Snap fades out where the pitch is genuinely + sliding (|slope| > SLIDE_ST_PER_S) — a slide is not out of tune. + THE COMPOSITE voiced = WORLD render; unvoiced = the original stem + (5 ms cosine seams), so consonants stay hers. + +Writes: + src/vox/vocals-aesthetivox.wav tuned lead, same length/timing as the take + src/vox/vocals-dry-48k.wav the untuned stem at 48 k (for A/B) + vox-notes.json held-note segmentation (for sine harmonies) + + pyin before/after grid receipt + + pop/.venv/bin/python pop/sailor-song/bin/aesthetivox.py [--snap 1.0] [--smooth-ms 15] +""" +import argparse, json, os, subprocess +import numpy as np +import soundfile as sf +import pyworld as pw +import librosa +from scipy.signal import butter, sosfilt + +HERE = os.path.dirname(os.path.abspath(__file__)) +LANE = os.path.dirname(HERE) +POP = os.path.dirname(LANE) +STEM = os.path.join(POP, "samples/sailor-song-take/stems/htdemucs/vocals.wav") +VOX = os.path.join(LANE, "src/vox") +FS = 48000 +FRAME_MS = 5.0 + +ap = argparse.ArgumentParser() +ap.add_argument("--snap", type=float, default=1.0) +ap.add_argument("--smooth-ms", type=float, default=50.0) # v1 15 ms read glitchy, 35 still skippy +ap.add_argument("--hyst", type=float, default=0.45) # semitones +ap.add_argument("--vib-keep", type=float, default=0.30) +ap.add_argument("--formant-db", type=float, default=-2.5) # a DIP: v1's +1.6 lift read screamy +ap.add_argument("--slide", type=float, default=40.0) # st/s; at 14 the raw contour leaked through as blips +args = ap.parse_args() + +A = json.load(open(os.path.join(LANE, "take.analysis.json"))) +TUNE = A["tuningCents"] / 100.0 # +0.13 semitone +MINOR_GS = [8, 10, 11, 1, 3, 4, 6] # G# A# B C# D# E F# +NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] + +os.makedirs(VOX, exist_ok=True) +dry = os.path.join(VOX, "vocals-dry-48k.wav") +if os.path.exists(STEM): # the Demucs stem; on a machine without pop/samples, reuse the dry stem already made from it + subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", STEM, "-ac", "1", "-ar", str(FS), + "-c:a", "pcm_f32le", dry], check=True) +x, fs = sf.read(dry, dtype="float64") + + +def hz_to_m(hz): # midi in the guitar's frame (tuning offset removed) + return 69 + 12 * np.log2(hz / 440.0) - TUNE + + +def m_to_hz(m): + return 440.0 * 2 ** ((m + TUNE - 69) / 12) + + +def nearest_scale(m): + base = 12 * np.floor(m / 12.0) + opts = np.array([p + base + o for p in MINOR_GS for o in (-12, 0, 12)], float) + return opts[np.argmin(np.abs(opts - m))] + + +def smooth(v, frames): + if frames <= 1: + return v + k = np.hanning(frames * 2 + 1); k /= k.sum() + return np.convolve(v, k, mode="same") + + +print(f"WORLD analysis of {len(x)/fs:.1f}s …") +f0, t = pw.harvest(x, fs, f0_floor=150.0, f0_ceil=900.0, frame_period=FRAME_MS) +f0 = pw.stonemask(x, f0, t, fs) +fft_size = pw.get_cheaptrick_fft_size(fs, f0_floor=150.0) +sp = pw.cheaptrick(x, f0, t, fs, fft_size=fft_size, f0_floor=150.0) +apr = pw.d4c(x, f0, t, fs, fft_size=fft_size) +voiced = f0 > 0 +# Demucs bleed gives WORLD short false-voiced blips; require ≥ 40 ms runs. +runs = np.diff(np.concatenate([[0], voiced.astype(int), [0]])) +for a, b in zip(np.where(runs == 1)[0], np.where(runs == -1)[0]): + if (b - a) * FRAME_MS < 40: + voiced[a:b] = False + +# ── CLEAN THE TRACK ──────────────────────────────────────────────────── +# v1 fed the raw harvest contour to a hard snap: 251 frame-to-frame jumps +# > 2 st in 30 s of chorus (guitar bleed, octave slips) each became an +# audible step. Now: median-filter, fold octave errors against a 150 ms +# running median, and drop what still sits a 4th+ away. +def nanmedfilt(v, k): + out = np.full(len(v), np.nan) + h = k // 2 + for i in range(len(v)): + w = v[max(0, i - h):i + h + 1] + w = w[np.isfinite(w)] + if len(w): + out[i] = np.median(w) + return out + + +m = np.where(voiced, hz_to_m(np.maximum(f0, 1e-6)), np.nan) +m = nanmedfilt(m, 7) # 35 ms spikes out +ref = nanmedfilt(m, 31) # 155 ms context +off = m - ref +fold = np.isfinite(off) & (np.abs(off) > 7) +m[fold] -= 12 * np.round(off[fold] / 12) +bad = np.isfinite(m) & (np.abs(m - ref) > 4.5) +m[bad] = np.nan +voiced &= np.isfinite(m) + +# ── CONSONANTS ───────────────────────────────────────────────────────── +# harvest calls much of a sung line "voiced" straight through s/t/k/h and +# breaths, and WORLD then buzzes them. A frame is a consonant (the +# original audio plays) when it is aperiodic in the 1–5 kHz band OR its +# energy leans above 4 kHz. +fq = np.linspace(0, fs / 2, sp.shape[1]) +band = (fq > 1000) & (fq < 5000) +ap_band = apr[:, band].mean(axis=1) +hf = sp[:, fq > 4000].sum(axis=1) / (sp.sum(axis=1) + 1e-12) +sib = hf > max(0.12, np.percentile(hf[voiced], 90)) # s / sh / t bursts +noisy = ap_band > np.percentile(ap_band[voiced], 97) # only the breathiest +cons = sib | noisy +cons = np.convolve(cons.astype(float), np.ones(3) / 3, mode="same") > 0.5 +sung = voiced & ~cons + + +def runs_of(b): + d = np.diff(np.concatenate([[0], b.astype(int), [0]])) + return zip(np.where(d == 1)[0], np.where(d == -1)[0]) + + +# v3 read "skippy": airy vowels flagged as consonants dropped to her +# UNTUNED original for 5–60 ms slivers (~4 switches/s). Close short gaps +# unless they are real sibilants; drop short tuned islands. +for a, b in runs_of(~sung): + if 0 < a and b < len(sung) and (b - a) * FRAME_MS < 70 and not sib[a:b].any(): + sung[a:b] = voiced[a:b] | True +for a, b in runs_of(sung): + if (b - a) * FRAME_MS < 60: + sung[a:b] = False +sung &= voiced | np.roll(voiced, 1) | np.roll(voiced, -1) +sw = np.abs(np.diff(sung.astype(int))).sum() / (len(sung) * FRAME_MS / 1000) +print(f"switches/sec {sw:.2f}") +print(f"voiced {voiced.mean():.0%} consonant-in-voiced {(voiced & cons).sum() / max(1, voiced.sum()):.0%}") + +# ── THE NOTE: hysteresis + minimum note length ───────────────────────── +ms = smooth(np.where(np.isfinite(m), m, 0), 3) +ms = np.where(np.isfinite(m), ms / np.maximum(smooth(np.isfinite(m).astype(float), 3), 1e-6), np.nan) +target = np.full(len(m), np.nan) +cur = None +for i in range(len(m)): + if not voiced[i]: + cur = None + continue + s_ = nearest_scale(ms[i]) + if cur is None or (s_ != cur and abs(ms[i] - s_) < abs(ms[i] - cur) - args.hyst): + cur = s_ + target[i] = cur +MIN_NOTE = int(110 / FRAME_MS) # a note must hold 110 ms +i = 0 +while i < len(target): + if not np.isfinite(target[i]): + i += 1; continue + j = i + while j < len(target) and target[j] == target[i]: + j += 1 + if j - i < MIN_NOTE and i > 0 and np.isfinite(target[i - 1]): + target[i:j] = target[i - 1] + i = j + +# Ornament flatten: a trip to a neighbour and straight back (A→B→A under +# 160 ms) is her decoration, not a new note — a hard snap turns it into a +# stair-step blip (measured: 66 in 30 s of chorus). Hold A through it. +TRIP = int(160 / FRAME_MS) +for _ in range(2): + i = 0 + while i < len(target): + if not np.isfinite(target[i]): + i += 1; continue + j = i + while j < len(target) and target[j] == target[i]: + j += 1 + if (j - i) < TRIP and i > 0 and j < len(target) and np.isfinite(target[i - 1]) \ + and np.isfinite(target[j]) and target[i - 1] == target[j]: + target[i:j] = target[j] + i = j + +# ── THE SNAP: output = the note, plus a trace of her own movement ────── +vi = np.where(voiced)[0] +tgt_c = np.interp(np.arange(len(m)), vi, target[vi]) +msc = np.interp(np.arange(len(m)), vi, ms[vi]) +slope = np.abs(np.gradient(smooth(msc, 4))) / (FRAME_MS / 1000.0) +slide_w = np.clip(1.0 - (slope - args.slide) / args.slide, 0.0, 1.0) +vib = msc - smooth(msc, int(120 / FRAME_MS)) +tuned = smooth(tgt_c, int(args.smooth_ms / FRAME_MS)) + vib * args.vib_keep +w = args.snap * slide_w +m_new = w * tuned + (1 - w) * msc +m_new = np.where(voiced, m_new, np.nan) + +# continuous f0 through gaps (WORLD pops on 0→target jumps) +vi = np.where(voiced)[0] +f0_synth = m_to_hz(np.interp(np.arange(len(m)), vi, m_new[vi])) +f0_synth = np.where(np.isfinite(f0_synth), f0_synth, 200.0) +np.savez(os.environ.get('AVOX_DEBUG', '/dev/null.npz'), f0=f0_synth, sung=sung, voiced=voiced, target=target, ms=ms) if os.environ.get('AVOX_DEBUG') else None + +# presence: a broad dip around 3 kHz, not cult/sing.py's lift — her belts are already bright +freqs = np.linspace(0.0, fs / 2.0, sp.shape[1]) +sp = sp * (10.0 ** (args.formant_db * np.exp(-((freqs - 3000.0) / 1100.0) ** 2) / 10.0))[None, :] +print("resynthesizing …") +y = pw.synthesize(f0_synth, sp, apr, fs, frame_period=FRAME_MS) + +# ── THE COMPOSITE ─────────────────────────────────────────────────────── +spf = int(round(fs * FRAME_MS / 1000.0)) +n = len(x) +mask = np.repeat(sung.astype(np.float64), spf) +mask = np.pad(mask, (0, max(0, n - len(mask))), mode="edge")[:n] +y = np.pad(y, (0, max(0, n - len(y))))[:n] +ramp = int(0.012 * fs) # 12 ms seams (v1: 5 ms clicked) +edges = np.diff(mask.astype(np.int8)) +for idx in np.where(edges == 1)[0]: + k = np.arange(min(ramp, n - idx - 1)) + mask[idx + 1 + k] *= 0.5 - 0.5 * np.cos(np.pi * (k + 1) / ramp) +for idx in np.where(edges == -1)[0]: + k = np.arange(min(ramp, idx + 1)) + mask[idx - k] *= 0.5 - 0.5 * np.cos(np.pi * (k + 1) / ramp) +# WORLD renders slightly hotter than the stem; match voiced RMS first. +vr = np.sqrt(np.mean((y * mask) ** 2) + 1e-12) / np.sqrt(np.mean((x * mask) ** 2) + 1e-12) +out = mask * y / vr + (1.0 - mask) * x + + +def deess(v, thresh=0.05, ratio=0.45): + band = sosfilt(butter(2, [5000, 9000], btype="band", fs=fs, output="sos"), v) + env = np.abs(band) + a = np.exp(-1.0 / (0.004 * fs)) + for i in range(1, len(env)): + env[i] = max(env[i], a * env[i - 1]) + g = 1.0 / (1.0 + ratio * np.clip(env / thresh - 1.0, 0.0, None)) + return v - band * (1.0 - g) + + +out = deess(out) + + +def tame(v, lo=2000, hi=5000, thresh=0.04, ratio=0.8): + """The scream tamer: de-ess's shape one band down. Only when she belts + does 2–5 kHz get pulled back; quiet lines keep their air.""" + band = sosfilt(butter(2, [lo, hi], btype="band", fs=fs, output="sos"), v) + env = np.abs(band) + a = np.exp(-1.0 / (0.010 * fs)) + for i in range(1, len(env)): + env[i] = max(env[i], a * env[i - 1]) + g = 1.0 / (1.0 + ratio * np.clip(env / thresh - 1.0, 0.0, None)) + return v - band * (1.0 - g) + + +def level(v, win_ms=300, strength=0.55): + """Slow RMS leveling: loud phrases come down toward the median, soft + ones stay put. strength 0 = off, 1 = flat.""" + w = int(win_ms / 1000 * fs) + rms = np.sqrt(np.convolve(v ** 2, np.ones(w) / w, mode="same") + 1e-10) + ref = np.median(rms[rms > np.percentile(rms, 40)]) + g = np.where(rms > ref, (ref / rms) ** strength, 1.0) + return v * smooth(g, 20) + + +out = level(tame(out)) +# vowels-only octave halo (xpld's move): the sung frames, an octave up, +# dark and soft — written as its own stem so the mix sets its level. +f0_halo = f0_synth * 2.0 +sp_dark = sp * (1.0 / (1.0 + (freqs / 2500.0) ** 4))[None, :] +halo = pw.synthesize(f0_halo, sp_dark, apr, fs, frame_period=FRAME_MS) +halo = np.pad(halo, (0, max(0, n - len(halo))))[:n] * mask +halo *= 0.9 / (np.max(np.abs(halo)) + 1e-9) +sf.write(os.path.join(VOX, "vocals-halo.wav"), halo.astype(np.float32), fs) + +# ── HER OWN HARMONIES ────────────────────────────────────────────────── +# Each stem is her tuned line moved k steps along G# minor — the same +# vibrato, slides and envelope, a different scale degree. Formants stay +# put (cheaptrick's envelope untouched) so it is her voice, not a +# chipmunk; vowels only (consonants live in the lead). The arrangement +# (render.mjs) decides which interval sings where. +SC = sorted(MINOR_GS) + + +def step(midi, k): + pc, octv = int(round(midi)) % 12, int(round(midi)) // 12 + if pc not in SC: + return midi + {2: 3, -2: -4, 4: 7, -5: -8, -7: -12}.get(k, 0) + i = SC.index(pc) + k + return octv * 12 + SC[i % 7] + 12 * (i // 7) + (midi - round(midi)) + + +dark = sp * (1.0 / (1.0 + (freqs / 5500.0) ** 4))[None, :] +m_syn = hz_to_m(f0_synth) +HARM = {"up3": 2, "down3": -2, "up5": 4, "down6": -5, "down8": -7, "up8": 7} # v6.2: up8 — her voice an octave up, for the moments it should fly +for name, k in HARM.items(): + shifted = np.array([step(t_, k) for t_ in tgt_c]) - tgt_c # per-frame interval + shifted = smooth(shifted, int(40 / FRAME_MS)) # interval changes glide + hy = pw.synthesize(m_to_hz(m_syn + shifted), dark, apr, fs, frame_period=FRAME_MS) + hy = np.pad(hy, (0, max(0, n - len(hy))))[:n] * mask + hy *= 0.9 / (np.max(np.abs(hy)) + 1e-9) + sf.write(os.path.join(VOX, f"harm-{name}.wav"), hy.astype(np.float32), fs) + print(f" harmony {name:6s} ({k:+d} steps)") +sf.write(os.path.join(VOX, "vocals-aesthetivox.wav"), out.astype(np.float32), fs) + +# ── notes for the harmonies ───────────────────────────────────────────── +notes, a = [], None +for i in range(len(target) + 1): + v = target[i] if i < len(target) else np.nan + if a is not None and (not np.isfinite(v) or v != target[a]): + dur = (i - a) * FRAME_MS / 1000 + if dur >= 0.12: + notes.append({"t": round(a * FRAME_MS / 1000, 3), "dur": round(dur, 3), + "midi": int(target[a]), "note": NAMES[int(target[a]) % 12] + str(int(target[a]) // 12 - 1)}) + a = None + if a is None and np.isfinite(v): + a = i + + +# ── receipt: pyin |cents to grid| before/after ───────────────────────── +def grid_dev(sig): + seg = sig[int(27 * fs):int(87 * fs)] # a minute of singing + seg = librosa.resample(seg.astype(np.float32), orig_sr=fs, target_sr=22050) + p, vf, vp = librosa.pyin(seg, fmin=150, fmax=900, sr=22050, frame_length=2048) + p = p[vf & (vp > 0.5)] + p = p[np.isfinite(p)] + mm = hz_to_m(p) + return float(np.median(np.abs([mv - nearest_scale(mv) for mv in mm]))) * 100 + + +before, after = grid_dev(x), grid_dev(out) +json.dump({"grid": "G# natural minor, +13c frame", "snap": args.snap, + "smoothMs": args.smooth_ms, "hyst": args.hyst, "vibKeep": args.vib_keep, + "centsToGrid": {"before": round(before, 1), "after": round(after, 1)}, + "notes": notes}, open(os.path.join(LANE, "vox-notes.json"), "w"), indent=1) +print(f"grid-dev (27–87s): {before:.1f}¢ → {after:.1f}¢ {len(notes)} held notes") +print(f"✓ {os.path.join(VOX, 'vocals-aesthetivox.wav')}") diff --git a/pop/sailor-song/bin/analyze-take.py b/pop/sailor-song/bin/analyze-take.py new file mode 100644 index 0000000000..e51465ce65 --- /dev/null +++ b/pop/sailor-song/bin/analyze-take.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +# analyze-take.py — read a live voice+guitar take the way we read a +# menuband take: where the pulse is (and how far it drifts), what the +# tuning is, what chord sits under each beat, and what the voice sings. +# +# Unlike toolchain/whistlegraph/analyze.py (whistle range, fmin 200 Hz), +# this tracks a sung voice (80–1000 Hz) and a strummed guitar. Feed it +# the full mix, or pass --vocals / --guitar stems once Demucs has split +# them for cleaner melody and chord reads. +# +# Usage: +# pop/.venv/bin/python pop/sailor-song/bin/analyze-take.py src/take.wav \ +# [--vocals stems/vocals.wav] [--guitar stems/other.wav] [--json out.json] + +import argparse, json +import numpy as np +import librosa + +NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] + + +def name(m): + m = int(round(m)) + return f"{NAMES[m % 12]}{m // 12 - 1}" + + +def load(path, sr=22050): + y, _ = librosa.load(path, sr=sr, mono=True) + return y, sr + + +# ── pulse ───────────────────────────────────────────────────────────── +def pulse(y, sr): + onset = librosa.onset.onset_strength(y=y, sr=sr, hop_length=512) + tempo, beats = librosa.beat.beat_track( + onset_envelope=onset, sr=sr, hop_length=512, tightness=60, units="time" + ) + tempo = float(np.atleast_1d(tempo)[0]) + # Strummed guitar at a slow tempo often reads at half speed — keep + # both and let the reader pick the felt pulse. + ibi = np.diff(beats) + local = 60.0 / ibi if len(ibi) else np.array([]) + # Drift per 8-beat window: how rubato is the take? + win = [] + for i in range(0, len(beats) - 8, 8): + seg = beats[i : i + 9] + win.append({"t": round(float(seg[0]), 2), + "bpm": round(float(8 * 60.0 / (seg[-1] - seg[0])), 1)}) + return { + "tempoBPM": round(tempo, 2), + "doubleTimeBPM": round(tempo * 2, 2), + "beats": [round(float(b), 3) for b in beats], + "ibiMedian": round(float(np.median(ibi)), 3) if len(ibi) else None, + "ibiCV": round(float(np.std(ibi) / np.mean(ibi)), 3) if len(ibi) else None, + "localBPM": {"min": round(float(local.min()), 1), + "p10": round(float(np.percentile(local, 10)), 1), + "median": round(float(np.median(local)), 1), + "p90": round(float(np.percentile(local, 90)), 1), + "max": round(float(local.max()), 1)} if len(local) else None, + "drift": win, + } + + +# ── chords ──────────────────────────────────────────────────────────── +def chord_templates(): + t, labels = [], [] + shapes = {"": [0, 4, 7], "m": [0, 3, 7], "7": [0, 4, 7, 10], + "maj7": [0, 4, 7, 11], "m7": [0, 3, 7, 10], "sus4": [0, 5, 7], + "sus2": [0, 2, 7]} + for root in range(12): + for q, iv in shapes.items(): + v = np.zeros(12) + for i in iv: + v[(root + i) % 12] = 1 + # Bias toward plain triads so 7ths only win when clearly there. + w = 1.0 if q in ("", "m") else 0.93 + t.append(w * v / np.linalg.norm(v)) + labels.append(NAMES[root] + q) + return np.array(t), labels + + +def chords(y, sr, beats, tuning): + yh = librosa.effects.harmonic(y, margin=4) + chroma = librosa.feature.chroma_cqt(y=yh, sr=sr, hop_length=512, + tuning=tuning, bins_per_octave=36) + frames = librosa.time_to_frames(beats, sr=sr, hop_length=512) + sync = librosa.util.sync(chroma, frames, aggregate=np.median) + T, labels = chord_templates() + sync = sync / (np.linalg.norm(sync, axis=0, keepdims=True) + 1e-9) + score = T @ sync + idx = score.argmax(axis=0) + # Median-ish smoothing: a chord must hold 2 beats unless neighbors agree. + for i in range(1, len(idx) - 1): + if idx[i - 1] == idx[i + 1] != idx[i]: + idx[i] = idx[i - 1] + bounds = np.concatenate([[0.0], beats, [len(y) / sr]]) + per_beat = [] + for i, k in enumerate(idx): + per_beat.append({"t": round(float(bounds[i]), 2), "chord": labels[k], + "conf": round(float(score[k, i]), 2)}) + # Collapse runs. + runs = [] + for b in per_beat: + if runs and runs[-1]["chord"] == b["chord"]: + runs[-1]["beats"] += 1 + else: + runs.append({"t": b["t"], "chord": b["chord"], "beats": 1}) + # Vocabulary by total beats. + vocab = {} + for r in runs: + vocab[r["chord"]] = vocab.get(r["chord"], 0) + r["beats"] + vocab = dict(sorted(vocab.items(), key=lambda kv: -kv[1])) + return {"runs": runs, "vocab": vocab, + "chromaMean": [round(float(c), 3) for c in chroma.mean(axis=1)]} + + +def key_of(chroma_mean): + maj = np.array([6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88]) + mnr = np.array([6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17]) + c = np.array(chroma_mean) + best = [] + for prof, q in ((maj, "major"), (mnr, "minor")): + for i in range(12): + best.append((float(np.corrcoef(np.roll(prof, i), c)[0, 1]), NAMES[i] + " " + q)) + best.sort(reverse=True) + return [{"key": k, "r": round(r, 3)} for r, k in best[:4]] + + +# ── voice ───────────────────────────────────────────────────────────── +def voice(y, sr, tuning): + hop = 256 + f0, voiced, prob = librosa.pyin(y, sr=sr, fmin=80, fmax=1000, + frame_length=2048, hop_length=hop) + t = librosa.times_like(f0, sr=sr, hop_length=hop) + midi = librosa.hz_to_midi(f0) - tuning # tuning in semitones + ok = voiced & (prob > 0.5) & np.isfinite(midi) + notes, cur = [], None + for i in range(len(midi)): + if not ok[i]: + if cur: + notes.append(cur); cur = None + continue + q = int(round(midi[i])) + if cur and abs(q - cur["q"]) == 0: + cur["end"] = t[i]; cur["cents"].append(100 * (midi[i] - q)) + else: + if cur: + notes.append(cur) + cur = {"q": q, "start": t[i], "end": t[i], "cents": [100 * (midi[i] - q)]} + if cur: + notes.append(cur) + out = [] + for n in notes: + d = n["end"] - n["start"] + hop / sr + if d < 0.09: # drop portamento blips + continue + out.append({"t": round(float(n["start"]), 3), "dur": round(float(d), 3), + "midi": n["q"], "note": name(n["q"]), + "cents": int(round(float(np.median(n["cents"]))))}) + # Merge same-pitch notes split by tiny gaps. + merged = [] + for n in out: + if merged and merged[-1]["midi"] == n["midi"] and \ + n["t"] - (merged[-1]["t"] + merged[-1]["dur"]) < 0.08: + merged[-1]["dur"] = round(n["t"] + n["dur"] - merged[-1]["t"], 3) + else: + merged.append(n) + vm = midi[ok] + hist = np.bincount((np.round(vm).astype(int) % 12), minlength=12) if len(vm) else np.zeros(12) + return { + "range": [name(np.percentile(vm, 2)), name(np.percentile(vm, 98))] if len(vm) else None, + "median": name(np.median(vm)) if len(vm) else None, + "voicedFraction": round(float(ok.mean()), 3), + "pitchClassHist": {NAMES[i]: int(hist[i]) for i in range(12)}, + "notes": merged, + } + + +# ── sections (energy + phrase gaps) ─────────────────────────────────── +def phrases(vnotes, gap=0.9): + out, cur = [], None + for n in vnotes: + if cur and n["t"] - cur["end"] > gap: + out.append(cur); cur = None + if not cur: + cur = {"start": n["t"], "end": n["t"] + n["dur"], "notes": []} + cur["end"] = n["t"] + n["dur"] + cur["notes"].append(n["note"]) + if cur: + out.append(cur) + return [{"start": round(p["start"], 2), "end": round(p["end"], 2), + "line": " ".join(p["notes"])} for p in out] + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("mix") + ap.add_argument("--vocals") + ap.add_argument("--guitar") + ap.add_argument("--json") + a = ap.parse_args() + + y, sr = load(a.mix) + yv = load(a.vocals)[0] if a.vocals else y + yg = load(a.guitar)[0] if a.guitar else y + + tuning = float(librosa.estimate_tuning(y=yg, sr=sr)) # fraction of a semitone + P = pulse(yg, sr) + C = chords(yg, sr, np.array(P["beats"]), tuning) + K = key_of(C["chromaMean"]) + V = voice(yv, sr, tuning) + ph = phrases(V["notes"]) + + res = {"source": a.mix, "duration": round(len(y) / sr, 2), + "tuningCents": round(tuning * 100, 1), "pulse": P, "key": K, + "chords": C, "voice": V, "phrases": ph, + "stems": {"vocals": a.vocals, "guitar": a.guitar}} + + print(f"duration {res['duration']}s tuning {res['tuningCents']:+}¢ off A440") + print(f"tempo {P['tempoBPM']} BPM (x2 = {P['doubleTimeBPM']}) " + f"beat-interval CV {P['ibiCV']}") + print(f"local BPM {P['localBPM']}") + print("drift " + " ".join(f"{w['t']:.0f}s:{w['bpm']}" for w in P["drift"])) + print("key " + ", ".join(f"{k['key']} ({k['r']})" for k in K)) + print("chords " + ", ".join(f"{c} {n}" for c, n in list(C["vocab"].items())[:10])) + print(f"voice {V['range']} median {V['median']} voiced {V['voicedFraction']}") + print("── chord runs ──") + line = [] + for r in C["runs"]: + line.append(f"{r['chord']}×{r['beats']}") + print(" ".join(line)) + print("── phrases ──") + for p in ph: + print(f" {p['start']:7.2f}–{p['end']:7.2f} {p['line']}") + if a.json: + with open(a.json, "w") as f: + json.dump(res, f, indent=1) + + +if __name__ == "__main__": + main() diff --git a/pop/sailor-song/bin/bake.sh b/pop/sailor-song/bin/bake.sh new file mode 100755 index 0000000000..06f0a0fe82 --- /dev/null +++ b/pop/sailor-song/bin/bake.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# bake.sh — the whole record from the baked stems, wannadash's bake-c.sh shape: +# chart → build → render (C) → cut. Run the measuring side first +# (aesthetivox.py, measures.py, lock-vox.mjs, replay-guitar.mjs, bells.sh, +# then regularize.mjs — the engine plays src/vox/reg/ by default). +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LANE="$(dirname "$HERE")" +ROOT="$(cd "$LANE/../.." && pwd)" +echo "→ bells"; bash "$HERE/bells.sh" | tail -1 +echo "→ build"; bash "$LANE/c/build.sh" | tail -1 +echo "→ render (C)"; (cd "$ROOT" && time "$LANE/c/sailorremix") +echo "→ cut"; bash "$LANE/c/cut.sh" diff --git a/pop/sailor-song/bin/bells.sh b/pop/sailor-song/bin/bells.sh new file mode 100755 index 0000000000..a44245e82a --- /dev/null +++ b/pop/sailor-song/bin/bells.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# bells.sh — bake the FEM bell bank the C engine rings (pop/bell/c/bell: +# a thin-shell finite-element model solved for its modes, rendered by +# modal synthesis). Bronze handbell, one strike per G#-minor pitch from +# E4 to E5 — the loner rule keeps bells at or under E5 — tuned into her +# guitar's +13¢ frame. ~10 s per pitch; cached, so only the first run pays. +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LANE="$(dirname "$HERE")" +BELL="$LANE/../bell/c/bell" +DIR="$LANE/src/bells" +mkdir -p "$DIR" +[ -x "$BELL" ] || (cd "$LANE/../bell/c" && bash build.sh) +for m in 63 64 66 68 70 71 73 75 76; do + out="$DIR/bell-$m.wav" + [ -f "$out" ] && continue + hz=$(awk -v m="$m" 'BEGIN{printf "%.3f", 440*2^((m+0.13-69)/12)}') + "$BELL" --note "$hz" --material bronze --geometry handbell --dur 4 --vel 0.5 --out "$out" 2>/dev/null + echo " fem bell $m ($hz Hz)" +done +echo "✓ $DIR" diff --git a/pop/sailor-song/bin/bounds-study.py b/pop/sailor-song/bin/bounds-study.py new file mode 100644 index 0000000000..8852383b57 --- /dev/null +++ b/pop/sailor-song/bin/bounds-study.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""bounds-study.py — what the hand-drawn word bounds teach the aligner. + +Compares src/word-bounds.json (SyllaWizard, take seconds — the ear's answer) +against the automatic answers for the same words: + FA src/.word-times/forced-align.json MMS forced alignment (bin/forced-align.py) + AUTO word-times.py --fa's full pipeline + (pass it with --auto; default: src/.word-times/words-aligned-auto.json) +and against her own energy envelope (src/vox/vocals-natural.wav, the clock the +clips were cut from), to read off the thresholds the ear actually uses: + · how many dB under the word's peak an onset sits (the consonant attack) + · how many dB under the peak a word is released (the held-vowel tail) + · how far a hand onset is from the nearest detected vocal onset (snap window) + · the gap that separates "same phrase" from "breath" (the phrase threshold) +Then a quality pass over the hand bounds themselves: onsets that start late +(an energy attack shortly before them), ends cut inside a loud vowel, words +shorter than a syllable can be, and tiny gaps that are really contiguous. + + pop/.venv/bin/python pop/sailor-song/bin/bounds-study.py # report + pop/.venv/bin/python pop/sailor-song/bin/bounds-study.py --apply # also nudge + snaps a hand onset ≤ --snap-ms (default 25) onto the detected attack and + writes it back through the SyllaWizard files (sylls-NN / words-NN, clip ms) + +Writes src/.word-times/bounds-study.json (every word's numbers) for the platter. +""" +import argparse, json, os, sys +import numpy as np, librosa, soundfile as sf + +ap = argparse.ArgumentParser() +ap.add_argument("--auto", default=None) +ap.add_argument("--apply", action="store_true") +ap.add_argument("--snap-ms", type=float, default=0, help="also snap onsets this close to a detected attack (0 = off)") +a = ap.parse_args() +LANE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SRC = os.path.join(LANE, "src"); SY = os.path.join(SRC, "sylla"); TMP = os.path.join(SRC, ".word-times") +hand = json.load(open(os.path.join(SRC, "word-bounds.json")))["words"] +fa = json.load(open(os.path.join(TMP, "forced-align.json"))) +auto_path = a.auto or os.path.join(TMP, "words-aligned-auto.json") +auto = json.load(open(auto_path)) if os.path.exists(auto_path) else None +lyric = [w for l in open(os.path.join(SRC, "lyrics-sung.txt")) if l.strip() and not l.startswith("[") for w in l.split()] + +# ── her envelope: 10 ms RMS in dB, detected attacks (backtracked), the floor ── +y, sr = librosa.load(os.path.join(SRC, "vox", "vocals-natural.wav"), sr=22050, mono=True) +hop = int(sr * 0.01) +e = librosa.feature.rms(y=y, frame_length=hop * 3, hop_length=hop)[0] +edb = (20 * np.log10(e + 1e-6)).astype(float) +t = np.arange(len(edb)) * 0.01 +floor = float(np.percentile(edb[edb > -80], 20)) +env = librosa.onset.onset_strength(y=y, sr=sr, hop_length=256) +on = librosa.onset.onset_detect(onset_envelope=env, sr=sr, hop_length=256, backtrack=True, units="time", delta=0.05) +def db_at(s): return float(np.interp(s, t, edb)) +def peak_in(s0, s1): + i0, i1 = int(s0 / 0.01), max(int(s0 / 0.01) + 1, int(s1 / 0.01)); return float(edb[i0:i1].max()) +def near_on(s): d = np.abs(on - s); k = int(np.argmin(d)); return float(on[k]), float(on[k] - s) + +rows = [] +for k, h in enumerate(hand): + i = h["i"]; f = fa[i] if i < len(fa) else None; u = auto[i] if auto and i < len(auto) else None + pk = peak_in(h["from"], h["to"]) + prev = hand[k - 1] if k and hand[k - 1]["i"] == i - 1 else None + nxt = hand[k + 1] if k + 1 < len(hand) and hand[k + 1]["i"] == i + 1 else None + o, dv = near_on(h["from"]) + row = {"i": i, "text": h["text"], "from": h["from"], "to": h["to"], "dur": round(h["to"] - h["from"], 3), + "on_db_under_peak": round(pk - db_at(h["from"]), 1), + "off_db_under_peak": round(pk - db_at(h["to"]), 1), + "peak_db_over_floor": round(pk - floor, 1), + "nearest_attack_ms": round(dv * 1000), + "gap_before": None if not prev else round(h["from"] - prev["to"], 3), + "gap_after": None if not nxt else round(nxt["from"] - h["to"], 3), + "fa_on_ms": None if not f else round((h["from"] - f["from"]) * 1000), + "fa_off_ms": None if not f else round((h["to"] - f["to"]) * 1000), + "auto_on_ms": None if not u else round((h["from"] - u["fromMs"] / 1000) * 1000), + "auto_off_ms": None if not u else round((h["to"] - u["toMs"] / 1000) * 1000)} + rows.append(row) +json.dump({"floor_db": round(float(floor), 1), "words": rows}, open(os.path.join(TMP, "bounds-study.json"), "w"), indent=1) + +def q(xs, ps=(10, 25, 50, 75, 90)): + xs = np.array([x for x in xs if x is not None], float) + return ", ".join(f"p{p} {np.percentile(xs, p):+.0f}" for p in ps) if len(xs) else "-" +def within(xs, ms): + xs = [x for x in xs if x is not None]; return f"{sum(abs(x) <= ms for x in xs)}/{len(xs)}" +print(f"{len(rows)} hand words · floor {floor:.1f} dB · {len(on)} detected attacks") +print("\n── the ear vs the machines (hand − machine, ms; + = hand later) ──") +print(f"FA onsets {q([r['fa_on_ms'] for r in rows])} within 40: {within([r['fa_on_ms'] for r in rows], 40)}") +print(f"FA ends {q([r['fa_off_ms'] for r in rows])} within 40: {within([r['fa_off_ms'] for r in rows], 40)}") +if auto: + print(f"AUTO onsets {q([r['auto_on_ms'] for r in rows])} within 40: {within([r['auto_on_ms'] for r in rows], 40)}") + print(f"AUTO ends {q([r['auto_off_ms'] for r in rows])} within 40: {within([r['auto_off_ms'] for r in rows], 40)}") +print("\n── the ear vs her envelope ──") +print(f"onset sits under the word's peak by {q([r['on_db_under_peak'] for r in rows])} dB") +print(f"release sits under the word's peak by {q([r['off_db_under_peak'] for r in rows])} dB") +print(f"hand onset → nearest detected attack {q([r['nearest_attack_ms'] for r in rows])} ms · within 40: {within([r['nearest_attack_ms'] for r in rows], 40)} · within 100: {within([r['nearest_attack_ms'] for r in rows], 100)}") +gaps = [r["gap_after"] for r in rows if r["gap_after"] is not None] +print(f"gap to the next word: contiguous (≤15 ms) {sum(g <= 0.015 for g in gaps)}, 15–150 ms {sum(0.015 < g <= 0.15 for g in gaps)}, 150–350 ms {sum(0.15 < g <= 0.35 for g in gaps)}, >350 ms (breath) {sum(g > 0.35 for g in gaps)} of {len(gaps)}") +print(f"durations: {q([r['dur'] * 1000 for r in rows])} ms · shortest: " + ", ".join(f"{r['text']}@{r['from']:.2f} {r['dur']*1000:.0f}" for r in sorted(rows, key=lambda r: r['dur'])[:5])) + +# ── which onset policy would have matched the ear? (scored against the hand onsets) ── +print("\n── onset policies scored against the ear (share of words within 40 / 80 ms) ──") +def score(pred): + d = [abs(r["from"] - p) * 1000 for r, p in zip(rows, pred) if p is not None] + return f"within 40: {sum(x <= 40 for x in d):3d}/{len(d)} within 80: {sum(x <= 80 for x in d):3d}/{len(d)} median {np.median(d):.0f} ms" +def snapped(src, win): + out = [] + for r in rows: + s = src(r) + if s is None: out.append(None); continue + o, dv = near_on(s); out.append(o if abs(dv) <= win else s) + return out +fa_on = lambda r: fa[r["i"]]["from"] if r["i"] < len(fa) else None +au_on = lambda r: auto[r["i"]]["fromMs"] / 1000 if auto and r["i"] < len(auto) else None +print(f"FA raw {score([fa_on(r) for r in rows])}") +for win in (0.04, 0.08, 0.12, 0.2): + print(f"FA → attack within {int(win*1000):3d} ms {score(snapped(fa_on, win))}") +if auto: + print(f"AUTO (shape pass) {score([au_on(r) for r in rows])}") + print(f"AUTO → attack within 100 {score(snapped(au_on, 0.1))}") + +# ── quality pass ── +print("\n── quality pass on the hand bounds ──") +late, cut, short, seams, snaps = [], [], [], [], [] +for r in rows: + # an attack 30–120 ms BEFORE the hand onset, with the hand onset already loud: the consonant was left out + if -0.12 <= r["nearest_attack_ms"] / 1000 <= -0.03 and r["on_db_under_peak"] < 6: late.append(r) + # released while still within 6 dB of the peak and nothing follows for 150 ms: a held note cut short + if r["off_db_under_peak"] < 6 and (r["gap_after"] is None or r["gap_after"] > 0.15) and r["peak_db_over_floor"] > 12: cut.append(r) + if r["dur"] < 0.06: short.append(r) + if r["gap_after"] is not None and 0 < r["gap_after"] <= 0.03: seams.append(r) + if 0 < abs(r["nearest_attack_ms"]) <= a.snap_ms: snaps.append(r) +def show(label, rs, fmt): + print(f"{label} ({len(rs)}):" + ("" if rs else " none")) + for r in rs[:14]: print(" " + fmt(r)) + if len(rs) > 14: print(f" … {len(rs) - 14} more") +show("onset misses an attack just before it", late, lambda r: f"{r['text']!r}@{r['from']:.2f} attack {r['nearest_attack_ms']:+d} ms, onset {r['on_db_under_peak']} dB under peak") +show("released inside a loud vowel", cut, lambda r: f"{r['text']!r}@{r['from']:.2f} end {r['off_db_under_peak']} dB under peak, {r['gap_after'] or 999:.2f} s before the next word") +show("shorter than 60 ms", short, lambda r: f"{r['text']!r}@{r['from']:.2f} {r['dur']*1000:.0f} ms") +show("tiny gaps (≤30 ms) that read as contiguous", seams, lambda r: f"{r['text']!r}@{r['from']:.2f} {r['gap_after']*1000:.0f} ms") +show(f"onsets within {a.snap_ms:.0f} ms of a detected attack (snap candidates)", snaps, lambda r: f"{r['text']!r}@{r['from']:.2f} {r['nearest_attack_ms']:+d} ms") + +# ── nudges: the onset moves back to the attack it missed; a loud line-final release runs out to the fall ── +nudges = {} # word index → (new from | None, new to | None) +for r in late: nudges[r["i"]] = (round(r["from"] + r["nearest_attack_ms"] / 1000, 3), None) +for r in cut: + s = r["to"] + while s < t[-1] - 0.02 and db_at(s) > peak_in(r["from"], r["to"]) - 12: s += 0.01 + nudges[r["i"]] = (nudges.get(r["i"], (None, None))[0], round(min(s, r["to"] + 1.0), 3)) +for r in snaps: + if r["i"] not in nudges: nudges[r["i"]] = (round(r["from"] + r["nearest_attack_ms"] / 1000, 3), None) +if a.apply and nudges: + spec = json.load(open(os.path.join(SY, "spec.json"))) + moved = [] + for tk in spec["takes"]: + wf = os.path.join(SY, f"words-{tk['id']}.json"); sp = os.path.join(SY, f"sylls-{tk['id']}.json") + if not os.path.exists(sp): continue + seed = json.load(open(wf)); off = seed["offsetSec"]; sy = json.load(open(sp)) + by_i = {seed["wordIndex"][wi]: wi for wi in range(len(seed["wordIndex"]))} + dirty = False + for i, (nf, nt) in nudges.items(): + if i not in by_i: continue + wi = by_i[i]; mine = [s for s in sy["sylls"] if s["wi"] == wi] + if not mine: continue + text = seed["words"][wi]["text"] if wi < len(seed["words"]) else "?" + if nf is not None: + s0 = min(mine, key=lambda s: s["fromMs"]); old = s0["fromMs"]; new = round((nf - off) * 1000) + s0["fromMs"] = new; seed["words"][wi]["fromMs"] = new + prevs = [s for s in sy["sylls"] if s["wi"] == wi - 1] # a joined neighbour's end rides along + if prevs: + p = max(prevs, key=lambda s: s["toMs"]) + if abs(p["toMs"] - old) <= 15: p["toMs"] = new; seed["words"][wi - 1]["toMs"] = new + moved.append(f"{text}@{nf:.2f} onset {new - old:+d} ms"); dirty = True + if nt is not None: + s1 = max(mine, key=lambda s: s["toMs"]); old = s1["toMs"]; new = round((nt - off) * 1000) + s1["toMs"] = new; seed["words"][wi]["toMs"] = new + moved.append(f"{text}@{off + old / 1000:.2f} release {new - old:+d} ms"); dirty = True + if dirty: + json.dump(sy, open(sp, "w"), indent=2, sort_keys=True); json.dump(seed, open(wf, "w"), indent=1) + print(f"\napplied {len(moved)} nudges — rerun sylla-collect.py + word-times.py --fa (and relaunch SyllaWizard):") + for m in moved: print(" " + m) diff --git a/pop/sailor-song/bin/chart.mjs b/pop/sailor-song/bin/chart.mjs new file mode 100644 index 0000000000..6aaefeb9ef --- /dev/null +++ b/pop/sailor-song/bin/chart.mjs @@ -0,0 +1,74 @@ +#!/usr/bin/env node +// chart.mjs — emit c/sailor-chart.h from the analysis, the loner deal: +// the Python/Node side owns measuring her, the C engine owns the score. +// +// measures.json → bars: start, length, her 4–5 beats (snapped to her +// strums), the chord she held +// vox-notes.json → her tuned notes (aesthetivox segmentation), onsets +// moved through lock-vox's time map so they match the +// locked stems the engine plays +// +// node pop/sailor-song/bin/chart.mjs + +import { readFileSync, writeFileSync, existsSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const LANE = resolve(HERE, ".."); +// regularized clock (bin/regularize.mjs) when present: measures.reg.json + reg/timemap.txt +const RAW = process.env.SAILOR_CLOCK === "raw"; // v12: her take as played — no lock, no regularize, no cut +const REG = !RAW && existsSync(resolve(LANE, "measures.reg.json")) && existsSync(resolve(LANE, "src/vox/reg/timemap.txt")); +const CUT = REG && existsSync(resolve(LANE, "measures.cut.json")) && existsSync(resolve(LANE, "src/vox/cut/segmap.txt")); // bin/splice.mjs +const M = JSON.parse(readFileSync(resolve(LANE, CUT ? "measures.cut.json" : REG ? "measures.reg.json" : "measures.json"), "utf8")); +const V = JSON.parse(readFileSync(resolve(LANE, "vox-notes.json"), "utf8")).notes; +const A = JSON.parse(readFileSync(resolve(LANE, "take.analysis.json"), "utf8")); + +// lock-vox time map (sample pairs) → move note onsets with the stems +const mapPath = resolve(LANE, "src/vox/locked/timemap.txt"); +const map = existsSync(mapPath) + ? readFileSync(mapPath, "utf8").trim().split("\n").map((l) => l.split(" ").map((x) => Number(x) / 48000)) + : [[0, 0], [1e9, 1e9]]; +const through = (map) => (t) => { + let k = map.findIndex(([s]) => s > t); if (k <= 0) k = 1; + const [s0, d0] = map[k - 1], [s1, d1] = map[k]; + return d0 + ((t - s0) / (s1 - s0)) * (d1 - d0); +}; +const lockWarp = RAW ? (t) => t : through(map); +const regMap = REG ? readFileSync(resolve(LANE, "src/vox/reg/timemap.txt"), "utf8").trim().split("\n").map((l) => l.split(" ").map((x) => Number(x) / 48000)) : null; +const segs = CUT ? readFileSync(resolve(LANE, "src/vox/cut/segmap.txt"), "utf8").trim().split("\n").map((l) => l.split(" ").map(Number)) : []; +const cutOf = (t) => { for (const [a, b, o] of segs) if (t >= a && t < b) return t - a + o; return null; }; +const regWarp = REG ? (t) => through(regMap)(lockWarp(t)) : lockWarp; +const warp = CUT ? (t) => cutOf(regWarp(t)) : regWarp; // notes: sung → locked → regularized → cut (null inside a cut) + +const CH = { "G#m": 0, Emaj7: 1, B: 2 }; +const f = (x) => x.toFixed(4); +let h = `// sailor-chart.h — GENERATED by bin/chart.mjs; do not edit. +// Her clock and her melody. Bars are anchored on her strum motif +// (bin/measures.py); beats are snapped to her strums. Notes are her tuned +// line (bin/aesthetivox.py), onsets moved through the lock-vox map so they +// sit where the locked stems sing them. Pitches are MIDI in the guitar's +// frame; the engine adds CHART_TUNE semitones (+${A.tuningCents}¢ over A440). +#pragma once + +#define CHART_TUNE ${(A.tuningCents / 100).toFixed(3)} +#define CHORD_GSM 0 +#define CHORD_EMAJ7 1 +#define CHORD_B 2 + +typedef struct { int n; double t, dur; int chord, nb; double beats[7]; } ChartBar; +typedef struct { double t, dur; int midi; } ChartNote; + +#define CHART_NBARS ${M.bars.length} +static const ChartBar CHART_BARS[CHART_NBARS] = { +`; +for (const b of M.bars) { + const beats = b.beats.slice(0, 7).map(f).join(", "); + h += ` { ${b.n}, ${f(b.t)}, ${f(b.dur)}, ${CH[b.chord] ?? 0}, ${b.beats.length - 1}, { ${beats} } },\n`; +} +const NOTES = V.map((v) => ({ t: warp(v.t), e: warp(v.t + v.dur), midi: v.midi })).filter((n) => n.t !== null && n.e !== null && n.e > n.t).sort((a, b) => a.t - b.t); +h += `};\n\n#define CHART_NNOTES ${NOTES.length}\nstatic const ChartNote CHART_NOTES[CHART_NNOTES] = {\n`; +for (const n of NOTES) h += ` { ${f(n.t)}, ${f(n.e - n.t)}, ${n.midi} },\n`; +h += `};\n`; +writeFileSync(resolve(LANE, "c/sailor-chart.h"), h); +console.log(`✓ c/sailor-chart.h — ${M.bars.length} bars, ${NOTES.length} notes${CUT ? " (cut chart)" : ""}`); diff --git a/pop/sailor-song/bin/choir.py b/pop/sailor-song/bin/choir.py new file mode 100644 index 0000000000..899bc4c1ec --- /dev/null +++ b/pop/sailor-song/bin/choir.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""choir.py — accompaniment voices behind her, two ways. + + SAGE CHOIR her own frequency profile: WORLD (harvest → cheaptrick → d4c) + on the regularized lead, then RESYNTHESIS with her spectral + envelope and aperiodicity per frame but the f0 replaced by a + chord tone held machine-steady (a little vibrato). Her vowels + and consonant air follow her line; the pitch is the chord. + Three voices, low / mid / high, below her register mostly, + each only sounding where she is voiced. A vocoder choir of her. + JEFFREY pop/voice/lab/jeffrey-pvc/*-iso.mp3 — @jeffrey's isolated + sustained vowels (ah / iy / uw). WORLD-analysed once, the frames + looped, resynthesized on the bar's root (and fifth) two octaves + under her: a computer drone that changes vowel with the chord + (ah on G#m, uw on B, iy on Emaj7). + +Outputs, in the regularized clock (src/vox/reg/): + choir-low.wav choir-mid.wav choir-high.wav jeffrey-root.wav jeffrey-fifth.wav + + choir.json (per-bar tones) + + pop/.venv/bin/python pop/sailor-song/bin/choir.py [--vib 0.10] +""" +import argparse, json, os, sys, warnings +import numpy as np, soundfile as sf +with warnings.catch_warnings(): + warnings.simplefilter("ignore"); import pyworld as pw +import librosa + +ap = argparse.ArgumentParser() +ap.add_argument("--vib", type=float, default=0.10) # semitones of vibrato depth +ap.add_argument("--vib-hz", type=float, default=5.2) +ap.add_argument("--stems", default="reg") # reg (regularized clock) or raw (her take as played) +a = ap.parse_args() + +LANE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +ROOT = os.path.dirname(os.path.dirname(LANE)) +REG = os.path.join(LANE, "src/vox", a.stems) +M = json.load(open(os.path.join(LANE, "measures.reg.json" if a.stems == "reg" else "measures.json")))["bars"] +A = json.load(open(os.path.join(LANE, "take.analysis.json"))) +TUNE = A["tuningCents"] / 100.0 +FP = 5.0 # ms + +def mtof(m): return 440.0 * 2 ** ((m + TUNE - 69) / 12) + +# chord tones per bar: pcs in the guitar frame (G#m = 8 11 3, B = 11 3 6, Emaj7 sounds G#m here) +PCS = {"G#m": [8, 11, 3], "Emaj7": [8, 11, 3, 4], "B": [11, 3, 6]} +ROOTPC = {"G#m": 8, "Emaj7": 8, "B": 11} +VOWEL = {"G#m": "ah", "Emaj7": "iy", "B": "uw"} + +def nearest(pcs, target, lo, hi): + c = [m for m in range(lo, hi + 1) if m % 12 in pcs] + return min(c, key=lambda m: abs(m - target)) + +# ── SAGE CHOIR ─────────────────────────────────────────────────────────── +x, fs = sf.read(os.path.join(REG, "vocals-aesthetivox.wav")) +if x.ndim > 1: x = x.mean(1) +x = x.astype(np.float64) +print("WORLD on her lead …", flush=True) +f0, t = pw.harvest(x, fs, f0_floor=150, f0_ceil=900, frame_period=FP) +f0 = pw.stonemask(x, f0, t, fs) +fft = pw.get_cheaptrick_fft_size(fs, f0_floor=150.0) +sp = pw.cheaptrick(x, f0, t, fs, fft_size=fft, f0_floor=150.0) +apr = pw.d4c(x, f0, t, fs, fft_size=fft) +voiced = f0 > 0 +# require ≥ 60 ms voiced runs (bleed blips out) +runs = np.diff(np.concatenate([[0], voiced.astype(int), [0]])) +for s0, s1 in zip(np.where(runs == 1)[0], np.where(runs == -1)[0]): + if (s1 - s0) * FP < 60: voiced[s0:s1] = False +nf = len(t) +bar_of = np.zeros(nf, int) - 1 +for i, b in enumerate(M): + a0, a1 = int(b["t"] * 1000 / FP), int((b["t"] + b["dur"]) * 1000 / FP) + bar_of[a0:a1] = i + +# voice leading: three voices, nearest chord tone per bar, no doubling, kept below her (she sits 56–68) +voices = {"low": 44, "mid": 49, "high": 53} +ranges = {"low": (41, 51), "mid": (46, 56), "high": (51, 60)} +tones = {k: np.zeros(len(M), int) for k in voices} +cur = dict(voices) +for i, b in enumerate(M): + used = set() + for k in ("low", "mid", "high"): + pcs = [p for p in PCS[b["chord"]] if p not in used] or PCS[b["chord"]] + m = nearest(pcs, cur[k], *ranges[k]); cur[k] = m; tones[k][i] = m; used.add(m % 12) + +def vibrato(n, depth_st, hz): + tt = np.arange(n) * FP / 1000 + return 2 ** (depth_st * np.sin(2 * np.pi * hz * tt + 0.7) / 12) + +vib = vibrato(nf, a.vib, a.vib_hz) +out_tones = {} +for k in voices: + f = np.zeros(nf) + for i in range(nf): + if voiced[i] and bar_of[i] >= 0: f[i] = mtof(tones[k][bar_of[i]]) * vib[i] + # glide between bars over 60 ms so the tone changes don't click + fm = f.copy(); k60 = 12 + for i in range(1, nf): + if f[i] > 0 and fm[i - 1] > 0: fm[i] = fm[i - 1] + (f[i] - fm[i - 1]) / k60 + print(f" choir {k}: synthesizing …", flush=True) + y = pw.synthesize(np.ascontiguousarray(fm), sp, apr, fs, frame_period=FP) + y = y[:len(x)] + # gate to her voiced regions with 20 ms fades (WORLD hums through gaps otherwise) + g = np.repeat(voiced.astype(float), int(fs * FP / 1000))[:len(y)] + g = np.convolve(g, np.ones(int(0.08 * fs)) / int(0.08 * fs), mode="same") # v12: 80 ms swells, not 20 ms chops + y = y * g + y *= 0.5 / (np.max(np.abs(y)) + 1e-9) + sf.write(os.path.join(REG, f"choir-{k}.wav"), y.astype(np.float32), fs) + out_tones[k] = tones[k].tolist() + +# ── JEFFREY vowel drone ────────────────────────────────────────────────── +LAB = os.path.join(ROOT, "pop/voice/lab/jeffrey-pvc") +vow = {} +for v in ("ah", "iy", "uw"): + p = os.path.join(LAB, f"jeffrey-pvc-{v}-iso.mp3") + if not os.path.exists(p): print(f" ! missing {p}"); continue + z, zfs = librosa.load(p, sr=fs, mono=True) + z = z.astype(np.float64) + # trim silence, keep the steady middle + zi = np.where(np.abs(z) > 0.02)[0] + if len(zi) < fs // 4: continue + z = z[zi[0]:zi[-1]]; z = z[len(z) // 5: -len(z) // 5] + zf0, zt = pw.harvest(z, fs, f0_floor=60, f0_ceil=400, frame_period=FP) + zf0 = pw.stonemask(z, zf0, zt, fs) + zfft = pw.get_cheaptrick_fft_size(fs, f0_floor=60.0) + zsp = pw.cheaptrick(z, zf0, zt, fs, fft_size=zfft, f0_floor=60.0) + zap = pw.d4c(z, zf0, zt, fs, fft_size=zfft) + keep = zf0 > 0 + vow[v] = (zsp[keep], zap[keep]) + print(f" jeffrey {v}: {keep.sum()} voiced frames at ~{np.median(zf0[keep]):.0f} Hz") +if vow: + total = int(M[-1]["t"] + M[-1]["dur"]) + 2 + nfj = int(total * 1000 / FP) + fftj = next(iter(vow.values()))[0].shape[1] + for name, interval in (("root", 0), ("fifth", 7)): + spj = np.zeros((nfj, fftj)); apj = np.ones((nfj, fftj)); fj = np.zeros(nfj) + vibj = vibrato(nfj, 0.12, 4.0) + for i, b in enumerate(M): + a0, a1 = int(b["t"] * 1000 / FP), min(nfj, int((b["t"] + b["dur"]) * 1000 / FP)) + v = VOWEL[b["chord"]] if VOWEL[b["chord"]] in vow else next(iter(vow)) + vs, va = vow[v]; n = len(vs) + midi = 32 + ((ROOTPC[b["chord"]] - 8) % 12) + interval # G#1 / B1 root, fifth above + if midi > 39: midi -= 12 + for k, fr in enumerate(range(a0, a1)): + j = (k // 2) % n if n else 0 # half-speed loop of the vowel's frames + spj[fr] = vs[j]; apj[fr] = va[j]; fj[fr] = mtof(midi) * vibj[fr] + # 80 ms glide at bar lines + for i in range(1, nfj): + if fj[i] > 0 and fj[i - 1] > 0: fj[i] = fj[i - 1] + (fj[i] - fj[i - 1]) / 16 + print(f" jeffrey {name}: synthesizing …", flush=True) + y = pw.synthesize(np.ascontiguousarray(fj), np.ascontiguousarray(spj), np.ascontiguousarray(apj), fs, frame_period=FP) + y *= 0.5 / (np.max(np.abs(y)) + 1e-9) + sf.write(os.path.join(REG, f"jeffrey-{name}.wav"), y.astype(np.float32), fs) +json.dump({"frameMs": FP, "tones": out_tones, "vowels": VOWEL, "jeffrey": sorted(vow)}, open(os.path.join(LANE, "choir.json"), "w"), indent=1) +print("✓", REG) diff --git a/pop/sailor-song/bin/cover.py b/pop/sailor-song/bin/cover.py new file mode 100644 index 0000000000..611877882e --- /dev/null +++ b/pop/sailor-song/bin/cover.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +"""cover.py — the Sailor Song cover: one still from the take, mastered. + +The source is a 960×540 iPhone HLG (bt2020) clip. The frame is read as +16-bit, taken through the HLG inverse OETF + OOTF, bt2020→bt709, and a +soft Reinhard shoulder (exposure 2.6), then graded for the remix (v8: room +white-balanced near neutral, shirt pushed deep red and the LED string lit +violet through feathered hue masks, hard S, blacks down), upscaled locally +(lanczos + mild unsharp; nothing leaves this machine) and grained to +hide the upscale. + + pop/.venv/bin/python pop/sailor-song/bin/cover.py [--t 11.583] [--x 110] + → cover/sailor-song-cover.jpg (clean) + cover/sailor-song-cover-title.jpg +""" +import argparse, os, subprocess +import numpy as np +from PIL import Image, ImageDraw, ImageFilter, ImageFont +import scipy.ndimage as nd + +ap = argparse.ArgumentParser() +ap.add_argument("--t", type=float, default=107.15) # v6.2: eyes open at the lens, mouth open on a held note (chorus 2) +ap.add_argument("--x", type=int, default=80) # square crop left edge (of 960) +ap.add_argument("--size", type=int, default=3000) +ap.add_argument("--debug", default="") # v8: dump the grade masks here +a = ap.parse_args() +LANE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +OUT = os.path.join(LANE, "cover"); os.makedirs(OUT, exist_ok=True) +W, H = 960, 540 + +raw = subprocess.run(["ffmpeg", "-v", "error", "-ss", str(a.t), "-i", os.path.expanduser("~/Desktop/IMG_8699.mov"), + "-vf", "scale=in_color_matrix=bt2020:in_range=tv:out_range=pc,format=rgb48le", + "-frames:v", "1", "-f", "rawvideo", "-"], capture_output=True, check=True).stdout +im = np.frombuffer(raw, dtype="hwd", E, M), 0, None) * 2.6 +L = np.clip(L / (1 + L / 2.2) * (1 + 1 / 2.2), 0, 1) # linear, 0..1 + +# ── crop + upscale (in linear light) ──────────────────────────────────── +L = L[:, a.x:a.x + H] +chans = [] +for c in range(3): + ch = Image.fromarray((L[..., c] ** (1 / 2.2) * 255).astype(np.float32)) + chans.append(np.asarray(ch.resize((a.size, a.size), Image.LANCZOS))) +g = np.clip(np.stack(chans, -1) / 255.0, 0, 1) ** 2.2 # back to linear + +# ── v7: remove the ceiling light (inpaint from the ceiling around it) ─── +# lamp centre / radii in the 540-square, scaled with the output +cy, cx, ry, rx = 0.07 * a.size, 0.43 * a.size, 0.10 * a.size, 0.125 * a.size +yy, xx = np.mgrid[0:a.size, 0:a.size] +d = ((yy - cy) / ry) ** 2 + ((xx - cx) / rx) ** 2 +hole = np.clip((1.25 - d) / 0.35, 0, 1) # 1 inside, feathered edge +keep = 1 - hole +sig = 0.05 * a.size +fill = np.stack([nd.gaussian_filter(g[..., c] * keep, sig) for c in range(3)], -1) / (nd.gaussian_filter(keep, sig)[..., None] + 1e-6) +fill += (np.random.default_rng(1).standard_normal(fill.shape) * 0.006) # a breath of texture so it is not plastic +g = g * (1 - hole[..., None]) + fill * hole[..., None] + +# ── grade ─────────────────────────────────────────────────────────────── +s = a.size / 540 +lum = np.einsum("hwc,c->hw", g, np.array([0.2126, 0.7152, 0.0722])) +glow = nd.gaussian_filter(np.clip(g - 0.55, 0, None), sigma=(18 * s, 18 * s, 0)) +g = 1 - (1 - g) * (1 - 0.55 * glow) # highlight bloom (screen) +# ── v8 grade: neutral room, targeted shirt / wood / LED / skin pushes (replaces +# the v7 global warm multiply and the "redness" lift that pulled skin along) ── +def _hsv(x): + """hue (deg), sat, val of a gamma-space rgb array.""" + mx = x.max(-1); mn = x.min(-1); dl = mx - mn + 1e-6 + r_, g_, b_ = x[..., 0], x[..., 1], x[..., 2] + h = np.where(mx == r_, (g_ - b_) / dl, np.where(mx == g_, 2 + (b_ - r_) / dl, 4 + (r_ - g_) / dl)) * 60 % 360 + return h, dl / (mx + 1e-6), mx +def _ramp(x, lo, hi): return np.clip((x - lo) / (hi - lo), 0, 1) +def _hue_win(h, c, half, soft): + """1 within ±half° of hue c, feathered to 0 over `soft` degrees outside.""" + dh = np.abs((h - c + 180) % 360 - 180) + return 1 - _ramp(dh, half, half + soft) +def _luma(x): return np.einsum("hwc,c->hw", x, np.array([0.2126, 0.7152, 0.0722]))[..., None] +def _poly(pts, feather): + """soft 0..1 region from fractional polygon points.""" + pm = Image.new("L", (a.size, a.size), 0) + ImageDraw.Draw(pm).polygon([(x * a.size, y * a.size) for x, y in pts], fill=255) + return nd.gaussian_filter(np.asarray(pm).astype(np.float64) / 255, feather * a.size) + +# v8.1 white balance on the right ceiling patch (the left is clipped to R=G=1 +# and useless as a reference). 75 % of full neutral: the room reads white, +# the skin keeps a little of the lamp. +ref = g[int(0.08 * a.size):int(0.16 * a.size), int(0.78 * a.size):int(0.95 * a.size)].reshape(-1, 3).mean(0) +wb = (ref.mean() / ref) ** 0.75 # gains in linear light +g = g * wb +g = _luma(g) + (g - _luma(g)) * 1.20 # v8: moderate global sat (was 1.35) + +gam = np.clip(g, 0, 1) ** (1 / 2.2) +hue, sat, val = _hsv(gam) +fy = yy / a.size + +# v8.2 the guitar body: a soft polygon around the wood (this frame is fixed, so +# geometry is fair), gated by hue/chroma so strings and the black fretboard +# stay put. In the neutral frame the wood reads pink-brown (hue ~340, sat .27); +# this is where its orange comes from now, instead of from the whole image. +wood = _poly([(0.25, 0.63), (0.30, 0.60), (0.39, 0.585), (0.50, 0.55), (0.58, 0.56), (0.64, 0.60), (0.68, 0.70), + (0.72, 0.82), (0.70, 1.0), (0.06, 1.0), (0.06, 0.90), (0.12, 0.80), (0.22, 0.72)], 0.006) +wood *= _hue_win(hue, 350, 45, 15) * _ramp(sat, 0.10, 0.25) * _ramp(val, 0.20, 0.35) +wood = nd.gaussian_filter(wood, 0.003 * a.size) +mw = wood[..., None] +g = g * (1 + mw * np.array([0.15, 0.0, -0.50])) # pink-brown → orange +g = _luma(g) + (g - _luma(g)) * (1 + 0.35 * mw) + +# v8.3 the shirt: red-magenta hue at high chroma (chest sat .55-.73; skin sits +# at hue 0-15 sat .25, the wood at sat .27, both drop out), below the face, +# never inside the wood polygon. +shirt = _hue_win(hue, 338, 24, 10) * _ramp(sat, 0.42, 0.56) * _ramp(val, 0.06, 0.16) * _ramp(fy, 0.30, 0.40) +shirt *= 1 - _poly([(0.25, 0.63), (0.30, 0.60), (0.39, 0.585), (0.50, 0.55), (0.58, 0.56), (0.64, 0.60), (0.68, 0.70), + (0.72, 0.82), (0.70, 1.0), (0.06, 1.0), (0.06, 0.90), (0.12, 0.80), (0.22, 0.72)], 0.004) +shirt = nd.gaussian_filter(shirt, 0.004 * a.size) +m = shirt[..., None] +g = _luma(g) + (g - _luma(g)) * (1 + 0.65 * m) # richer +g = g * (1 - m * np.array([0.0, 0.10, 0.32])) # magenta → red (blue down) +g = g * (1 - 0.14 * m) # deeper + +# v8.4 the purple LED string along the wall: violet, bright, on the wall band; +# saturated + lifted, with a soft violet glow bled around each point. +led = _hue_win(hue, 285, 35, 10) * _ramp(sat, 0.15, 0.28) * _ramp(val, 0.35, 0.55) * _ramp(fy, 0.22, 0.27) * (1 - _ramp(fy, 0.49, 0.54)) +led = np.clip(nd.gaussian_filter(led, 0.0025 * a.size) * 1.6, 0, 1) # each point becomes a bulb +ml = led[..., None] +g = _luma(g) + (g - _luma(g)) * (1 + 1.8 * ml) +g = g * (1 + 0.9 * ml) +violet = np.array([0.50, 0.10, 1.0]) * nd.gaussian_filter(led * val, 0.011 * a.size)[..., None] * 1.4 +g = 1 - (1 - np.clip(g, 0, 1)) * (1 - np.clip(violet, 0, 1)) # glow, screened + +# v8.5 skin: a whisper of warmth back into mid-chroma, mid-bright skin hues +# only (the white balance took the lamp off it), not the shirt, not the wood. +skin = _hue_win(hue, 12, 16, 8) * _ramp(sat, 0.14, 0.26) * _ramp(val, 0.40, 0.55) * (1 - shirt) * (1 - wood) +skin = nd.gaussian_filter(skin, 0.005 * a.size) +ms = skin[..., None] +g = g * (1 + ms * np.array([0.04, 0.0, -0.10])) +g = _luma(g) + (g - _luma(g)) * (1 + 0.10 * ms) +if a.debug: + for nm, mk in [("shirt", shirt), ("led", led), ("wood", wood), ("skin", skin)]: + Image.fromarray((np.clip(mk, 0, 1) * 255).astype(np.uint8)).resize((450, 450)).save(os.path.join(a.debug, f"mask-{nm}.png")) + print("wb gains", wb.round(3), "ref", ref.round(3)) +out = np.clip(g, 0, 1) ** (1 / 2.2) +out = 0.02 + 0.96 * out # v7: blacks down +out = out + 0.16 * np.sin(np.pi * out) * (out - 0.5) # v7: a hard S +out = np.clip(out, 0, 1) ** 1.12 # v7: darker mids +# mild unsharp on the upscale, then grain +blur = nd.gaussian_filter(out, sigma=(1.6 * s / 5.5, 1.6 * s / 5.5, 0)) +out = out + 0.75 * (out - blur) # v7: crisp +rng = np.random.default_rng(8699) +grain = nd.gaussian_filter(rng.standard_normal(out.shape[:2]), 1.1) * 0.022 +out = np.clip(out + grain[..., None], 0, 1) +# vignette +yy, xx = np.mgrid[0:a.size, 0:a.size] / a.size - 0.5 +out *= (1 - 0.22 * np.clip((xx ** 2 + yy ** 2) * 2.2, 0, 1) ** 1.5)[..., None] + +img = Image.fromarray((np.clip(out, 0, 1) * 255 + 0.5).astype(np.uint8)) +# ── v7: a jpeg decode glitch — a few bands re-decoded at quality 14 with the +# 8×8 block rows slid sideways and the chroma torn off by a block or two +import io +buf = io.BytesIO(); img.save(buf, format="JPEG", quality=14, subsampling=2); buf.seek(0) +low = np.asarray(Image.open(buf).convert("RGB")).copy() +arr = np.asarray(img).copy() +grng = np.random.default_rng(8699) +for y0, h, shift, cshift in [(int(a.size * f), int(a.size * hh), sh, cs) for f, hh, sh, cs in + [(0.31, 0.018, 24, 8), (0.47, 0.010, -16, 16), (0.585, 0.026, 40, -8), (0.73, 0.008, -8, 24), (0.86, 0.014, 56, 0)]]: + y0 -= y0 % 8; h = max(8, h - h % 8) + band = np.roll(low[y0:y0 + h], shift, axis=1) + band[..., 0] = np.roll(band[..., 0], cshift, axis=1) # red torn sideways + arr[y0:y0 + h] = band +img = Image.fromarray(arr) +img.save(os.path.join(OUT, "sailor-song-cover.jpg"), quality=92, subsampling=0) + +# ── titled variant ────────────────────────────────────────────────────── +t = img.copy().convert("RGBA") +layer = Image.new("RGBA", t.size, (0, 0, 0, 0)) +d = ImageDraw.Draw(layer) +font = ImageFont.truetype("/System/Library/Fonts/NewYorkItalic.ttf", int(a.size * 0.045)) +text = "sailor song" +pad = int(a.size * 0.05) +bb = d.textbbox((0, 0), text, font=font) +pos = (pad, a.size - pad - (bb[3] - bb[1]) - bb[1]) +shadow = Image.new("RGBA", t.size, (0, 0, 0, 0)) +ImageDraw.Draw(shadow).text(pos, text, font=font, fill=(40, 20, 10, 150)) +shadow = shadow.filter(ImageFilter.GaussianBlur(a.size * 0.004)) +d.text(pos, text, font=font, fill=(255, 246, 236, 235)) +t = Image.alpha_composite(Image.alpha_composite(t, shadow), layer).convert("RGB") +t.save(os.path.join(OUT, "sailor-song-cover-title.jpg"), quality=92, subsampling=0) +print("✓", OUT) diff --git a/pop/sailor-song/bin/forced-align.py b/pop/sailor-song/bin/forced-align.py new file mode 100644 index 0000000000..bea4b70e1f --- /dev/null +++ b/pop/sailor-song/bin/forced-align.py @@ -0,0 +1,71 @@ +"""forced-align.py — sung-word boundaries by forced alignment, not recognition. + +Whisper GUESSES the words and times them as a side effect: whisper.cpp's -ml 1 +splits words into syllable tokens (the labels slide), and whisper-1's word +times drift by half a second on held notes. We already know the lyric, so this +asks a different question: given these exact words, where in her audio does +each one sit? torchaudio's MMS forced aligner (wav2vec2, character CTC) is run +over the whole dry stem with the lyric as the transcript and a token +between lines to soak up guitar bleed and breaths. + +Runs in its own venv (torch is not in pop/.venv): + uv venv -p 3.12 pop/.venv-align && uv pip install -p pop/.venv-align/bin/python torch torchaudio soundfile numpy + pop/.venv-align/bin/python pop/sailor-song/bin/forced-align.py + → src/.word-times/forced-align.json (take seconds; word-times.py --fa reads it) +""" +import json, os, re +import numpy as np, soundfile as sf, torch, torchaudio +from torchaudio.pipelines import MMS_FA as bundle + +LANE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SRC = os.path.join(LANE, "src") +DRY = os.path.join(SRC, "vox", "vocals-dry-48k.wav") +OUT = os.path.join(SRC, ".word-times", "forced-align.json") + +lines = [l.strip() for l in open(os.path.join(SRC, "lyrics-sung.txt")) if l.strip() and not l.startswith("[")] +words = [w for l in lines for w in l.split()] +DICT = bundle.get_dict(star="*") +norm = lambda w: "".join(c for c in w.lower().replace("’", "'") if c in DICT and c != "*") + +x, sr = sf.read(DRY, dtype="float32", always_2d=True) +wav = torch.from_numpy(x.mean(1))[None] +wav = torchaudio.functional.resample(wav, sr, bundle.sample_rate) +model = bundle.get_model(with_star=True).eval() +with torch.inference_mode(): + # the stem in 30 s windows (overlap 2 s) so memory stays small; emissions are stitched on the frame grid + S, hop = bundle.sample_rate * 30, bundle.sample_rate * 28 + parts, fps = [], None + for a in range(0, wav.shape[1], hop): + e, _ = model(wav[:, a:a + S]) + fps = fps or e.shape[1] / (wav[:, a:a + S].shape[1] / bundle.sample_rate) + # windows overlap by 2 s: each keeps [a + 1 s, a + 29 s) of its own frames (the first from 0, + # the last to its end), so the pieces butt together with no frame lost or doubled + lo = 0 if a == 0 else round(fps) + hi = e.shape[1] if a + S >= wav.shape[1] else round(29 * fps) + keep = e[0][lo:hi] + parts.append(keep) + emission = torch.cat(parts)[None] +frame_s = wav.shape[1] / bundle.sample_rate / emission.shape[1] + +# transcript: every word, and a between lines (guitar-only bars, breaths) +tokens, word_of = [[DICT["*"]]], [None] # the guitar intro before her first word +for li, l in enumerate(lines): + if li: tokens.append([DICT["*"]]); word_of.append(None) + for w in l.split(): + n = norm(w) + tokens.append([DICT[c] for c in n] if n else [DICT["*"]]); word_of.append(w) +tokens.append([DICT["*"]]); word_of.append(None) # and the outro after her last +flat = [t for ws in tokens for t in ws] +aligned, scores = torchaudio.functional.forced_align(emission, torch.tensor([flat], dtype=torch.int32), blank=0) +spans = torchaudio.functional.merge_tokens(aligned[0], scores[0].exp()) +out, k = [], 0 +for ws, w in zip(tokens, word_of): + sp = spans[k:k + len(ws)]; k += len(ws) + if w is None: continue + out.append({"text": w, "from": round(sp[0].start * frame_s, 3), "to": round(sp[-1].end * frame_s, 3), + "score": round(float(np.mean([s.score for s in sp])), 3), + "tokens": [{"text": w, "from": round(sp[0].start * frame_s, 3), "to": round(sp[-1].end * frame_s, 3)}]}) +json.dump(out, open(OUT, "w"), indent=1) +low = sorted(out, key=lambda w: w["score"])[:8] +print(f"{len(out)} words aligned ({frame_s * 1000:.0f} ms frames) → {OUT}") +print("least sure:", ", ".join(f"{w['text']}@{w['from']:.2f}({w['score']:.2f})" for w in low)) diff --git a/pop/sailor-song/bin/lock-vox.mjs b/pop/sailor-song/bin/lock-vox.mjs new file mode 100644 index 0000000000..56417ad9b2 --- /dev/null +++ b/pop/sailor-song/bin/lock-vox.mjs @@ -0,0 +1,84 @@ +#!/usr/bin/env node +// lock-vox.mjs — pull her sung onsets onto her own strum grid. +// +// The lane FOLLOWS her guitar, so the grid is hers (measures.json). Her +// voice floats a little off it: note onsets sit a median 41 ms from the +// nearest 8th (p90 105 ms, slightly late). This builds one smooth time map +// — every tuned-note onset (vox-notes.json) moved STRENGTH of the way to +// its nearest 8th, never more than MAX_MS — and runs the SAME map through +// rubberband (R3, formant-preserving, -M timemap) on the lead, the halo and +// every harmony stem, so the stack stays one voice. Timing only; pitch is +// untouched. +// +// node pop/sailor-song/bin/lock-vox.mjs [--strength 0.8] [--max-ms 100] +// → src/vox/locked/*.wav (render.mjs prefers these when present) + +import { readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const LANE = resolve(HERE, ".."); +const VOX = resolve(LANE, "src/vox"); +const OUT = resolve(VOX, "locked"); +mkdirSync(OUT, { recursive: true }); +const SR = 48_000; + +const flags = {}; +for (let i = 2; i < process.argv.length; i++) { + const a = process.argv[i]; + if (a.startsWith("--")) flags[a.slice(2)] = process.argv[++i]; +} +const STRENGTH = Number(flags.strength ?? 0.6); // v6.1: was 0.8 — keep more of her placement (ANALYSIS.md §2) +const MAX = Number(flags["max-ms"] ?? 70) / 1000; // v6.1: was 100 +// v6.1: no note may be squeezed or stretched past these local ratios. Before +// this clamp adjacent anchors moving opposite ways compressed a held note at +// 79.5 s to 0.20 of its length and 21 more by 30–40% ("true is really short"). +const RATIO_MIN = Number(flags["ratio-min"] ?? 0.85), RATIO_MAX = Number(flags["ratio-max"] ?? 1.15); +const MIN_GAP = 0.04; + +const M = JSON.parse(readFileSync(resolve(LANE, "measures.json"), "utf8")); +const V = JSON.parse(readFileSync(resolve(LANE, "vox-notes.json"), "utf8")).notes; +const beats = M.bars.flatMap((b) => b.beats.slice(0, -1)); +const g8 = []; +for (let i = 0; i < beats.length - 1; i++) g8.push(beats[i], (beats[i] + beats[i + 1]) / 2); +const near = (t) => g8.reduce((p, c) => (Math.abs(c - t) < Math.abs(p - t) ? c : p)); + +// anchors: (source time → locked time), monotonic, spaced +const anchors = [[0, 0]]; +let moved = []; +for (const v of V) { + const d = near(v.t) - v.t; + const shift = Math.max(-MAX, Math.min(MAX, d * STRENGTH)); + const src = v.t; let dst = v.t + shift; + const [ps, pd] = anchors.at(-1); + dst = Math.max(pd + RATIO_MIN * (src - ps), Math.min(pd + RATIO_MAX * (src - ps), dst)); // local ratio clamp + if (src - ps < MIN_GAP || dst - pd < MIN_GAP) continue; // keep it monotonic + smooth + anchors.push([src, dst]); + moved.push(Math.abs(d) * 1000); +} +const dur = 179.14; +{ const [ps, pd] = anchors.at(-1); anchors.push([dur, Math.max(pd + RATIO_MIN * (dur - ps), Math.min(pd + RATIO_MAX * (dur - ps), dur))]); } +{ const r = anchors.slice(1).map(([s, d], i) => (d - anchors[i][1]) / (s - anchors[i][0])); + console.log(`local ratio ${Math.min(...r).toFixed(3)}–${Math.max(...r).toFixed(3)} (clamped to ${RATIO_MIN}–${RATIO_MAX})`); } +const mapPath = resolve(OUT, "timemap.txt"); +writeFileSync(mapPath, anchors.map(([s, d]) => `${Math.round(s * SR)} ${Math.round(d * SR)}`).join("\n") + "\n"); + +// before/after check: onset distance to the 8th grid +const after = V.map((v) => { + let k = anchors.findIndex(([s]) => s > v.t); if (k <= 0) k = 1; + const [s0, d0] = anchors[k - 1], [s1, d1] = anchors[k]; + const t = d0 + ((v.t - s0) / (s1 - s0)) * (d1 - d0); + return Math.abs(t - near(t)) * 1000; +}).sort((a, b) => a - b); +const before = V.map((v) => Math.abs(v.t - near(v.t)) * 1000).sort((a, b) => a - b); +const med = (a) => a[a.length >> 1].toFixed(0), p90 = (a) => a[Math.floor(0.9 * (a.length - 1))].toFixed(0); +console.log(`onsets vs 8th grid: median ${med(before)} → ${med(after)} ms, p90 ${p90(before)} → ${p90(after)} ms (${anchors.length - 2} anchors)`); + +const stems = readdirSync(VOX).filter((f) => /^(vocals-aesthetivox|vocals-halo|harm-.*)\.wav$/.test(f)); +for (const f of stems) { + const r = spawnSync("rubberband", ["-3", "-F", "-q", "-D", String(dur), "-M", mapPath, resolve(VOX, f), resolve(OUT, f)], { encoding: "utf8" }); + if (r.status !== 0) { console.error(r.stderr); process.exit(1); } + console.log(` locked ${f}`); +} diff --git a/pop/sailor-song/bin/lyric-video.py b/pop/sailor-song/bin/lyric-video.py new file mode 100644 index 0000000000..3d7b7d3206 --- /dev/null +++ b/pop/sailor-song/bin/lyric-video.py @@ -0,0 +1,371 @@ +#!/usr/bin/env python3 +# lyric-video.py — the singalong MP4 for the sailor-song remix: cover +# blurred and darkened behind, a small square of it up top, the current +# line big and centred with a karaoke wipe through the word being sung, +# the next line smaller underneath. Frames are PIL (this ffmpeg has no +# drawtext / subtitles filter), piped raw into x264, then muxed with the +# master. +# +# Everything lyrical stays under src/ (gitignored): the lyric as she +# sings it (src/lyrics-sung.txt) and the aligned word times +# (src/words-aligned.json, whisper boundaries in TAKE time with the true +# words substituted in by pop/bin/mfa-align.mjs). The take is not the +# record: record_t = reg(lock(t)) − startSec, where lock and reg are the +# piecewise-linear sample maps under src/vox/{locked,reg}/timemap.txt +# and startSec is the record's open in out/sailor-song-v7.events.json. +# The bake can move startSec (and the maps) while this draws, so the +# receipt is read again after the render and the frames redrawn if it +# moved; a master that only changed bytes is re-muxed. +# +# pop/.venv/bin/python pop/sailor-song/bin/lyric-video.py +# [--master out/sailor-song-v7-master.wav] [--out out/sailor-song-v7-lyrics.mp4] +# [--fps 30] [--mux-only] # re-mux the last silent render with the master +# [--stills 5,40,70,120] # also write PNG frames at those record seconds + +import argparse, json, math, os, re, subprocess, sys, time +import numpy as np +from PIL import Image, ImageChops, ImageDraw, ImageFilter, ImageFont + +HERE = os.path.dirname(os.path.abspath(__file__)) +LANE = os.path.dirname(HERE) +SRC = os.path.join(LANE, "src") +OUTD = os.path.join(LANE, "out") + +ap = argparse.ArgumentParser() +import glob as _glob +_newest = sorted(_glob.glob(os.path.join(OUTD, "sailor-song-v*-master.wav")), key=os.path.getmtime) +_V = os.path.basename(_newest[-1]).split("-")[2] if _newest else "v7" # newest version's tag, e.g. v9 +ap.add_argument("--master", default=os.path.join(OUTD, f"sailor-song-{_V}-master.wav")) +ap.add_argument("--events", default=os.path.join(OUTD, f"sailor-song-{_V}.events.json")) +ap.add_argument("--words", default=os.path.join(SRC, "words-aligned.json")) +ap.add_argument("--lyrics", default=os.path.join(SRC, "lyrics-sung.txt")) +ap.add_argument("--vocal", default=os.path.join(SRC, "vox", "vocals-dry-48k.wav")) +ap.add_argument("--cover", default=os.path.join(LANE, "cover", "sailor-song-cover.jpg")) +ap.add_argument("--out", default=os.path.join(OUTD, f"sailor-song-{_V}-lyrics.mp4")) +ap.add_argument("--silent", default=None, help="video-only intermediate (default: beside --out, dot-prefixed)") +ap.add_argument("--fps", type=int, default=30) +ap.add_argument("--mux-only", action="store_true") +ap.add_argument("--stills", default=None, help="comma-separated record seconds → PNGs") +ap.add_argument("--stills-dir", default=None) +ap.add_argument("--no-render", action="store_true", help="stills / timing check only") +A = ap.parse_args() + +W, H = 1920, 1080 +FPS = A.fps +SILENT = A.silent or os.path.join(os.path.dirname(A.out), "." + os.path.basename(A.out).replace(".mp4", ".video.mp4")) + +# ── palette (warm, off the cover: cream text on a dark warm blur) ───── +CREAM = (246, 234, 214) +DIM = 0.45 # unsung words of the current line +HOT = (255, 200, 118) # the word being sung (the lamp in the cover) +NEXT_ALPHA = 0.52 +LEAD = 1.0 # s before the first word a line starts fading in +FADE = 0.5 # s fade in / out +HOLD = 2.2 # s a line lingers after its last word when nothing follows + +def font(size, name="Semibold"): + f = ImageFont.truetype("/System/Library/Fonts/NewYork.ttf", size) + try: f.set_variation_by_name(name) + except Exception: pass + return f + +# ── time base: take → locked → regularized → record ─────────────────── +LOCK_MAP = os.path.join(SRC, "vox", "locked", "timemap.txt") +REG_MAP = os.path.join(SRC, "vox", "reg", "timemap.txt") + +def timemap(path): + a = np.loadtxt(path) + return a[:, 0] / 48000.0, a[:, 1] / 48000.0 + +def clock_state(): + """What the time base depends on — compared after the render.""" + return (float(json.load(open(A.events))["startSec"]), + os.path.getmtime(LOCK_MAP), os.path.getmtime(REG_MAP)) + +# ── the vocal's envelope in take time, to firm up line onsets ──────── +ENV_SR = 100 +def vocal_env(): + r = subprocess.run(["ffmpeg", "-v", "error", "-i", A.vocal, "-ac", "1", "-ar", "8000", + "-f", "f32le", "-"], capture_output=True) + y = np.frombuffer(r.stdout, np.float32) + hop = 8000 // ENV_SR + n = len(y) // hop + rms = np.sqrt((y[:n * hop].reshape(n, hop) ** 2).mean(axis=1)) + return rms / max(1e-9, np.percentile(rms, 99)) + +# ── the lines, each carrying its words ──────────────────────────────── +HEADER_RE = re.compile(r"^\[?(hook|verse \d+|outro|bridge|chorus|intro)\]?$", re.I) +norm = lambda s: re.sub(r"[^a-z0-9']", "", s.lower()) + +def load_words(): + words = json.load(open(A.words)) + # zero-length words come out of the aligner's gap interpolation when + # the neighbours touch — spread the neighbours' span evenly over the run + i = 0 + while i < len(words): + if words[i]["toMs"] > words[i]["fromMs"]: + i += 1; continue + j = i + while j < len(words) and words[j]["toMs"] <= words[j]["fromMs"]: j += 1 + lo = max(0, i - 1); hi = min(len(words) - 1, j) + t0, t1 = words[lo]["fromMs"], words[hi]["toMs"] + n = hi - lo + 1 + for k in range(lo, hi + 1): + words[k]["fromMs"] = round(t0 + (t1 - t0) * (k - lo) / n) + words[k]["toMs"] = round(t0 + (t1 - t0) * (k - lo + 1) / n) + i = j + return words + +def build_lines(): + RAW = os.environ.get("SAILOR_CLOCK") == "raw" + LS, LD = ((np.array([0.0, 1e4]), np.array([0.0, 1e4])) if RAW else timemap(LOCK_MAP)); RS, RD = ((np.array([0.0, 1e4]), np.array([0.0, 1e4])) if RAW else timemap(REG_MAP)) + start = float(json.load(open(A.events))["startSec"]) + segmap = os.path.join(SRC, "vox", "cut", "segmap.txt") + segs = [tuple(map(float, l.split())) for l in open(segmap).read().strip().split("\n")] if os.path.exists(segmap) and not RAW else [] + def rec(s): + t = float(np.interp(np.interp(s, LS, LD), RS, RD)) + if not segs: return t - start + for a0, b0, o in segs: + if a0 <= t < b0: return t - a0 + o - start + return None # outside the arrangement + env = vocal_env() + words = load_words() + lines_txt = [l.strip() for l in open(A.lyrics, encoding="utf8")] + lines_txt = [l for l in lines_txt if l and not HEADER_RE.match(l)] + lines = []; wi = 0 + for text in lines_txt: + toks = [t for t in text.split() if norm(t)] + ws = [] + for tok in toks: + w = words[wi]; wi += 1 + assert norm(w["text"]) == norm(tok), f"lyric/word drift at {tok!r} vs {w['text']!r}" + ws.append(dict(text=tok, a=w["fromMs"] / 1000, b=w["toMs"] / 1000)) + # whisper's boundary after a rest sits early: where a word starts in + # silence, walk its onset forward to where her voice comes in (never + # back, never past its own end), and let a squeezed word push the next + prev_b = lines[-1]["words"][-1]["b"] if lines else 0.0 + for w in ws: + w["a"] = max(w["a"], prev_b) + i0 = int(w["a"] * ENV_SR); i1 = int((w["a"] + 0.5) * ENV_SR) + if env[i0] >= 0.02: # in voice already: only a breath within 250 ms counts as a rest + d = next((i for i in range(i0, i0 + 25) if env[i] < 0.02), None) + i0, i1 = (d, int(min(w["b"] - 0.05, w["a"] + 0.5) * ENV_SR)) if d else (None, None) + if i0 is not None: + for i in range(i0, max(i0, i1)): + if env[i] > 0.05: w["a"] = i / ENV_SR; break + w["b"] = max(w["b"], w["a"] + 0.08); prev_b = w["b"] + kept = [] + for w in ws: + r0, r1 = rec(w["a"]), rec(w["b"]) + if r0 is None: continue # the word is in the edit + w["t0"] = max(0.0, r0); w["t1"] = max(w["t0"] + 0.02, r1 if r1 is not None else w["t0"] + 0.06); kept.append(w) + if not kept: continue # the whole line is in the edit + lines.append(dict(text=" ".join(w["text"] for w in kept), words=kept, t0=kept[0]["t0"], t1=kept[-1]["t1"])) + assert wi == len(words), f"{len(words) - wi} aligned words left over" + lines.sort(key=lambda L: L["t0"]) # record order: the arrangement may reorder verses + # display windows: fade in LEAD before the first word; out when the next + # line takes over, or HOLD after the last word if the gap is long + # (a line that follows closely waits in the small slot underneath and takes + # the big slot only once this line's last word is sung — never earlier) + for k, L in enumerate(lines): + L["on"] = L["t0"] - LEAD if k == 0 or not lines[k - 1]["cut"] else lines[k - 1]["off"] + if k + 1 < len(lines): + N = lines[k + 1] + L["cut"] = N["t0"] - LEAD <= L["t1"] + HOLD + L["off"] = min(N["t0"], max(N["t0"] - LEAD, L["t1"] - 0.3)) if L["cut"] else L["t1"] + HOLD + else: + L["cut"] = False; L["off"] = L["t1"] + HOLD + # the record-time word list, for checking against the vocal (local) + json.dump([dict(text=w["text"], fromMs=round(w["t0"] * 1000), toMs=round(w["t1"] * 1000)) + for L in lines for w in L["words"]], + open(os.path.join(SRC, "words-record-lyricvideo.json"), "w"), indent=1) + return lines, start, len(words) + +# ── background: the cover, blurred and darkened, a small square of it ─ +def build_bg(): + cov = Image.open(A.cover).convert("RGB") + s = W / cov.width + big = cov.resize((W, round(cov.height * s)), Image.LANCZOS) + top = (big.height - H) // 2 + bg = big.crop((0, top, W, top + H)).filter(ImageFilter.GaussianBlur(42)) + arr = np.asarray(bg).astype(np.float32) + gray = arr.mean(axis=2, keepdims=True) + arr = arr * 0.72 + gray * 0.28 # soften the colour + arr *= 0.36 # darken + arr *= np.array([1.06, 0.98, 0.9], np.float32) # warm + yy, xx = np.mgrid[0:H, 0:W] + r = np.sqrt(((xx - W / 2) / (W / 2)) ** 2 + ((yy - H / 2) / (H / 2)) ** 2) + vig = np.clip(1.0 - 0.55 * np.clip(r - 0.35, 0, 1) ** 1.6, 0, 1) + arr *= vig[..., None] + bg = Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8)) + # the square: soft shadow, rounded corners + S = 300; x0, y0 = (W - S) // 2, 118 + sq = cov.resize((S, S), Image.LANCZOS) + mask = Image.new("L", (S, S), 0) + ImageDraw.Draw(mask).rounded_rectangle([0, 0, S - 1, S - 1], radius=22, fill=255) + shadow = Image.new("L", (S + 160, S + 160), 0) + ImageDraw.Draw(shadow).rounded_rectangle([80, 88, 80 + S, 88 + S], radius=22, fill=150) + shadow = shadow.filter(ImageFilter.GaussianBlur(28)) + bg.paste((8, 5, 4), (x0 - 80, y0 - 80), shadow) + bg.paste(sq, (x0, y0), mask) + return bg + +# ── text layers, cached per line ────────────────────────────────────── +F_CUR_MAX, F_NEXT = 86, 46 +MAXW = W - 200 +PAD = 40 +_meas = ImageDraw.Draw(Image.new("L", (4, 4))) + +def fit_font(text, size): + f = font(size) + while _meas.textlength(text, font=f) > MAXW and size > 40: + size -= 2; f = font(size) + return f + +def render_text(text, f, color): + bbox = _meas.textbbox((0, 0), text, font=f) + w, h = bbox[2] - bbox[0], bbox[3] - bbox[1] + im = Image.new("RGBA", (w + PAD * 2, h + PAD * 2), (0, 0, 0, 0)) + ImageDraw.Draw(im).text((PAD - bbox[0], PAD - bbox[1]), text, font=f, fill=(*color, 255)) + return im, bbox + +def with_alpha(layer, k): + out = layer.copy(); out.putalpha(layer.split()[3].point(lambda v: int(v * k))); return out + +def glow_of(layer): + g = Image.new("RGBA", layer.size, (0, 0, 0, 0)) + g.paste((10, 6, 4, 210), (0, 0), layer.split()[3]) + return g.filter(ImageFilter.GaussianBlur(14)) + +CACHE = {} +def line_layers(k): + if k in CACHE: return CACHE[k] + L = LINES[k] + f = fit_font(L["text"], F_CUR_MAX) + lit, bbox = render_text(L["text"], f, CREAM) + hot, _ = render_text(L["text"], f, HOT) + # x of each word's start/end inside the layer, from prefix widths + xs = []; pos = 0; text = L["text"] + for w in L["words"]: + j = text.index(w["text"], pos) + x0 = PAD - bbox[0] + _meas.textlength(text[:j], font=f) + x1 = PAD - bbox[0] + _meas.textlength(text[:j + len(w["text"])], font=f) + xs.append((x0, x1)); pos = j + len(w["text"]) + nxt, _ = render_text(L["text"], fit_font(L["text"], F_NEXT), CREAM) + CACHE[k] = dict(dim=with_alpha(lit, DIM), lit=lit, hot=hot, glow=glow_of(lit), xs=xs, + nxt=with_alpha(nxt, NEXT_ALPHA), nglow=glow_of(nxt)) + return CACHE[k] + +def wipe_x(L, xs, t): + """(x where the sung-cream stops, x where the gold wipe stops).""" + cur = -1 + for i, w in enumerate(L["words"]): + if t >= w["t0"]: cur = i + if cur < 0: return 0, 0 + w = L["words"][cur] + p = min(1.0, (t - w["t0"]) / (w["t1"] - w["t0"])) # gold stays on through the gap after a word + x0, x1 = xs[cur] + return x0, x0 + (x1 - x0) * p + +def paste_fade(frame, layer, xy, fade, mask=None): + if fade <= 0: return + a = layer.split()[3] + if mask is not None: a = ImageChops.multiply(a, mask) + if fade < 1: a = a.point(lambda v: int(v * fade)) + frame.paste(layer, xy, a) + +Y_CUR, Y_NEXT = 640, 790 + +def draw_frame(t, bg): + frame = bg.copy() + cur = next((k for k, L in enumerate(LINES) if L["on"] <= t < L["off"]), None) + if cur is None: + # a rest: the upcoming line waits small, underneath, until its own fade-in + nk = next((k for k, L in enumerate(LINES) if L["on"] > t), None) + if nk is None: return frame + gap0 = LINES[nk - 1]["off"] + 0.4 if nk else -1.0 + fade = min(1.0, (t - gap0) / FADE, (LINES[nk]["on"] - t) / FADE) + if fade > 0: + ln = line_layers(nk); nw, nh = ln["nxt"].size + nx, ny = (W - nw) // 2, Y_NEXT - nh // 2 + paste_fade(frame, ln["nglow"], (nx, ny), fade * 0.8) + paste_fade(frame, ln["nxt"], (nx, ny), fade) + return frame + L = LINES[cur]; ly = line_layers(cur) + fade = min(1.0, (t - L["on"]) / (FADE if cur == 0 or not LINES[cur - 1]["cut"] else FADE * 0.5)) + if not L["cut"]: + fade = min(fade, max(0.0, (L["off"] - t) / FADE)) + # current line: glow, dim base, cream up to the current word, gold wipe through it + lw, lh = ly["dim"].size + x, y = (W - lw) // 2, Y_CUR - lh // 2 + paste_fade(frame, ly["glow"], (x, y), fade) + paste_fade(frame, ly["dim"], (x, y), fade) + xa, xb = wipe_x(L, ly["xs"], t) + if xa > 0: + m = Image.new("L", (lw, lh), 0); ImageDraw.Draw(m).rectangle([0, 0, int(xa), lh], fill=255) + paste_fade(frame, ly["lit"], (x, y), fade, m) + if xb > xa: + m = Image.new("L", (lw, lh), 0); ImageDraw.Draw(m).rectangle([int(xa), 0, int(xb), lh], fill=255) + paste_fade(frame, ly["hot"], (x, y), fade, m) + # the next line, small, underneath + if cur + 1 < len(LINES): + ln = line_layers(cur + 1) + nw, nh = ln["nxt"].size + nx, ny = (W - nw) // 2, Y_NEXT - nh // 2 + paste_fade(frame, ln["nglow"], (nx, ny), fade * 0.8) + paste_fade(frame, ln["nxt"], (nx, ny), fade) + return frame + +# ── ffmpeg ──────────────────────────────────────────────────────────── +def duration(path): + return float(subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration", + "-of", "csv=p=0", path], capture_output=True, text=True).stdout.strip()) + +def mux(silent, master, out): + subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", "-y", + "-i", silent, "-i", master, "-map", "0:v", "-map", "1:a", + "-c:v", "copy", "-c:a", "aac", "-b:a", "320k", + "-movflags", "+faststart", out], check=True) + print(f"✓ {out} ({duration(out):.3f} s · master {duration(master):.3f} s)") + +def render(silent, dur, bg): + frames = math.ceil(dur * FPS) + proc = subprocess.Popen(["ffmpeg", "-hide_banner", "-loglevel", "error", "-y", + "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{W}x{H}", "-r", str(FPS), "-i", "-", + "-an", "-c:v", "libx264", "-preset", "medium", "-crf", "18", "-pix_fmt", "yuv420p", + "-r", str(FPS), silent], stdin=subprocess.PIPE) + t_start = time.time() + for i in range(frames): + t = (i + 0.5) / FPS + proc.stdin.write(draw_frame(t, bg).tobytes()) + if i % 600 == 0: + print(f" {i}/{frames} {time.time() - t_start:.0f}s", flush=True) + proc.stdin.close(); proc.wait() + if proc.returncode: sys.exit("✗ x264 failed") + print(f"✓ {silent} ({frames} frames, {time.time() - t_start:.0f}s)") + +if __name__ == "__main__": + bg = build_bg() + for attempt in range(3): + clock0 = clock_state() + master_mtime = os.path.getmtime(A.master) + dur = duration(A.master) + LINES, START, nwords = build_lines(); CACHE.clear() + print(f"master {A.master} · {dur:.3f} s · startSec {START} · {len(LINES)} lines / {nwords} words") + print(f"first word at {LINES[0]['t0']:.2f} s record · last word ends {LINES[-1]['t1']:.2f} s") + if A.stills: + d = A.stills_dir or os.path.dirname(A.out); os.makedirs(d, exist_ok=True) + for s in A.stills.split(","): + p = os.path.join(d, f"lyrics-still-{float(s):05.1f}s.png") + draw_frame(float(s), bg).save(p); print(f" still {p}") + if A.no_render: break + if not A.mux_only: + render(SILENT, dur, bg) + mux(SILENT, A.master, A.out) + if clock_state() != clock0: + print("! the time base moved during the render — drawing again") + A.mux_only = False; continue + if os.path.getmtime(A.master) != master_mtime: + print("! the master changed during the render — re-muxing") + mux(SILENT, A.master, A.out) + break diff --git a/pop/sailor-song/bin/measures.py b/pop/sailor-song/bin/measures.py new file mode 100644 index 0000000000..d24b3733f6 --- /dev/null +++ b/pop/sailor-song/bin/measures.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +# measures.py — the guitar's own clock, strum by strum, so the drums hit +# where her hand hits. +# +# 1. STRUMS onsets on the guitar stem (backtracked to the attack), +# strength, and a down/up guess (downstrokes hit the low +# strings first → lower spectral centroid in the first 30 ms). +# 2. BEATS take.analysis.json's beat track, each beat pulled onto the +# nearest strong strum within ±70 ms. A beat with no strum +# nearby is interpolated between its neighbours, never left +# where the tracker guessed. +# 3. GRID where strums land inside a beat (phase histogram) → what +# subdivision she plays (8ths vs 16ths) — and so which pulse +# is the quarter note. +# 4. CHORDS per beat, template-matched against the song's real +# vocabulary only. The tabs agree (capo 4: Cmaj7 Em G G → +# Emaj7 G#m B B), so the choice is between three chords. +# 5. BARS the bar length + phase that puts the most chord changes +# on downbeats; each bar gets its chord and strum pattern. +# +# pop/.venv/bin/python pop/sailor-song/bin/measures.py + +import json, os +import numpy as np +import librosa + +HERE = os.path.dirname(os.path.abspath(__file__)) +LANE = os.path.dirname(HERE) +GUITAR = os.path.join(os.path.dirname(LANE), "samples/sailor-song-take/stems/htdemucs/guitar.wav") +A = json.load(open(os.path.join(LANE, "take.analysis.json"))) +SR, HOP = 22050, 256 + +y, _ = librosa.load(GUITAR, sr=SR, mono=True) +tuning = A["tuningCents"] / 100.0 + +# ── 1. strums ────────────────────────────────────────────────────────── +env = librosa.onset.onset_strength(y=y, sr=SR, hop_length=HOP, aggregate=np.median) +on = librosa.onset.onset_detect(onset_envelope=env, sr=SR, hop_length=HOP, + backtrack=True, units="frames", delta=0.06) +on_t = librosa.frames_to_time(on, sr=SR, hop_length=HOP) +peak = np.array([env[f:f + 4].max() if f < len(env) else 0 for f in on]) +peak = peak / (np.percentile(peak, 95) + 1e-9) +cent = [] +for t in on_t: + a = int(t * SR); seg = y[a:a + int(0.03 * SR)] + cent.append(float(librosa.feature.spectral_centroid(y=seg, sr=SR, n_fft=512).mean()) if len(seg) > 512 else 0) +cent = np.array(cent) +med_c = np.median(cent[cent > 0]) +strums = [{"t": round(float(t), 3), "amp": round(float(p), 2), + "dir": "D" if c <= med_c else "U"} for t, p, c in zip(on_t, peak, cent)] +strong = on_t[peak > 0.35] + +# ── 2. beats snapped to strums ───────────────────────────────────────── +# The tracker's beats sit on her strums but run ~35 ms early; pull each +# onto the nearest strong strum. +raw = np.array(A["pulse"]["beats"]) +snapped, hit = raw.copy(), np.zeros(len(raw), bool) +for i, b in enumerate(raw): + if len(strong): + j = np.argmin(np.abs(strong - b)) + if abs(strong[j] - b) <= 0.07: + snapped[i] = strong[j]; hit[i] = True +for i in range(len(raw)): # fill misses by interpolation + if not hit[i]: + lo = max([k for k in range(i) if hit[k]], default=None) + hi = min([k for k in range(i + 1, len(raw)) if hit[k]], default=None) + if lo is not None and hi is not None: + snapped[i] = snapped[lo] + (snapped[hi] - snapped[lo]) * (i - lo) / (hi - lo) +snapped = np.maximum.accumulate(snapped) +offs = (snapped - raw)[hit] * 1000 + +# ── 3. subdivision ───────────────────────────────────────────────────── +phases = [] +for t in on_t: + k = np.searchsorted(snapped, t) - 1 + if 0 <= k < len(snapped) - 1: + phases.append((t - snapped[k]) / (snapped[k + 1] - snapped[k])) +phases = np.array(phases) +hist16 = np.histogram(phases, bins=8, range=(-1 / 16, 1 - 1 / 16))[0] # 32nd-centred 8 bins +per_beat = len(on_t) / max(1, len(snapped)) + +# ── 4. chords per beat (real vocabulary only) ────────────────────────── +N = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] +VOCAB = {"Emaj7": [4, 8, 11, 3], "G#m": [8, 11, 3], "B": [11, 3, 6]} +yh = librosa.effects.harmonic(y, margin=4) +chroma = librosa.feature.chroma_cqt(y=yh, sr=SR, hop_length=512, tuning=tuning, bins_per_octave=36) +bass = librosa.feature.chroma_cqt(y=yh, sr=SR, hop_length=512, tuning=tuning, fmin=librosa.note_to_hz("E2"), n_octaves=2, bins_per_octave=36) +fr = librosa.time_to_frames(snapped, sr=SR, hop_length=512) +C = librosa.util.sync(chroma, fr, aggregate=np.median)[:, 1:] # column k = beat k → k+1 +B = librosa.util.sync(bass, fr, aggregate=np.median)[:, 1:] +labels = [] +for k in range(C.shape[1]): + c = C[:, k] / (np.linalg.norm(C[:, k]) + 1e-9) + b = B[:, k] / (B[:, k].max() + 1e-9) + best = None + for name, pcs in VOCAB.items(): + tpl = np.zeros(12); tpl[pcs] = 1; tpl /= np.linalg.norm(tpl) + root = {"Emaj7": 4, "G#m": 8, "B": 11}[name] + s = float(c @ tpl) + 0.35 * float(b[root]) # the bass note decides G#m vs B + if best is None or s > best[0]: + best = (s, name) + labels.append(best[1]) +labels = labels[:len(snapped) - 1] + +# ── 5. bars, anchored on her strum motif ─────────────────────────────── +# Her hand plays a 2-beat motif X..X (8ths): the beat, then the "and" of +# the next beat. The tracker gains/drops a beat in places (measured: the +# motif reads .XX. through ~29–60 s), so bars are NOT every 4 tracked +# beats — they start where the motif starts. Motif-start score for beat k: +# h(beat k) + h(and of k+1) − h(and of k) − h(beat k+1) +def h(t): + d = np.abs(on_t - t) + j = np.argmin(d) + return float(peak[j]) if d[j] < 0.06 else 0.0 + + +nb = len(snapped) +mid = np.append((snapped[:-1] + snapped[1:]) / 2, snapped[-1]) +sk = np.array([h(snapped[k]) + h(mid[k + 1]) - h(mid[k]) - h(snapped[k + 1]) + if k + 1 < nb - 1 else 0.0 for k in range(nb)]) +# parity per 8-beat window, then a motif start every 2 beats of that parity +W = 8 +par = [] +for w0 in range(0, nb, W): + e = sk[w0:w0 + W][0::2].sum() if (w0 % 2 == 0) else sk[w0:w0 + W][1::2].sum() + o = sk[w0:w0 + W][1::2].sum() if (w0 % 2 == 0) else sk[w0:w0 + W][0::2].sum() + par.append(0 if e >= o else 1) +starts = [k for k in range(nb) if k % 2 == par[k // W]] +starts = [k for i, k in enumerate(starts) if i == 0 or k - starts[i - 1] >= 2 or True] +dedup = [] +for k in starts: + if dedup and k - dedup[-1] < 2: + continue + dedup.append(k) +starts = dedup +flips = [round(float(snapped[starts[i]]), 2) for i in range(1, len(starts)) if starts[i] - starts[i - 1] != 2] + +# downbeat = every other motif start; pick the parity that puts chord changes on it +chg = [k for k in range(1, len(labels)) if labels[k] != labels[k - 1]] +def near(k, pool): return min(abs(k - p) for p in pool) <= 1 +best = None +for ph in (0, 1): + downs = starts[ph::2] + sc = sum(1 for c in chg if near(c, downs)) / max(1, len(chg)) + if best is None or sc > best[0]: + best = (sc, ph) +L, PH, GRID_SHIFT = 4, best[1], f"motif-anchored, {len(flips)} beat-count flips at {flips}" +downs = starts[PH::2] +bars = [] +for d0, d1 in zip(downs, downs[1:]): + seg = labels[d0:d1] or [labels[min(d0, len(labels) - 1)]] + chord = max(set(seg), key=seg.count) + t0, t1 = snapped[d0], snapped[d1] + nbeat = d1 - d0 + pat = ["."] * (2 * nbeat) + for st in strums: + if t0 - 0.03 <= st["t"] < t1 - 0.03 and st["amp"] > 0.2: + k = int(round((st["t"] - t0) / ((t1 - t0) / (2 * nbeat)))) + if 0 <= k < 2 * nbeat: + pat[k] = "X" + bars.append({"n": len(bars) + 1, "t": round(float(t0), 3), "dur": round(float(t1 - t0), 3), + "beats": [round(float(x), 3) for x in snapped[d0:d1 + 1]], + "bpm": round(60 * nbeat / float(t1 - t0), 1), "chord": chord, + "agree": round(seg.count(chord) / len(seg), 2), "strum": "".join(pat)}) + +res = { + "strums": {"count": len(strums), "perBeat": round(per_beat, 2), + "phaseHist8": hist16.tolist()}, + "beats": {"snapped": [round(float(b), 3) for b in snapped], + "hitFraction": round(float(hit.mean()), 3), + "trackerOffsetMs": {"median": round(float(np.median(offs)), 1), + "p90abs": round(float(np.percentile(np.abs(offs), 90)), 1)}}, + "meter": {"gridShift": GRID_SHIFT, "beatsPerBar": L, "phase": PH, "downbeatChangeFraction": round(best[0], 2)}, + "bars": bars, + "strumList": strums, +} +json.dump(res, open(os.path.join(LANE, "measures.json"), "w"), indent=1) + +print(f"strums {len(strums)} ({per_beat:.2f} per beat) phase hist (8 slots/beat) {hist16.tolist()}") +print(f"beats snapped to a strum: {hit.mean():.0%} tracker was off by median " + f"{np.median(offs):+.0f} ms, p90 |{np.percentile(np.abs(offs), 90):.0f}| ms") +print(f"grid shift: {GRID_SHIFT} meter: {L} beats/bar, phase {PH}, {best[0]:.0%} of chord changes on downbeats") +print("bar time bpm chord agree strum(8ths)") +for b in bars: + print(f"{b['n']:3d} {b['t']:7.2f} {b['bpm']:6.1f} {b['chord']:6s} {b['agree']:.2f} {b['strum']}") diff --git a/pop/sailor-song/bin/natural-vox.py b/pop/sailor-song/bin/natural-vox.py new file mode 100644 index 0000000000..1597e14925 --- /dev/null +++ b/pop/sailor-song/bin/natural-vox.py @@ -0,0 +1,73 @@ +"""natural-vox.py — her real voice, gently tuned, no vocoder. + +aesthetivox.py rebuilds every sung frame with WORLD and splices her own +consonants back in between. That composite is what made the vowels skip and +lose their sung quality (v19 ear-check). This keeps the recording itself: +it takes aesthetivox's tuning curve (the note it would sing, per 5 ms frame), +turns it into a per-frame pitch OFFSET against what she actually sang, and +runs the dry stem through ONE rubberband R3 pass with that pitch map +(formant-preserving). Her breath, grit and vibrato stay hers. + + pop/.venv/bin/python pop/sailor-song/bin/natural-vox.py [--strength 0.7] + → src/vox/vocals-natural.wav (the engine prefers it over vocals-aesthetivox) +""" +import argparse, os, subprocess, tempfile +import numpy as np +import soundfile as sf +from scipy.signal import correlate + +HERE = os.path.dirname(os.path.abspath(__file__)) +VOX = os.path.join(HERE, "..", "src", "vox") +FRAME_MS = 5.0 + +ap = argparse.ArgumentParser() +ap.add_argument("--strength", type=float, default=0.7) # of aesthetivox's correction; 1.0 = the full snap +ap.add_argument("--max-st", type=float, default=1.5) # never shift a frame further than this +args = ap.parse_args() + +dbg = os.path.join(tempfile.gettempdir(), "sailor-avox-debug.npz") +if not os.path.exists(dbg): + subprocess.run([os.path.join(HERE, "..", "..", ".venv", "bin", "python"), os.path.join(HERE, "aesthetivox.py")], + env={**os.environ, "AVOX_DEBUG": dbg}, check=True) +d = np.load(dbg) +f0, sung, ms = d["f0"], d["sung"].astype(bool), d["ms"] +her_hz = 440.0 * 2 ** ((ms - 69) / 12) +corr = np.where(sung & (ms > 0), 12 * np.log2(np.maximum(f0, 1) / np.maximum(her_hz, 1)), np.nan) +corr = np.clip(corr * args.strength, -args.max_st, args.max_st) + +# hold the correction through unsung frames (so a consonant never snaps the pitch +# back mid-word) and smooth it over 25 ms so the shift glides, never steps +idx = np.where(np.isfinite(corr))[0] +corr = np.interp(np.arange(len(corr)), idx, corr[idx]) +k = int(25 / FRAME_MS) +corr = np.convolve(np.pad(corr, k, mode="edge"), np.ones(2 * k + 1) / (2 * k + 1), "valid") + +dry = os.path.join(VOX, "vocals-dry-48k.wav") +x, fs = sf.read(dry, dtype="float64", always_2d=True) +spf = int(round(fs * FRAME_MS / 1000)) +out = os.path.join(VOX, "vocals-natural.wav") +with tempfile.TemporaryDirectory() as tmp: + pmap = os.path.join(tmp, "pitch.map") + with open(pmap, "w") as f: + f.write("".join(f"{i * spf} {c:.4f}\n" for i, c in enumerate(corr))) + raw = os.path.join(tmp, "shifted.wav") + subprocess.run(["rubberband", "-3", "-F", "-q", "--pitchmap", pmap, dry, raw], check=True) + y, _ = sf.read(raw, dtype="float64", always_2d=True) + +# the pitch map runs rubberband in realtime mode, which adds latency: find it by +# cross-correlating against the dry stem and slide the result back into place +a, b = x[: 30 * fs, 0], y[: 30 * fs + fs, 0] +lag = int(np.argmax(correlate(b, a, mode="valid", method="fft"))) +y = y[lag: lag + len(x)] +if len(y) < len(x): + y = np.pad(y, ((0, len(x) - len(y)), (0, 0))) +# the bedroom out, a crisp pop vocal in: the phone mic's boxy room (250) and honk (450) +# cut, presence at 3.2 k and air above 9 k lifted, the esses held back after the lift +EQ = ("highpass=f=110:p=2,equalizer=f=250:t=q:w=1.2:g=-3.5,equalizer=f=450:t=q:w=1.5:g=-2.5," + "equalizer=f=900:t=q:w=2:g=-1,equalizer=f=3200:t=q:w=1.2:g=2.5,highshelf=f=9000:g=3.5," + "deesser=i=0.35:m=0.5:f=0.5") +with tempfile.TemporaryDirectory() as tmp: + flat = os.path.join(tmp, "flat.wav") + sf.write(flat, y, fs, subtype="FLOAT") + subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", flat, "-af", EQ, "-c:a", "pcm_f32le", out], check=True) +print(f"strength {args.strength} · median |shift| {np.median(np.abs(corr[idx])):.2f} st · latency {lag / fs * 1000:.1f} ms → {out}") diff --git a/pop/sailor-song/bin/platter-analysis.mjs b/pop/sailor-song/bin/platter-analysis.mjs new file mode 100644 index 0000000000..0ef5f8ba5f --- /dev/null +++ b/pop/sailor-song/bin/platter-analysis.mjs @@ -0,0 +1,345 @@ +#!/usr/bin/env node +// platter-analysis.mjs — read her take against the papers platter. +// +// measures.json, vox-notes.json and take.analysis.json say WHAT she played. +// This pass asks what the platter's rules say about it: the rhythm platter +// (necklace geometry, syncopation vector, chronotonic distance, entrainment, +// beat bins), the chamber platter (form proportions, climax and stillness +// positions, phrase lengths, register per phrase), the bass platter (sub +// register, tempo band, delay in beats) and the pop strategies digest +// (track length, the cut, the build curve). Every number cites the digest +// rule it comes from. Nothing here touches audio except the vocal and guitar +// stems, read once for a loudness envelope per bar. +// +// node pop/sailor-song/bin/platter-analysis.mjs → platter-analysis.json + stdout +// +// Rhythm math is pop/lib/necklace.mjs (papers/rhythm-platter is its spec). + +import { readFileSync, writeFileSync, existsSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import * as N from "../../lib/necklace.mjs"; +import { readWavMono } from "../../lib/wav.mjs"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const LANE = resolve(HERE, ".."); +const M = JSON.parse(readFileSync(resolve(LANE, "measures.json"), "utf8")); +const V = JSON.parse(readFileSync(resolve(LANE, "vox-notes.json"), "utf8")); +const A = JSON.parse(readFileSync(resolve(LANE, "take.analysis.json"), "utf8")); +const DUR = A.duration; +const bars = M.bars; + +// Sections as render.mjs has them (bar numbers, 1-based inclusive). +const SECTIONS = [ + ["intro", 1, 10], ["verse1", 11, 27], ["chorus1", 28, 43], ["verse2", 44, 51], + ["chorus2", 52, 67], ["break", 68, 71], ["bridge", 72, 80], ["outro", 81, 84], +]; +const barOf = (t) => bars.find((b) => t >= b.t && t < b.t + b.dur); +const sectionOfBar = (n) => SECTIONS.find(([, a, b]) => n >= a && n <= b)?.[0]; +const r3 = (x) => Math.round(x * 1000) / 1000; +const r1 = (x) => Math.round(x * 10) / 10; +const median = (a) => { const s = [...a].sort((x, y) => x - y); return s.length ? s[Math.floor(s.length / 2)] : NaN; }; +const pct = (a, p) => { const s = [...a].sort((x, y) => x - y); return s.length ? s[Math.min(s.length - 1, Math.floor(p * s.length))] : NaN; }; +const mean = (a) => (a.length ? a.reduce((x, y) => x + y, 0) / a.length : NaN); + +const out = { source: "measures.json + vox-notes.json + take.analysis.json", date: "2026-09-29" }; + +// ── 1. RHYTHM · her strum motif as a necklace (digest 01–06) ───────────────── +// The motif is X..XX..X on 8 pulses (8ths): onsets {0,3,4,7}, IOI (3,1,3,1). +const motif = "x..xx..x"; +const motifBars = bars.filter((b) => b.strum.length === 8).map((b) => ({ ...b, strum: b.strum.toLowerCase() })); +const patternCounts = {}; +for (const b of motifBars) patternCounts[b.strum] = (patternCounts[b.strum] || 0) + 1; +const an = N.analyze(motif); +const E48 = N.toBox(N.bjorklund(4, 8)); +const named = { + "E(4,8) straight 8ths": E48, + "tresillo E(3,8)": N.toBox(N.bjorklund(3, 8)), + "cinquillo": "x.xx.xx.", + "her complement (hocket partner)": N.toBox(N.complement(motif)), +}; +const distances = Object.fromEntries(Object.entries(named).map(([k, v]) => { + const d = N.dist(motif, v, { measure: "chronotonic", cyclic: true }); + let swap = null; + try { swap = N.dist(motif, v, { measure: "swap", cyclic: true }).distance; } catch { /* unequal k */ } + return [k, { box: v, chronotonic: d.distance, rotation: d.rotation, swap }]; +})); +// per-bar drift from the motif: chronotonic (the platter's default), fixed rotation +// because the bar's downbeat is anchored — a rotation would be a different bar. +const perBarDrift = motifBars.map((b) => ({ + n: b.n, strum: b.strum, chord: b.chord, + chronotonic: N.dist(motif, b.strum, { cyclic: false }).distance, + extraOnsets: N.rhythm(b.strum).k - 4, +})); +const driftBySection = Object.fromEntries(SECTIONS.map(([s]) => { + const rows = perBarDrift.filter((d) => sectionOfBar(d.n) === s); + return [s, { bars: rows.length, meanChronotonic: r3(mean(rows.map((d) => d.chronotonic))), meanExtraOnsets: r3(mean(rows.map((d) => d.extraOnsets))), verbatim: rows.filter((d) => d.chronotonic === 0).length }]; +})); +out.rhythm = { + motif: { box: motif, ...an, syncopation: N.syncopationProfile(motif), metricWeights: N.metricWeights(8), periodic: N.isPeriodic(motif) }, + kitPattern: { box: "x..xx..x", note: "render.mjs kit: kick 1 and &2, snare 3, rim &4 — the SAME necklace as her hand; the kit doubles her, it does not interlock" }, + distances, + patternCounts, + perBarDrift, + driftBySection, + morphToStraight: N.morphPath(motif, E48).map(N.toBox), +}; + +// ── 2. RHYTHM · beat bins: where her off-beat strums sit inside the beat (07) ─ +// The necklace decides which pulse; an offset table decides where in the bin. +const beats = M.beats.snapped; +const offbeatPhase = [], onbeatOff = []; +for (const s of M.strumList) { + if (s.amp < 0.3) continue; + let i = beats.findIndex((b) => b > s.t) - 1; + if (i < 0 || i >= beats.length - 1) continue; + const ph = (s.t - beats[i]) / (beats[i + 1] - beats[i]); + if (ph > 0.3 && ph < 0.75) offbeatPhase.push(ph); + else if (ph < 0.15) onbeatOff.push((s.t - beats[i]) * 1000); + else if (ph > 0.85) onbeatOff.push((s.t - beats[i + 1]) * 1000); +} +const swingPhase = median(offbeatPhase); +// her VOICE against her own 8th grid (vox-notes onsets are pre-lock, i.e. as sung) +const beatList = bars.flatMap((b) => b.beats.slice(0, -1)); +const grid8 = []; +for (let i = 0; i < beatList.length - 1; i++) grid8.push(beatList[i], (beatList[i] + beatList[i + 1]) / 2); +const voiceRows = V.notes.map((n) => { let best = grid8[0]; for (const t of grid8) if (Math.abs(t - n.t) < Math.abs(best - n.t)) best = t; const b = barOf(n.t); return { off: (n.t - best) * 1000, sec: b ? sectionOfBar(b.n) : null, dur: n.dur }; }); +const voiceBySection = Object.fromEntries(SECTIONS.map(([sName]) => { const r = voiceRows.filter((x) => x.sec === sName); return r.length > 3 ? [sName, { notes: r.length, signedMedianMs: Math.round(median(r.map((x) => x.off))), absMedianMs: Math.round(median(r.map((x) => Math.abs(x.off)))), lateShare: r3(r.filter((x) => x.off > 15).length / r.length) }] : null; }).filter(Boolean)); +out.beatBins = { + offbeatStrums: offbeatPhase.length, + offbeatPhaseMedian: r3(swingPhase), offbeatPhaseP10: r3(pct(offbeatPhase, 0.1)), offbeatPhaseP90: r3(pct(offbeatPhase, 0.9)), + swingRatio: r3(swingPhase / (1 - swingPhase)), + offbeatLateMsAt120: r1((swingPhase - 0.5) * 500), + voiceVsGrid: { + all: { signedMedianMs: Math.round(median(voiceRows.map((x) => x.off))), absMedianMs: Math.round(median(voiceRows.map((x) => Math.abs(x.off)))) }, + bySection: voiceBySection, + longNotesSignedMs: Math.round(median(voiceRows.filter((x) => x.dur > 0.4).map((x) => x.off))), + shortNotesSignedMs: Math.round(median(voiceRows.filter((x) => x.dur <= 0.2).map((x) => x.off))), + afterLockVox: "lock-vox.mjs pulls these onto the grid: abs median 41 → 8 ms (its own readout)", + }, + note: "digest 07 (Danielsen): a beat is a bin, not a point. Her & sits late of the mathematical 8th; that offset is the groove and must live in a separate microtiming layer, never folded into onsets", +}; + +// ── 3. RHYTHM · entrainment window (07, London) ─────────────────────────────── +const bpmBySection = Object.fromEntries(SECTIONS.map(([s, a, b]) => { + const rows = bars.filter((x) => x.n >= a && x.n <= b); + return [s, { median: r1(median(rows.map((x) => x.bpm))), min: r1(Math.min(...rows.map((x) => x.bpm))), max: r1(Math.max(...rows.map((x) => x.bpm))), start: r1(rows[0].t), end: r1(rows[rows.length - 1].t + rows[rows.length - 1].dur) }]; +})); +const bpmMed = median(bars.map((b) => b.bpm)); +const beatMs = 60000 / bpmMed; +out.entrainment = { + medianBpm: r1(bpmMed), + periodsMs: { eighth: r1(beatMs / 2), quarter: r1(beatMs), half: r1(beatMs * 2), bar: r1(beatMs * 4), chordCycle2bars: r1(beatMs * 8) }, + window: { floorMs: 100, ceilingMs: 2000, sweetSpotMs: [500, 700] }, + felt: { + quarterInSweetSpot: beatMs >= 500 && beatMs <= 700, quarterMsFromSweetSpot: r1(beatMs < 500 ? beatMs - 500 : beatMs > 700 ? beatMs - 700 : 0), + halfInWindow: beatMs * 2 <= 2000, + barAtCeiling: Math.abs(beatMs * 4 - 2000) < 200, + chordCycleFelt: beatMs * 8 <= 2000, + }, + bpmBySection, + tempoArch: { verse1Sag: r1(bpmBySection.verse1.median - bpmMed), chorus1Lift: r1(bpmBySection.chorus1.median - bpmMed), bridge: r1(bpmBySection.bridge.median - bpmMed) }, +}; + +// ── 4. HARMONY · harmonic rhythm and where the IV actually is ───────────────── +const chordRuns = []; +for (const b of bars) { + const last = chordRuns[chordRuns.length - 1]; + if (last && last.chord === b.chord) { last.bars++; last.to = b.n; } else chordRuns.push({ chord: b.chord, from: b.n, to: b.n, bars: 1 }); +} +const runLen = {}; +for (const r of chordRuns) { runLen[r.chord] = runLen[r.chord] || []; runLen[r.chord].push(r.bars); } +out.harmony = { + key: A.key[0], tuningCents: A.tuningCents, + vocabulary: { "G#m": "vi (the Cmaj7 shape through the open low string: Emaj7/G#)", "B": "I", "Emaj7": "IV" }, + barCounts: bars.reduce((o, b) => ((o[b.chord] = (o[b.chord] || 0) + 1), o), {}), + runs: chordRuns, + runLengths: Object.fromEntries(Object.entries(runLen).map(([c, l]) => [c, { runs: l.length, median: median(l), max: Math.max(...l) }])), + harmonicRhythmBeats: r1(mean(chordRuns.map((r) => r.bars)) * 4), + ivBars: bars.filter((b) => b.chord === "Emaj7").map((b) => `${b.n}:${sectionOfBar(b.n)}`), +}; + +// ── 5. MELODY · her tuned line (vox-notes.json) ─────────────────────────────── +// her voice enters at ~24 s; tuned "notes" before that are Demucs bleed of the guitar +const notes = V.notes.filter((n) => n.t >= 23); + +const TONIC = 56; // G#3 in the guitar frame +const degName = ["1", "b2", "2", "b3", "3", "4", "b5", "5", "b6", "6", "b7", "7"]; +// the hook: most repeated 4-note degree sequence (by count, ignoring octave) +const degSeq = notes.map((n) => degName[((n.midi - 56) % 12 + 12) % 12]); +const grams = {}; +for (let i = 0; i + 4 <= degSeq.length; i++) { const g = degSeq.slice(i, i + 4).join(" "); grams[g] = (grams[g] || 0) + 1; } +const topGrams = Object.entries(grams).sort((x, y) => y[1] - x[1]).slice(0, 6); +const degHist = {}; +for (const n of notes) { const d = degName[((n.midi - TONIC) % 12 + 12) % 12]; degHist[d] = (degHist[d] || 0) + n.dur; } +const totalDur = notes.reduce((s, n) => s + n.dur, 0); +const degShare = Object.fromEntries(Object.entries(degHist).sort((a, b) => b[1] - a[1]).map(([d, v]) => [d, r3(v / totalDur)])); +const intervals = notes.slice(1).map((n, i) => (n.t - (notes[i].t + notes[i].dur) < 0.6 ? n.midi - notes[i].midi : null)).filter((x) => x !== null); +const intHist = {}; +for (const i of intervals) intHist[i] = (intHist[i] || 0) + 1; +const steps = intervals.filter((i) => Math.abs(i) <= 2 && i !== 0).length, leaps = intervals.filter((i) => Math.abs(i) > 2).length, repeats = intervals.filter((i) => i === 0).length; +// phrases: the tuned notes run contiguous through vowels, so breaths are read +// off the dry vocal stem: 10 ms RMS, a phrase ends at a gap of ≥ 0.35 s under +// the gate, and must hold ≥ 0.3 s. +function phrasesFromStem(path, { gapS = 0.35, minS = 0.3 } = {}) { + if (!existsSync(path)) return []; + const { samples: x, sampleRate: sr } = readWavMono(path); + const hop = Math.floor(sr / 100), nf = Math.floor(x.length / hop); + const db = new Float32Array(nf); + for (let f = 0; f < nf; f++) { let s = 0; for (let i = f * hop; i < (f + 1) * hop; i++) s += x[i] * x[i]; db[f] = 10 * Math.log10(s / hop + 1e-12); } + const gate = pct(Array.from(db).filter((v) => v > -70), 0.85) - 10; // 10 dB under the loud frames: swept 6–22, this is the knee where breaths open and words do not + const on = Array.from(db, (v) => v > gate); + const res = []; let start = null, lastOn = null; + for (let f = 0; f <= nf; f++) { + const v = f < nf && on[f]; + if (v) { if (start === null) start = f; lastOn = f; } + else if (start !== null && (f - lastOn) / 100 >= gapS) { if ((lastOn - start) / 100 >= minS) res.push({ start: start / 100, end: (lastOn + 1) / 100 }); start = null; } + } + if (start !== null && (lastOn - start) / 100 >= minS) res.push({ start: start / 100, end: (lastOn + 1) / 100 }); + return res.filter((p) => p.start > 20); // her voice enters at ~24 s; before that is bleed +} +const phrases = phrasesFromStem(resolve(LANE, "src/vox/vocals-dry-48k.wav")).map((p) => ({ ...p, notes: notes.filter((n) => n.t + n.dur / 2 >= p.start && n.t + n.dur / 2 <= p.end) })).filter((p) => p.notes.length); +const phraseRows = phrases.map((p) => { + const b = barOf(p.start); + const m = p.notes.map((n) => n.midi); + const bt = b ? b.beats.slice(0, -1) : []; + const beatIn = bt.length ? bt.reduce((best, t, i) => (Math.abs(t - p.start) < Math.abs(bt[best] - p.start) ? i : best), 0) : null; + return { start: r1(p.start), bar: b?.n, section: b ? sectionOfBar(b.n) : null, beatIn: beatIn === null ? null : beatIn + 1, offsetMs: beatIn === null ? null : Math.round((p.start - bt[beatIn]) * 1000), beats: r1((p.end - p.start) / (beatMs / 1000)), notes: m.length, lo: Math.min(...m), hi: Math.max(...m), first: m[0], last: m[m.length - 1], degreeLast: degName[((m[m.length - 1] - TONIC) % 12 + 12) % 12], contour: m.length > 1 ? (m[m.length - 1] > m[0] ? "rise" : m[m.length - 1] < m[0] ? "fall" : "level") : "one" }; +}); +// register per section (R8 chamber-01): median midi of sung notes +const registerBySection = Object.fromEntries(SECTIONS.map(([s, a, b]) => { + const rows = notes.filter((n) => { const bb = barOf(n.t); return bb && bb.n >= a && bb.n <= b; }); + if (!rows.length) return [s, null]; + const m = rows.map((n) => n.midi); + return [s, { notes: rows.length, medianMidi: median(m), lo: Math.min(...m), hi: Math.max(...m), meanDurMs: r1(mean(rows.map((n) => n.dur)) * 1000) }]; +})); +// chord-tone share: over each bar's chord, is the sung pitch class a chord tone? +const CHORD_PCS = { "G#m": [8, 11, 3], "B": [11, 3, 6], "Emaj7": [4, 8, 11, 3] }; +let ct = 0, nct = 0; const nctByChord = {}; +for (const n of notes) { + const b = barOf(n.t); if (!b) continue; + const pcs = CHORD_PCS[b.chord]; const pc = n.midi % 12; + const isCt = pcs.includes(pc); + if (isCt) ct += n.dur; else { nct += n.dur; nctByChord[b.chord] = (nctByChord[b.chord] || 0) + n.dur; } +} +// harmony stems: which diatonic shift keeps the most chord tones over her chords +const SCALE = [8, 10, 11, 1, 3, 4, 6]; // G# natural minor pcs +const shiftDeg = (midi, k) => { const pc = midi % 12; let i = SCALE.indexOf(pc); if (i < 0) return null; let j = i + k, oct = 0; while (j < 0) { j += 7; oct--; } while (j >= 7) { j -= 7; oct++; } return midi - pc + SCALE[j] + 12 * oct; }; +const harmonyFit = Object.fromEntries([2, -2, 4, -5, -7].map((k) => { + let hit = 0, tot = 0; + for (const n of notes) { const b = barOf(n.t); if (!b) continue; const m = shiftDeg(n.midi, k); if (m == null) continue; tot += n.dur; if (CHORD_PCS[b.chord].includes(((m % 12) + 12) % 12)) hit += n.dur; } + return [`shift${k > 0 ? "+" : ""}${k}`, r3(hit / tot)]; +})); +out.melody = { + notes: notes.length, tonic: "G#3 = midi 56 (guitar frame)", + range: { lo: Math.min(...notes.map((n) => n.midi)), hi: Math.max(...notes.map((n) => n.midi)) }, + degreeShareByDuration: degShare, + motion: { steps, leaps, repeats, stepShare: r3(steps / (steps + leaps + repeats)), intervalHist: intHist }, + phrases: phraseRows, + phraseBeats: { median: median(phraseRows.map((p) => p.beats)), p90: pct(phraseRows.map((p) => p.beats), 0.9) }, + cadenceDegrees: phraseRows.reduce((o, p) => ((o[p.degreeLast] = (o[p.degreeLast] || 0) + 1), o), {}), + registerBySection, + topDegree4grams: topGrams, + phrasesBySection: phraseRows.reduce((o, p) => ((o[p.section] = (o[p.section] || 0) + 1), o), {}), + chordToneShare: r3(ct / (ct + nct)), nonChordToneByChord: Object.fromEntries(Object.entries(nctByChord).map(([c, v]) => [c, r1(v)])), + harmonyStemChordToneShare: harmonyFit, +}; + +// ── 6. FORM · proportions against chamber-01 R1/R5/R6/R12 and pop-strategies ─ +const secRows = SECTIONS.map(([s, a, b]) => { + const rows = bars.filter((x) => x.n >= a && x.n <= b); + const start = rows[0].t, end = rows[rows.length - 1].t + rows[rows.length - 1].dur; + return { section: s, bars: rows.length, from: a, to: b, start: r1(start), end: r1(end), frac: r3(start / DUR) }; +}); +// loudness envelope per bar from the stems (dB RMS), for where HER climax is +function rmsPerBar(path) { + if (!existsSync(path)) return null; + const { samples: x, sampleRate: sr } = readWavMono(path); + return bars.map((b) => { const a = Math.floor(b.t * sr), z = Math.min(x.length, Math.floor((b.t + b.dur) * sr)); let s = 0; for (let i = a; i < z; i++) s += x[i] * x[i]; return r1(10 * Math.log10(s / Math.max(1, z - a) + 1e-12)); }); +} +const voxRms = rmsPerBar(resolve(LANE, "src/vox/vocals-dry-48k.wav")); +const gtrRms = rmsPerBar(resolve(LANE, "src/vox/guitar-48k.wav")); +const loudest = voxRms ? voxRms.map((v, i) => [v, i + 1]).sort((p, q) => q[0] - p[0]).slice(0, 8).map(([v, n]) => ({ bar: n, section: sectionOfBar(n), t: r1(bars[n - 1].t), dB: v })) : null; +const voxBySection = voxRms ? Object.fromEntries(SECTIONS.map(([s, a, b]) => [s, r1(mean(voxRms.slice(a - 1, b)))])) : null; +const gtrBySection = gtrRms ? Object.fromEntries(SECTIONS.map(([s, a, b]) => [s, r1(mean(gtrRms.slice(a - 1, b)))])) : null; +const climaxBar = loudest ? loudest[0].bar : null; +const climaxSpan = loudest ? (() => { // longest run of bars within 3 dB of the peak + const peak = loudest[0].dB; let best = [0, 0], cur = null; + voxRms.forEach((v, i) => { if (v >= peak - 3) { if (!cur) cur = [i + 1, i + 1]; else cur[1] = i + 1; if (cur[1] - cur[0] >= best[1] - best[0]) best = [...cur]; } else cur = null; }); + return best; +})() : null; +out.form = { + durationS: DUR, bars: bars.length, + sections: secRows, + phraseLengthCheck: secRows.map((s) => ({ section: s.section, bars: s.bars, inR1Set: [8, 12, 16, 24].includes(s.bars) })), + rules: { + R5climaxAt: { frac: 0.71, t: r1(0.71 * DUR), bar: barOf(0.71 * DUR)?.n, section: sectionOfBar(barOf(0.71 * DUR)?.n) }, + R6stillnessAt: { frac: 0.36, t: r1(0.36 * DUR), bar: barOf(0.36 * DUR)?.n, section: sectionOfBar(barOf(0.36 * DUR)?.n) }, + popBuildLayerEntries: [0.10, 0.28, 0.40, 0.62, 0.78].map((f) => ({ frac: f, bar: Math.round(1 + f * (bars.length - 1)), section: sectionOfBar(Math.round(1 + f * (bars.length - 1))) })), + }, + measured: { + voxDbBySection: voxBySection, gtrDbBySection: gtrBySection, + loudestVoxBars: loudest, + herClimax: climaxBar ? { bar: climaxBar, t: r1(bars[climaxBar - 1].t), frac: r3(bars[climaxBar - 1].t / DUR), section: sectionOfBar(climaxBar), plateauBars: climaxSpan } : null, + voxRmsPerBar: voxRms, gtrRmsPerBar: gtrRms, + }, +}; + +// ── 7. THE CUT · pop-strategies: tracks run ~1:30; remove repeated statements ─ +// Candidate edit: seams on downbeats (10 ms fades). Bars dropped: intro 1–6 +// (keep 4), verse2 44–51 (a restatement), chorus2 52–67 (a restatement). +const keep = [[7, 43], [68, 84]]; +const cutLen = keep.reduce((s, [a, b]) => s + bars.slice(a - 1, b).reduce((t, x) => t + x.dur, 0), 0); +out.cut = { + rule: "chamber-04 form facts: tracks run about 1:30; wannadash went 3:28 → 1:54 by removing repeated statements, seams on downbeats with 10 ms fades", + keepBars: keep, dropBars: [[1, 6], [44, 67]], + lengthS: r1(cutLen), lengthMMSS: `${Math.floor(cutLen / 60)}:${String(Math.round(cutLen % 60)).padStart(2, "0")}`, + seams: keep.slice(1).map(([a]) => ({ bar: a, t: bars[a - 1].t, chord: bars[a - 1].chord, prevChord: bars[keep[0][1] - 1].chord })), + note: "verse2 + chorus2 restate verse1 + chorus1 on the same chords; the break (68–71) then lands right after chorus1 — check the lyric carries", +}; + +// ── 8. BASS · register, tempo band, delay in beats (bass-01 rules) ──────────── +const hz = (m) => 440 * 2 ** ((m - 69) / 12) * 2 ** (A.tuningCents / 1200); +out.bass = { + tempoBand: bpmMed >= 110 && bpmMed <= 125 ? "dub techno 110–125 (R12)" : "outside the platter's bands", + subRoots: { "G#1 (midi 32)": r1(hz(32)), "B1 (midi 35)": r1(hz(35)), "E1 (midi 28)": r1(hz(28)), "E2 (midi 40)": r1(hz(40)) }, + R1: "sub 30–60 Hz octave (C1–C2), mono, sine plus 2nd/3rd harmonic; B1 at 62 Hz sits at the top of the band — E1 and G#1 are the sub's home", + R3: `kit is kick 1 / snare 3 at ${r1(bpmMed)}: the dubstep half-time layout; a one-drop variant leaves beat 1 empty and puts kick + cross-stick on 3`, + R4: "first bass entry late: at least one 8-bar phrase of drums and space before it (her intro is 10 bars — the sub can wait until bar 11)", + R5: { delayMs: r1(0.93 * beatMs), repeats: "4–5", note: "0.93 of a beat, so it revolves around her beat rather than locking; in follow mode compute it per bar from measures.json" }, + R8: "duck the sub 2–3 dB per kick, fastest attack; her guitar's low G#2 (104 Hz) and B2 (124 Hz) share the kick body band 100–200 Hz — high-pass the guitar or thin the kick body", + R9: "mono below 100 Hz; stereo only on returns and on the sines above 100 Hz", + R10: "a harmonic copy of the sub high-passed at 150 Hz carrying 250–700 Hz so the note survives a phone", + phoneBand: "MASTERING.md: loudest 8 s may lose ≤ 5 dB through the mono 180 Hz–8 kHz proxy", +}; + +writeFileSync(resolve(LANE, "platter-analysis.json"), JSON.stringify(out, null, 1)); + +// ── stdout digest ───────────────────────────────────────────────────────────── +const P = (s) => process.stdout.write(s + "\n"); +P(`RHYTHM motif ${motif} IOI (${an.ioi}) k=${an.k} n=${an.n} euclidean=${an.euclidean} periodic=${N.isPeriodic(motif)} evenness=${r3(an.evenness)} balanced=${an.balance.balanced}`); +P(` syncopation ${JSON.stringify(N.syncopationProfile(motif))}`); +for (const [k, v] of Object.entries(distances)) P(` → ${k.padEnd(32)} ${v.box} chronotonic ${v.chronotonic}${v.swap != null ? ` swap ${v.swap}` : ""}`); +P(` morph to straight 8ths: ${out.rhythm.morphToStraight.join(" → ")}`); +P(` bars verbatim on the motif: ${patternCounts[motif]}/${motifBars.length}; drift by section: ${Object.entries(driftBySection).map(([s, v]) => `${s} ${v.meanChronotonic}`).join(", ")}`); +P(`BINS off-beat strum phase median ${out.beatBins.offbeatPhaseMedian} (p10 ${out.beatBins.offbeatPhaseP10}, p90 ${out.beatBins.offbeatPhaseP90}) swing ${out.beatBins.swingRatio}:1 & is ${out.beatBins.offbeatLateMsAt120} ms late at 120`); +P(` voice vs her 8th grid: signed median ${out.beatBins.voiceVsGrid.all.signedMedianMs} ms (abs ${out.beatBins.voiceVsGrid.all.absMedianMs}); by section ${Object.entries(voiceBySection).map(([k, v]) => `${k} ${v.signedMedianMs > 0 ? '+' : ''}${v.signedMedianMs}`).join(', ')}; long notes ${out.beatBins.voiceVsGrid.longNotesSignedMs}, short notes ${out.beatBins.voiceVsGrid.shortNotesSignedMs}`); +P(`ENTRAIN quarter ${out.entrainment.periodsMs.quarter} ms (sweet spot 500–700: ${out.entrainment.felt.quarterMsFromSweetSpot} ms off), bar ${out.entrainment.periodsMs.bar} ms (at ceiling ${out.entrainment.felt.barAtCeiling}), 2-bar chord cycle ${out.entrainment.periodsMs.chordCycle2bars} ms felt as rhythm: ${out.entrainment.felt.chordCycleFelt}`); +P(` bpm by section: ${Object.entries(bpmBySection).map(([s, v]) => `${s} ${v.median}`).join(", ")}`); +P(`HARMONY bars ${JSON.stringify(out.harmony.barCounts)} harmonic rhythm ${out.harmony.harmonicRhythmBeats} beats run lengths ${JSON.stringify(out.harmony.runLengths)}`); +P(` IV (Emaj7) bars: ${out.harmony.ivBars.join(" ")}`); +P(`MELODY ${notes.length} notes ${out.melody.range.lo}–${out.melody.range.hi} degrees by duration ${JSON.stringify(degShare)}`); +P(` motion steps ${steps} leaps ${leaps} repeats ${repeats} (step share ${out.melody.motion.stepShare}) phrases ${phraseRows.length}, median ${out.melody.phraseBeats.median} beats cadences ${JSON.stringify(out.melody.cadenceDegrees)}`); +P(` phrase starts by beat-in-bar ${JSON.stringify(phraseRows.reduce((o, p) => ((o[p.beatIn] = (o[p.beatIn] || 0) + 1), o), {}))} start offset ms median ${median(phraseRows.map((p) => p.offsetMs))} contours ${JSON.stringify(phraseRows.reduce((o, p) => ((o[p.contour] = (o[p.contour] || 0) + 1), o), {}))}`); +for (const p of phraseRows) P(` ${String(p.start).padStart(6)}s bar ${String(p.bar).padStart(2)} beat ${p.beatIn} ${String(p.offsetMs).padStart(4)}ms ${p.section.padEnd(7)} ${String(p.beats).padStart(4)} beats ${String(p.notes).padStart(2)} notes ${p.lo}–${p.hi} ${p.contour.padEnd(5)} ends on ${p.degreeLast}`); +P(` register by section: ${Object.entries(registerBySection).filter(([, v]) => v).map(([s, v]) => `${s} ${v.medianMidi} (${v.lo}–${v.hi})`).join(", ")}`); +P(` hook 4-grams ${JSON.stringify(topGrams)} phrases by section ${JSON.stringify(out.melody.phrasesBySection)}`); +P(` chord-tone share ${out.melody.chordToneShare} non-chord-tone seconds by chord ${JSON.stringify(out.melody.nonChordToneByChord)} harmony stems chord-tone share ${JSON.stringify(harmonyFit)}`); +P(`FORM ${secRows.map((s) => `${s.section} ${s.bars}b @${s.start}s (${s.frac})`).join(" · ")}`); +P(` R5 climax at 0.71 → ${out.form.rules.R5climaxAt.t}s bar ${out.form.rules.R5climaxAt.bar} (${out.form.rules.R5climaxAt.section}); R6 stillness at 0.36 → ${out.form.rules.R6stillnessAt.t}s bar ${out.form.rules.R6stillnessAt.bar} (${out.form.rules.R6stillnessAt.section})`); +if (out.form.measured.herClimax) P(` her loudest bar ${out.form.measured.herClimax.bar} (${out.form.measured.herClimax.section}) at ${out.form.measured.herClimax.t}s = ${out.form.measured.herClimax.frac}; plateau bars ${out.form.measured.herClimax.plateauBars}`); +if (voxBySection) P(` vox dB by section ${JSON.stringify(voxBySection)}`); +if (gtrBySection) P(` gtr dB by section ${JSON.stringify(gtrBySection)}`); +P(`CUT keep ${JSON.stringify(keep)} → ${out.cut.lengthMMSS}; seam at bar ${out.cut.seams[0].bar} (${out.cut.seams[0].prevChord} → ${out.cut.seams[0].chord})`); +P(`BASS ${out.bass.tempoBand}; sub roots ${JSON.stringify(out.bass.subRoots)}; delay ${out.bass.R5.delayMs} ms`); +P(`✓ ${resolve(LANE, "platter-analysis.json")}`); diff --git a/pop/sailor-song/bin/regularize.mjs b/pop/sailor-song/bin/regularize.mjs new file mode 100644 index 0000000000..587a0e3c08 --- /dev/null +++ b/pop/sailor-song/bin/regularize.mjs @@ -0,0 +1,99 @@ +#!/usr/bin/env node +// regularize.mjs — damp her tempo curve once the floor arrives. +// +// Her clock swings 113 → 130 BPM (ANALYSIS.md §3). In the bedroom opening +// that rubato is the performance and is left alone. From FLOOR_BAR on, the +// dance floor wants a steadier pulse: each beat's local BPM is smoothed over +// ±8 beats and pulled KEEP of the way from the median — so a third of her +// sway remains, and the stretch ratio never leaves ±6%, which rubberband R3 +// handles on a voice without artefacts. One time map (beat → beat) is run +// through rubberband on every stem the engine plays (the locked lead, halo +// and harmonies, her guitar, the replay guitars), and the same map produces +// measures.reg.json for the chart. Timing only; pitch is untouched. +// +// node pop/sailor-song/bin/regularize.mjs [--keep 0.3] [--floor-bar 24] [--jobs 4] +// → src/vox/reg/*.wav + src/vox/reg/timemap.txt + measures.reg.json + +import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawn } from "node:child_process"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const LANE = resolve(HERE, ".."); +const VOX = resolve(LANE, "src/vox"); +const OUT = resolve(VOX, "reg"); +mkdirSync(OUT, { recursive: true }); +const SR = 48_000; + +const flags = {}; +for (let i = 2; i < process.argv.length; i++) { const a = process.argv[i]; if (a.startsWith("--")) flags[a.slice(2)] = process.argv[++i]; } +const KEEP = Number(flags.keep ?? 0.3); +const FLOOR_BAR = Number(flags["floor-bar"] ?? 24); +const JOBS = Number(flags.jobs ?? 4); +const WIN = 8; +// v19: the song kicks off at verse 2 — every bar that plays after verse 1 in the record +// (source bars 28 on: verse 2, both choruses, break, bridge, outro) runs LIFT faster +const LIFT = Number(flags.lift ?? 1.05), LIFT_BAR = Number(flags["lift-bar"] ?? 28); + +const M = JSON.parse(readFileSync(resolve(LANE, "measures.json"), "utf8")); +const bars = M.bars; +// the full beat list: every bar's beats without its closing downbeat, plus the final end +const src = bars.flatMap((b) => b.beats.slice(0, -1)); +src.push(bars.at(-1).beats.at(-1)); +const barOfBeat = []; bars.forEach((b) => { for (let j = 0; j < b.beats.length - 1; j++) barOfBeat.push(b.n); }); barOfBeat.push(bars.at(-1).n); + +const ibi = src.slice(1).map((t, i) => t - src[i]); +const bpm = ibi.map((d) => 60 / d); +const sorted = [...bpm].sort((a, b) => a - b), median = sorted[sorted.length >> 1]; +const smooth = bpm.map((_, i) => { const a = Math.max(0, i - WIN), z = Math.min(bpm.length, i + WIN + 1); return bpm.slice(a, z).reduce((s, v) => s + v, 0) / (z - a); }); +// how much of her sway to keep per beat: all of it before the floor, KEEP after, eased over 4 bars before FLOOR_BAR +const keepAt = (i) => { const n = barOfBeat[i]; if (n >= FLOOR_BAR) return KEEP; if (n < FLOOR_BAR - 4) return 1; const x = (n - (FLOOR_BAR - 4)) / 4; return 1 + (KEEP - 1) * (0.5 - 0.5 * Math.cos(Math.PI * x)); }; +const target = bpm.map((v, i) => { const k = keepAt(i); return (k >= 1 ? v : median + k * (smooth[i] - median)) * (barOfBeat[i] >= LIFT_BAR ? LIFT : 1); }); +const dst = [src[0]]; +for (let i = 0; i < target.length; i++) dst.push(dst[i] + 60 / target[i]); +const ratio = ibi.map((d, i) => (60 / target[i]) / d); +const dur = 179.14; +const total = dst.at(-1) + (dur - src.at(-1)); + +// the map, in samples; identity before the first beat and after the last +const pairs = [[0, 0], ...src.map((s, i) => [s, dst[i]]), [dur, total]]; +const mapPath = resolve(OUT, "timemap.txt"); +writeFileSync(mapPath, pairs.map(([s, d]) => `${Math.round(s * SR)} ${Math.round(d * SR)}`).join("\n") + "\n"); +const warp = (t) => { let k = pairs.findIndex(([s]) => s > t); if (k <= 0) k = 1; const [s0, d0] = pairs[k - 1], [s1, d1] = pairs[k]; return d0 + ((t - s0) / (s1 - s0)) * (d1 - d0); }; + +// measures.reg.json: the same bars on the new clock +const reg = { ...M, regularized: { keep: KEEP, floorBar: FLOOR_BAR, medianBpm: Math.round(median * 10) / 10, totalSec: Math.round(total * 1000) / 1000 }, + beats: { ...M.beats, snapped: M.beats.snapped.map(warp) }, + bars: bars.map((b) => { const beats = b.beats.map(warp); const t = beats[0], d = beats.at(-1) - t; return { ...b, t: +t.toFixed(3), dur: +d.toFixed(3), beats: beats.map((x) => +x.toFixed(3)), bpm: +(60 * (beats.length - 1) / d).toFixed(1) }; }), + strumList: M.strumList.map((s) => ({ ...s, t: +warp(s.t).toFixed(3) })) }; +writeFileSync(resolve(LANE, "measures.reg.json"), JSON.stringify(reg, null, 1)); + +const sec = (a, z) => { const r = reg.bars.filter((b) => b.n >= a && b.n <= z).map((b) => b.bpm).sort((x, y) => x - y); return r[r.length >> 1]; }; +const was = (a, z) => { const r = bars.filter((b) => b.n >= a && b.n <= z).map((b) => b.bpm).sort((x, y) => x - y); return r[r.length >> 1]; }; +console.log(`median ${median.toFixed(1)} BPM · keep ${KEEP} from bar ${FLOOR_BAR} · stretch ratio ${Math.min(...ratio).toFixed(3)}–${Math.max(...ratio).toFixed(3)} · ${dur.toFixed(1)} → ${total.toFixed(1)} s`); +for (const [n, a, z] of [["intro", 1, 10], ["verse1", 11, 27], ["chorus1", 28, 43], ["verse2", 44, 51], ["chorus2", 52, 67], ["break", 68, 71], ["bridge", 72, 80], ["outro", 81, 84]]) console.log(` ${n.padEnd(8)} ${was(a, z)} → ${sec(a, z)}`); + +// stems through the same map +// v17: the voice stems are taken UNLOCKED (src/vox/*.wav) — the lock's stretch put a +// pre-echo on hard consonants ("k-kiss"); her placement stays as sung. The lock map is +// written as identity so every downstream tool composes the same way. +const stems = ["vocals-natural.wav", "vocals-aesthetivox.wav", "vocals-halo.wav", "harm-up3.wav", "harm-down3.wav", "harm-up5.wav", "harm-down6.wav", "harm-down8.wav", "harm-up8.wav", "guitar-48k.wav", "replay-acoustic.wav", "replay-electric.wav"].filter((f) => existsSync(resolve(VOX, f))); +mkdirSync(resolve(VOX, "locked"), { recursive: true }); +writeFileSync(resolve(VOX, "locked/timemap.txt"), `0 0\n${Math.round(dur * SR)} ${Math.round(dur * SR)}\n`); +const queue = [...stems]; +let running = 0, failed = false; +await new Promise((done) => { + const next = () => { + if (failed) return done(); + if (!queue.length) { if (!running) done(); return; } + const f = queue.shift(); running++; + const out = resolve(OUT, f.split("/").pop()); + const p = spawn("rubberband", ["-3", "-F", "-q", "-D", total.toFixed(4), "-M", mapPath, resolve(VOX, f), out], { stdio: ["ignore", "ignore", "pipe"] }); + let err = ""; p.stderr.on("data", (d) => (err += d)); + p.on("close", (code) => { running--; if (code !== 0) { console.error(`rubberband failed on ${f}\n${err}`); failed = true; } else console.log(` reg ${f}`); next(); }); + }; + for (let i = 0; i < JOBS; i++) next(); +}); +if (failed) process.exit(1); +console.log(`✓ ${OUT}`); diff --git a/pop/sailor-song/bin/render-draft.mjs b/pop/sailor-song/bin/render-draft.mjs new file mode 100644 index 0000000000..58dcab7c53 --- /dev/null +++ b/pop/sailor-song/bin/render-draft.mjs @@ -0,0 +1,210 @@ +#!/usr/bin/env node +// render-draft.mjs — put drums + a bed under the niece's Sailor Song take, +// two ways, so they can be A/B'd: +// +// follow — her audio untouched; every hit lands on *her* detected beat +// (take.analysis.json pulse.beats), so the kit breathes with +// her rubato. +// warp — rubberband time-maps the whole take onto a fixed grid in one +// pass (-M timemap: each detected beat → k·60/BPM), then the +// kit plays straight on that grid. +// +// Same kit, bed, and excerpt either way — only the clock differs. Kit is +// the booch inline boom-bap synthesis softened for a ballad; bed is the +// booch sub + mellow rhodes on the 2-bar chord cycle, detuned +13¢ to sit +// on her guitar. Bottom-up posture: nothing sampled but her. +// +// node pop/sailor-song/bin/render-draft.mjs --mode follow +// node pop/sailor-song/bin/render-draft.mjs --mode warp --bpm 120 +// node pop/sailor-song/bin/render-draft.mjs --mode follow --from 0 --to 179 + +import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; + +import { mixEventSub } from "../../booch/synths/sub.mjs"; +import { mixEventRhodes } from "../../booch/synths/rhodes.mjs"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const LANE = resolve(HERE, ".."); +const SR = 48_000; + +const flags = {}; +for (let i = 2; i < process.argv.length; i++) { + const a = process.argv[i]; + if (!a.startsWith("--")) continue; + const n = process.argv[i + 1]; + if (n !== undefined && !n.startsWith("--")) { flags[a.slice(2)] = n; i++; } else flags[a.slice(2)] = true; +} +const MODE = flags.mode || "follow"; +const FROM = Number(flags.from ?? 20); // excerpt, in *source* seconds +const TO = Number(flags.to ?? 80); +const BPM = Number(flags.bpm ?? 120); // warp grid +const BED = Number(flags.bed ?? 0.5); // bed level under her +const TUNE = 0.13; // her guitar sits +13¢ sharp + +const A = JSON.parse(readFileSync(resolve(LANE, "take.analysis.json"), "utf8")); +const SRC_BEATS = A.pulse.beats; +const TAKE = resolve(LANE, "src/take.wav"); +const OUT = resolve(LANE, "out"); +mkdirSync(OUT, { recursive: true }); + +// ── wav io (16/32-bit PCM or float in, float32 stereo out) ─────────────── +function readWav(path) { + const b = readFileSync(path); + let p = 12, fmt, data; + while (p < b.length) { + const id = b.toString("ascii", p, p + 4), sz = b.readUInt32LE(p + 4); + if (id === "fmt ") fmt = { tag: b.readUInt16LE(p + 8), ch: b.readUInt16LE(p + 10), sr: b.readUInt32LE(p + 12), bits: b.readUInt16LE(p + 22) }; + if (id === "data") data = b.subarray(p + 8, p + 8 + sz); + p += 8 + sz + (sz & 1); + } + const n = data.length / (fmt.bits / 8) / fmt.ch; + const L = new Float32Array(n), R = new Float32Array(n); + for (let i = 0; i < n; i++) { + for (let c = 0; c < 2; c++) { + const k = i * fmt.ch + Math.min(c, fmt.ch - 1); + const v = fmt.bits === 16 ? data.readInt16LE(k * 2) / 32768 + : fmt.tag === 3 ? data.readFloatLE(k * 4) : data.readInt32LE(k * 4) / 2147483648; + (c ? R : L)[i] = v; + } + } + return { sr: fmt.sr, L, R }; +} + +function writeWav(path, L, R) { + const n = L.length, b = Buffer.alloc(44 + n * 4); + b.write("RIFF", 0); b.writeUInt32LE(36 + n * 4, 4); b.write("WAVEfmt ", 8); + b.writeUInt32LE(16, 16); b.writeUInt16LE(1, 20); b.writeUInt16LE(2, 22); + b.writeUInt32LE(SR, 24); b.writeUInt32LE(SR * 4, 28); b.writeUInt16LE(4, 32); + b.writeUInt16LE(16, 34); b.write("data", 36); b.writeUInt32LE(n * 4, 40); + for (let i = 0; i < n; i++) { + b.writeInt16LE(Math.round(Math.max(-1, Math.min(1, L[i])) * 32767), 44 + i * 4); + b.writeInt16LE(Math.round(Math.max(-1, Math.min(1, R[i])) * 32767), 46 + i * 4); + } + writeFileSync(path, b); +} + +// ── the clock ──────────────────────────────────────────────────────────── +// beats[] = the times hits land on in the *output* file's timeline, plus +// the audio that goes under them. +let beats, voice, mapTime = (s) => s; +if (MODE === "warp") { + const src = readWav(TAKE); + const spb = 60 / BPM; + const t0 = SRC_BEATS[0]; + const map = SRC_BEATS.map((s, k) => [s, t0 + k * spb]); + const endSrc = src.L.length / src.sr; + const endDst = map.at(-1)[1] + (endSrc - map.at(-1)[0]); + const mapPath = resolve(OUT, `timemap-${BPM}.txt`); + writeFileSync(mapPath, map.map(([s, d]) => `${Math.round(s * src.sr)} ${Math.round(d * src.sr)}`).join("\n") + "\n"); + const warped = resolve(OUT, `take-warp-${BPM}.wav`); + if (!existsSync(warped) || flags.rewarp) { + console.log(`warping take → ${BPM} BPM grid (rubberband R3, timemap of ${map.length} beats)`); + const r = spawnSync("rubberband", ["-3", "-F", "-D", endDst.toFixed(4), "-M", mapPath, TAKE, warped], { stdio: "inherit" }); + if (r.status !== 0) process.exit(1); + } + voice = readWav(warped); + beats = SRC_BEATS.map((_, k) => t0 + k * spb); + // Excerpt bounds follow the same map, so both drafts cover the same song. + const toDst = (s) => { let k = SRC_BEATS.findIndex((b) => b > s); if (k <= 0) k = 1; + const [s0, s1] = [SRC_BEATS[k - 1], SRC_BEATS[k]], [d0, d1] = [beats[k - 1], beats[k]]; + return d0 + ((s - s0) / (s1 - s0)) * (d1 - d0); }; + mapTime = toDst; +} else { + voice = readWav(TAKE); + beats = SRC_BEATS.slice(); +} +const from = mapTime(FROM), to = mapTime(TO); + +// ── harmony per bar (bar = 4 beats; downbeats are beats[4k]) ───────────── +const perBeat = []; +for (const r of A.chords.runs) for (let i = 0; i < r.beats; i++) perBeat.push(r.chord); +const ROOT = { E: 40, "G#": 44, "G#m": 44, B: 47, A: 45 }; +function barChord(k) { + // perBeat[i] covers bounds[i] = [0, ...beats][i]; beat j starts at perBeat[j+1]. + const tally = {}; + for (let j = 4 * k; j < 4 * k + 4; j++) { + const c = (perBeat[j + 1] || "").replace(/(maj7|m7|7|sus4|sus2)$/, ""); + if (c in ROOT) tally[c] = (tally[c] || 0) + 1; + } + return Object.entries(tally).sort((a, b) => b[1] - a[1])[0]?.[0] || null; +} +// Pad voicings avoid the G#-vs-G#m third (unresolved by ear yet): G# +// chords get root–fifth–octave–ninth; E and B carry their thirds. +const PAD = { E: [52, 56, 59, 63], B: [59, 63, 66, 70], "G#": [56, 63, 68, 70], "G#m": [56, 63, 68, 70], A: [57, 61, 64, 68] }; + +// ── kit (booch inline synthesis, softened) ─────────────────────────────── +const n = Math.ceil((to - from + 3) * SR); +const dL = new Float32Array(n), dR = new Float32Array(n); +const bus = new Float32Array(n); +const at = (t) => Math.floor((t - from) * SR); +function add(buf, i, v) { if (i >= 0 && i < buf.length) buf[i] += v; } +function kick(t, g) { + const s0 = at(t); let ph = 0; + for (let i = 0; i < 0.34 * SR; i++) { const u = i / SR; + ph += (2 * Math.PI * (48 + 52 * Math.exp(-u * 30))) / SR; + const v = Math.sin(ph) * Math.exp(-u * 8) * g; add(dL, s0 + i, v); add(dR, s0 + i, v); } +} +function snare(t, g) { + const s0 = at(t); let prev = 0, lp = 0, ph = 0; + for (let i = 0; i < 0.22 * SR; i++) { const u = i / SR, nz = Math.random() * 2 - 1; + const hp = nz - prev; prev = nz; lp += (hp - lp) * 0.35; ph += (2 * Math.PI * 190) / SR; + const v = (lp * Math.exp(-u * 18) * 1.3 + Math.sin(ph) * 0.4 * Math.exp(-u * 30)) * g; + add(dL, s0 + i, v * 0.95); add(dR, s0 + i, v); } +} +function hat(t, g, pan) { + const s0 = at(t); let prev = 0; + for (let i = 0; i < 0.03 * SR; i++) { const nz = Math.random() * 2 - 1, hp = nz - prev; prev = nz; + const v = hp * Math.exp(-(i / SR) * 240) * g; add(dL, s0 + i, v * (1 - pan)); add(dR, s0 + i, v * (1 + pan)); } +} + +// ── arrangement: half-time ballad pocket ───────────────────────────────── +// At 123 the take *feels* ~61, so the backbeat snare goes on beat 3 of +// each 4-beat bar; kick on 1 and the "and" of 2; 8th hats, off-8ths +// quieter. Voice enters ~27s — before that, kick + bed only. +const VOICE_IN = mapTime(27); +const nBars = Math.floor(beats.length / 4); +for (let k = 0; k < nBars; k++) { + const b = beats.slice(4 * k, 4 * k + 5); + if (b.length < 5 || b[4] < from || b[0] > to) continue; + const full = b[0] >= VOICE_IN - 0.5; + const eighth = (j) => b[j] + (b[j + 1] - b[j]) / 2; + kick(b[0], 0.9); + if (full) { + kick(eighth(1), 0.55); + snare(b[2], 0.55); + for (let j = 0; j < 4; j++) { hat(b[j], 0.16, -0.3); hat(eighth(j), 0.09, 0.3); } + } + // bed: chords change every 2 bars — voice the pad on bar starts. + const c = barChord(k); + if (!c) continue; + const barDur = b[4] - b[0]; + mixEventSub({ startSec: b[0] - from, midi: ROOT[c] - 12 + TUNE, durSec: barDur * 0.95, gain: 0.5, preset: "funk" }, bus, { sampleRate: SR }); + for (const m of PAD[c]) { + mixEventRhodes({ startSec: b[0] - from, midi: m + TUNE, durSec: barDur * 0.98, gain: 0.16, preset: "mellow" }, bus, { sampleRate: SR, preset: "mellow" }); + } +} + +// ── mix: her take on top, bed under ────────────────────────────────────── +const L = new Float32Array(n), R = new Float32Array(n); +const vs = Math.floor(from * voice.sr); +for (let i = 0; i < n; i++) { + const vi = vs + i, vL = voice.L[vi] || 0, vR = voice.R[vi] || 0; + // 1.5s fades at both ends of the excerpt + const fi = Math.min(1, i / (1.5 * SR), (n - 3 * SR - i) / (1.5 * SR) + 1); + const f = Math.max(0, fi); + L[i] = (vL * 1.6 + (dL[i] + bus[i] * 0.8) * BED) * f; + R[i] = (vR * 1.6 + (dR[i] + bus[i] * 0.8) * BED) * f; +} +let peak = 0; +for (let i = 0; i < n; i++) peak = Math.max(peak, Math.abs(L[i]), Math.abs(R[i])); +const g = 0.89 / (peak || 1); +for (let i = 0; i < n; i++) { L[i] *= g; R[i] *= g; } + +const tag = MODE === "warp" ? `warp-${BPM}` : "follow"; +const wav = resolve(OUT, `sailor-song-${tag}.wav`); +writeWav(wav, L, R); +spawnSync("ffmpeg", ["-v", "error", "-y", "-i", wav, "-b:a", "256k", wav.replace(/\.wav$/, ".mp3")]); +console.log(`✓ ${tag}: ${(n / SR).toFixed(1)}s → ${wav.replace(/\.wav$/, ".mp3")}`); diff --git a/pop/sailor-song/bin/render.mjs b/pop/sailor-song/bin/render.mjs new file mode 100644 index 0000000000..fff67fec96 --- /dev/null +++ b/pop/sailor-song/bin/render.mjs @@ -0,0 +1,377 @@ +#!/usr/bin/env node +// render.mjs — Sailor Song, outside remix v2. FOLLOW: nothing is warped. +// +// her voice src/vox/vocals-aesthetivox.wav — WORLD-regulated, G# minor +// in the guitar's +13¢ frame (bin/aesthetivox.py) +// her guitar Demucs other+bass+drums, untouched +// the clock measures.json — bars anchored on her strum motif X..XX..X +// (bin/measures.py), so bar 1 is always her bar 1 even where +// the beat tracker slipped (27.85 s, 61.17 s) +// the kit hits land on HER strum points: kick 1 + "and of 2", snare 3, +// rim on "and of 4"; hats on 8ths from chorus 1 +// sine beds voice-led sine choir, orchestrated by section — sub, tenor +// pair under her, a high pair over her, her own register +// (G#3–G#4, median D#4) left clear. In the choruses a sine +// shadows her tuned melody a diatonic 3rd above +// (vox-notes.json); chorus 2 and the bridge add its octave. +// +// node pop/sailor-song/bin/render.mjs → out/sailor-song-v2.{wav,mp3} +// node pop/sailor-song/bin/render.mjs --from 55 --to 95 (an excerpt) + +import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const LANE = resolve(HERE, ".."); +const STEMS = resolve(LANE, "../samples/sailor-song-take/stems/htdemucs"); +const OUT = resolve(LANE, "out"); +const SR = 48_000; +mkdirSync(OUT, { recursive: true }); + +const flags = {}; +for (let i = 2; i < process.argv.length; i++) { + const a = process.argv[i]; + if (!a.startsWith("--")) continue; + const n = process.argv[i + 1]; + if (n !== undefined && !n.startsWith("--")) { flags[a.slice(2)] = n; i++; } else flags[a.slice(2)] = true; +} + +const M = JSON.parse(readFileSync(resolve(LANE, "measures.json"), "utf8")); +const VN = JSON.parse(readFileSync(resolve(LANE, "vox-notes.json"), "utf8")).notes; +const TUNE = 0.13; // guitar's frame, semitones over A440 + +// ── io ─────────────────────────────────────────────────────────────────── +function to48(src, tag) { + const dst = resolve(LANE, `src/vox/${tag}-48k.wav`); + if (!existsSync(dst)) spawnSync("ffmpeg", ["-v", "error", "-y", "-i", src, "-ar", String(SR), "-ac", "2", "-c:a", "pcm_f32le", dst]); + return dst; +} +function readWav(path) { + const b = readFileSync(path); + let p = 12, fmt, data; + while (p < b.length) { + const id = b.toString("ascii", p, p + 4), sz = b.readUInt32LE(p + 4); + if (id === "fmt ") fmt = { tag: b.readUInt16LE(p + 8), ch: b.readUInt16LE(p + 10), bits: b.readUInt16LE(p + 22) }; + if (id === "data") data = b.subarray(p + 8, p + 8 + sz); + p += 8 + sz + (sz & 1); + } + const bps = fmt.bits / 8, n = Math.floor(data.length / bps / fmt.ch); + const rd = (k) => fmt.bits === 16 ? data.readInt16LE(k * 2) / 32768 : fmt.tag === 3 ? data.readFloatLE(k * 4) : data.readInt32LE(k * 4) / 2147483648; + const L = new Float32Array(n), R = new Float32Array(n); + for (let i = 0; i < n; i++) { L[i] = rd(i * fmt.ch); R[i] = rd(i * fmt.ch + (fmt.ch > 1 ? 1 : 0)); } + return { L, R }; +} +function writeWav(path, L, R) { + const n = L.length, b = Buffer.alloc(44 + n * 4); + b.write("RIFF", 0); b.writeUInt32LE(36 + n * 4, 4); b.write("WAVEfmt ", 8); + b.writeUInt32LE(16, 16); b.writeUInt16LE(1, 20); b.writeUInt16LE(2, 22); + b.writeUInt32LE(SR, 24); b.writeUInt32LE(SR * 4, 28); b.writeUInt16LE(4, 32); + b.writeUInt16LE(16, 34); b.write("data", 36); b.writeUInt32LE(n * 4, 40); + for (let i = 0; i < n; i++) { + b.writeInt16LE(Math.round(Math.max(-1, Math.min(1, L[i])) * 32767), 44 + i * 4); + b.writeInt16LE(Math.round(Math.max(-1, Math.min(1, R[i])) * 32767), 46 + i * 4); + } + writeFileSync(path, b); +} + +const vox = readWav(resolve(LANE, "src/vox/vocals-aesthetivox.wav")); +const gtr = readWav(to48(resolve(STEMS, "guitar.wav"), "guitar")); +const halo = readWav(resolve(LANE, "src/vox/vocals-halo.wav")); +const FROM = Number(flags.from ?? 0), TO = Number(flags.to ?? vox.L.length / SR); +const n = Math.ceil((TO - FROM) * SR); +const at = (t) => Math.floor((t - FROM) * SR); + +// ── sections (bar numbers from measures.json, words mapped in src/) ───── +const SECTIONS = [ + { name: "intro", from: 1, to: 10 }, + { name: "verse1", from: 11, to: 27 }, + { name: "chorus1", from: 28, to: 43 }, + { name: "verse2", from: 44, to: 51 }, + { name: "chorus2", from: 52, to: 67 }, + { name: "break", from: 68, to: 71 }, + { name: "bridge", from: 72, to: 80 }, + { name: "outro", from: 81, to: 999 }, +]; +const sectionOf = (bar) => SECTIONS.find((s) => bar >= s.from && bar <= s.to)?.name; +// ── the arrangement ───────────────────────────────────────────────────── +// Per section: drum + sine layer levels (0 = out), her own harmonies +// (stem → [gain, pan]), and vocal presence (reverb send, air lift above +// 7 kHz — closer + brighter in the verses and bridge, roomier in choruses). +const ARR = { + intro: { kick: 0, snare: 0, rim: 0, hat: 0, sub: .5, tenor: .6, high: 0, third: 0, octave: 0, arp: .5, bell: 0, descant: 0, hook: 0, + harm: {}, send: .12, air: .2 }, + verse1: { kick: .7, snare: 0, rim: .5, hat: 0, sub: .8, tenor: .8, high: 0, third: 0, octave: 0, arp: .35, bell: .8, descant: 0, hook: 0, + harm: {}, send: .07, air: .45 }, + chorus1: { kick: 1, snare: .8, rim: .6, hat: .6, sub: 1, tenor: 1, high: .6, third: .35, octave: 0, arp: .6, bell: .5, descant: 0, hook: 0, + harm: { up3: [.34, -.55], down6: [.28, .55] }, send: .15, air: .25 }, + verse2: { kick: .8, snare: .5, rim: .5, hat: .3, sub: .9, tenor: .9, high: .3, third: 0, octave: 0, arp: .5, bell: .8, descant: 0, hook: 0, + harm: { down3: [.24, .5] }, send: .08, air: .45 }, + chorus2: { kick: 1, snare: 1, rim: .6, hat: .8, sub: 1, tenor: 1, high: .8, third: .3, octave: .3, arp: .7, bell: .4, descant: 1, hook: 0, + harm: { up3: [.34, -.6], down3: [.3, .6], down8: [.24, 0] }, send: .16, air: .3 }, + break: { kick: 0, snare: 0, rim: 0, hat: 0, sub: .8, tenor: 1, high: 1, third: 0, octave: 0, arp: .6, bell: 0, descant: .6, hook: 1, + harm: {}, send: .2, air: .2 }, + bridge: { kick: .7, snare: .6, rim: .5, hat: .4, sub: 1, tenor: 1, high: .9, third: 0, octave: .3, arp: .8, bell: .5, descant: .5, hook: 0, + harm: "rove", send: .09, air: .5 }, + outro: { kick: .5, snare: 0, rim: .4, hat: 0, sub: .8, tenor: .8, high: .6, third: 0, octave: 0, arp: .4, bell: .8, descant: 0, hook: 0, + harm: { down8: [.3, 0], up3: [.2, -.4] }, send: .2, air: .15 }, +}; +// "pitching around": in the bridge her harmony changes interval and side +// every bar. +const ROVE = [{ up3: [.36, -.6] }, { up5: [.3, .6] }, { down3: [.34, -.4] }, { up3: [.3, .5], down6: [.24, -.5] }]; +const harmFor = (bar) => { const a = ARR[sectionOf(bar.n)]; return a.harm === "rove" ? ROVE[(bar.n - 72) % 4] : a.harm; }; +const ORCH = Object.fromEntries(Object.entries(ARR).map(([k, v]) => [k, v])); + +// ── buses ──────────────────────────────────────────────────────────────── +const dL = new Float32Array(n), dR = new Float32Array(n); // drums +const sL = new Float32Array(n), sR = new Float32Array(n); // sines +const add = (buf, i, v) => { if (i >= 0 && i < n) buf[i] += v; }; + +function kick(t, g) { + const s0 = at(t); let ph = 0; + for (let i = 0; i < 0.36 * SR; i++) { const u = i / SR; + ph += (2 * Math.PI * (46 + 58 * Math.exp(-u * 32))) / SR; + const v = Math.sin(ph) * Math.exp(-u * 7.5) * g; add(dL, s0 + i, v); add(dR, s0 + i, v); } +} +function snare(t, g) { + const s0 = at(t); let prev = 0, lp = 0, ph = 0; + for (let i = 0; i < 0.24 * SR; i++) { const u = i / SR, nz = Math.random() * 2 - 1; + const hp = nz - prev; prev = nz; lp += (hp - lp) * 0.12; ph += (2 * Math.PI * 180) / SR; + // brushy: slower swell-in, darker noise, softer body + const v = (lp * Math.min(1, u / 0.004) * Math.exp(-u * 11) * 1.6 + Math.sin(ph) * 0.22 * Math.exp(-u * 30)) * g; + add(dL, s0 + i, v * 0.92); add(dR, s0 + i, v); } +} +function rim(t, g) { // woody cross-stick: two damped partials + click + const s0 = at(t); let p1 = 0, p2 = 0; + for (let i = 0; i < 0.07 * SR; i++) { const u = i / SR; + p1 += (2 * Math.PI * 1650) / SR; p2 += (2 * Math.PI * 520) / SR; + const v = (Math.sin(p1) * 0.5 + Math.sin(p2)) * Math.exp(-u * 70) * g; + add(dL, s0 + i, v * 1.1); add(dR, s0 + i, v * 0.8); } +} +function hat(t, g, pan) { + const s0 = at(t); let prev = 0; + for (let i = 0; i < 0.03 * SR; i++) { const nz = Math.random() * 2 - 1, hp = nz - prev; prev = nz; + const v = hp * Math.exp(-(i / SR) * 300) * g * 0.6; add(dL, s0 + i, v * (1 - pan)); add(dR, s0 + i, v * (1 + pan)); } +} + +// Sine voice: slow swell, gentle 4.5 Hz tremolo, a ±3¢ detuned twin for width. +function sine(t0, dur, midi, g, pan, { atk = 0.35, rel = 0.9, det = 3 } = {}) { + const f = 440 * Math.pow(2, (midi + TUNE - 69) / 12); + const s0 = at(t0), len = Math.floor((dur + rel) * SR); + const fa = f * Math.pow(2, det / 1200), fb = f * Math.pow(2, -det / 1200); + let pa = Math.random() * 6, pb = Math.random() * 6; + const gl = g * Math.sqrt((1 - pan) / 2), gr = g * Math.sqrt((1 + pan) / 2); + for (let i = 0; i < len; i++) { + const u = i / SR; + const env = Math.min(1, u / atk) * (u > dur ? Math.exp(-(u - dur) / (rel / 3)) : 1); + pa += (2 * Math.PI * fa) / SR; pb += (2 * Math.PI * fb) / SR; + const trem = 1 - 0.08 * (0.5 + 0.5 * Math.sin(2 * Math.PI * 4.5 * u)); + add(sL, s0 + i, Math.sin(pa) * env * trem * gl); + add(sR, s0 + i, Math.sin(pb) * env * trem * gr); + } +} + +// ── harmony: voice-led chord tones ────────────────────────────────────── +const CHORD = { "G#m": [8, 11, 3], "Emaj7": [8, 11, 3], B: [11, 3, 6] }; // Emaj7 bars sound G#m (bass G#) +const ROOT = { "G#m": 44, "Emaj7": 44, B: 47 }; // G#2, B2 +const SCALE = [8, 10, 11, 1, 3, 4, 6]; // G# natural minor +function nearestTone(pcs, target, lo, hi) { + let best = null; + for (let m = lo; m <= hi; m++) if (pcs.includes(((m % 12) + 12) % 12) && (best === null || Math.abs(m - target) < Math.abs(best - target))) best = m; + return best; +} +function lead(prev, pcs, lo, hi) { // move each voice to the nearest chord tone, no doubling + const used = new Set(); + return prev.map((p) => { let m = nearestTone(pcs.filter((c) => !used.has(c)).length ? pcs.filter((c) => !used.has(c)) : pcs, p, lo, hi); used.add(((m % 12) + 12) % 12); return m; }); +} +function thirdAbove(m) { // diatonic third in G# minor + const pc = ((m % 12) + 12) % 12, oct = Math.floor(m / 12); + const i = SCALE.indexOf(pc); + if (i < 0) return m + 3; + const t = SCALE[(i + 2) % 7]; + return oct * 12 + t + (t < pc ? 12 : 0); +} + +// A struck sine: bell (inharmonic 2.76 partial, long) or celesta (octave +// partial, short). Both sit above her register. +function bell(t0, midi, g, pan, { dec = 1.4, part = 2.76, pg = 0.25 } = {}) { + const f = 440 * Math.pow(2, (midi + TUNE - 69) / 12), s0 = at(t0), len = Math.floor(dec * 4 * SR); + const gl = g * Math.sqrt((1 - pan) / 2), gr = g * Math.sqrt((1 + pan) / 2); + for (let i = 0; i < len; i++) { const u = i / SR; + const v = (Math.sin(2 * Math.PI * f * u) * Math.exp(-u / dec) + pg * Math.sin(2 * Math.PI * f * part * u) * Math.exp(-u / (dec * 0.3))) * Math.min(1, u / 0.003); + add(sL, s0 + i, v * gl); add(sR, s0 + i, v * gr); } +} +const celesta = (t0, midi, g, pan) => bell(t0, midi, g, pan, { dec: 0.35, part: 2, pg: 0.15 }); + +let tenor = lead([52, 56, 59], CHORD["G#m"], 47, 58); // under her +let high = [75, 78]; // over her +let desc = 83; // descant, drifts down +const barOf = (t) => M.bars.find((b) => t >= b.t && t < b.t + b.dur); +for (const b of M.bars) { + const sec = sectionOf(b.n), o = ARR[sec]; + if (!o || b.t + b.dur < FROM || b.t > TO) continue; + const bt = b.beats, pcs = CHORD[b.chord] || CHORD["G#m"]; + const mid = (k) => bt[k] + (bt[k + 1] - bt[k]) / 2; + // bridge builds: everything swells bar by bar + const swell = sec === "bridge" ? 0.7 + 0.3 * ((b.n - 72) / 8) : 1; + // drums on her strum points (X..XX..X) + if (o.kick) { kick(bt[0], 0.95 * o.kick); if (bt.length > 2) kick(mid(1), 0.6 * o.kick); } + if (o.snare && bt.length > 3) snare(bt[2], 0.6 * o.snare * swell); + if (o.rim && bt.length > 4) rim(mid(3), 0.22 * o.rim); + if (o.hat) for (let k = 0; k < bt.length - 1; k++) { hat(bt[k], 0.15 * o.hat, -0.35); hat(mid(k), 0.09 * o.hat, 0.35); } + // sine choir + const dur = b.dur * 0.97; + if (o.sub) { sine(b.t, dur, ROOT[b.chord] - 12, 0.30 * o.sub, 0, { det: 0 }); sine(b.t, dur, ROOT[b.chord], 0.10 * o.sub, 0, { det: 1 }); } + tenor = lead(tenor, pcs, 47, 58); + if (o.tenor) tenor.forEach((m, k) => sine(b.t, dur, m, 0.07 * o.tenor * swell, [-0.5, 0, 0.5][k])); + high = lead(high, pcs, 70, 83); + if (o.high) high.forEach((m, k) => sine(b.t, dur, m, 0.035 * o.high * swell, k ? 0.7 : -0.7, { atk: 0.8, rel: 1.4, det: 5 })); + // descant: one long high chord tone per bar, leaning downward by step + if (o.descant) { + const c = [desc - 1, desc - 2, desc, desc + 1].map((x) => nearestTone(pcs, x, 76, 88)).find((x) => x <= desc) ?? nearestTone(pcs, desc, 76, 88); + desc = c <= 77 ? 87 : c; + sine(b.t, dur, c, 0.03 * o.descant, 0.2, { atk: 1.0, rel: 1.8, det: 4 }); + } + // celesta arpeggio on her 8th grid: chord tones rising, 16ths in the last bridge bars + if (o.arp) { + const tones = [0, 1, 2, 3, 4, 5].map((k) => nearestTone(pcs, 68 + k * 3, 68, 86)); + const dens = sec === "bridge" && b.n >= 77 ? 4 : 2; + let k = 0; + for (let j = 0; j < bt.length - 1; j++) for (let q = 0; q < dens; q++) { + const t = bt[j] + ((bt[j + 1] - bt[j]) * q) / dens; + celesta(t, tones[k++ % tones.length], 0.035 * o.arp * swell, (k % 2 ? -0.5 : 0.5)); + } + } +} +// her melody, shadowed: a sine 3rd above + an octave line +for (const v of VN) { + if (v.t < FROM || v.t > TO || v.dur < 0.18) continue; + const bar = barOf(v.t), o = bar && ARR[sectionOf(bar.n)]; + if (!o) continue; + if (o.third) sine(v.t, v.dur, thirdAbove(v.midi), 0.05 * o.third, 0.3, { atk: 0.06, rel: 0.35, det: 2 }); + if (o.octave) sine(v.t, v.dur, v.midi + 12, 0.025 * o.octave, -0.3, { atk: 0.08, rel: 0.5, det: 4 }); +} +// bell answers: in each breath gap > 1 s, her last three notes echo back +// an octave up on bells, one per 8th. +for (let i = 1; i < VN.length; i++) { + const prev = VN[i - 1], end = prev.t + prev.dur, gap = VN[i].t - end; + if (gap < 1.0 || end < FROM || end > TO) continue; + const bar = barOf(end), o = bar && ARR[sectionOf(bar.n)]; + if (!o || !o.bell) continue; + const e8 = (bar.beats[1] - bar.beats[0]) / 2; + const tail = VN.slice(Math.max(0, i - 3), i); + tail.forEach((v, k) => { const t = end + 0.12 + k * e8; if (t < VN[i].t - 0.1) bell(t, v.midi + 12, 0.05 * o.bell, k % 2 ? 0.4 : -0.4); }); +} +// the hook, on sines, over the guitar-only break: chorus 1's first four +// bars of her tuned melody re-laid bar-for-bar onto bars 68–71. +{ + const src = M.bars.filter((b) => b.n >= 28 && b.n < 32), dst = M.bars.filter((b) => b.n >= 68 && b.n < 72); + for (const v of VN) { + const bi = src.findIndex((b) => v.t >= b.t && v.t < b.t + b.dur); + if (bi < 0 || !dst[bi]) continue; + const f = (v.t - src[bi].t) / src[bi].dur, sc = dst[bi].dur / src[bi].dur; + const t = dst[bi].t + f * dst[bi].dur; + sine(t, v.dur * sc, v.midi, 0.07, 0, { atk: 0.05, rel: 0.6, det: 3 }); + sine(t, v.dur * sc, v.midi + 12, 0.025, 0.3, { atk: 0.08, rel: 0.8, det: 5 }); + } +} + +// ── reverb: 8-line FDN (Hadamard feedback, damped), the shared heaven ─── +// Voice, halo and sines all send here so they sit in ONE space. Long +// (~3.2 s), dark (damping ~5 kHz), 40 ms predelay so her words stay dry-clear. +function fdn(inL, inR, { t60 = 3.2, damp = 0.35, pre = 0.04 } = {}) { + const lens = [1433, 1601, 1867, 2053, 2251, 2399, 2617, 2797].map((x) => Math.round(x * SR / 44100)); + const bufs = lens.map((l) => new Float32Array(l)), idx = lens.map(() => 0), lp = lens.map(() => 0); + const gains = lens.map((l) => Math.pow(10, (-3 * l) / (SR * t60))); + const P = Math.floor(pre * SR); + const oL = new Float32Array(inL.length), oR = new Float32Array(inL.length); + const o = new Float32Array(8); + for (let i = 0; i < inL.length; i++) { + const xl = i >= P ? inL[i - P] : 0, xr = i >= P ? inR[i - P] : 0; + for (let k = 0; k < 8; k++) { const y = bufs[k][idx[k]]; lp[k] += (y - lp[k]) * (1 - damp); o[k] = lp[k]; } + // 8-point Hadamard, normalized + const a0 = o[0] + o[1], a1 = o[0] - o[1], a2 = o[2] + o[3], a3 = o[2] - o[3], a4 = o[4] + o[5], a5 = o[4] - o[5], a6 = o[6] + o[7], a7 = o[6] - o[7]; + const b0 = a0 + a2, b1 = a1 + a3, b2 = a0 - a2, b3 = a1 - a3, b4 = a4 + a6, b5 = a5 + a7, b6 = a4 - a6, b7 = a5 - a7; + const h = [b0 + b4, b1 + b5, b2 + b6, b3 + b7, b0 - b4, b1 - b5, b2 - b6, b3 - b7]; + for (let k = 0; k < 8; k++) { + bufs[k][idx[k]] = h[k] * 0.35355 * gains[k] + (k % 2 ? xr : xl) * 0.5; + idx[k] = (idx[k] + 1) % lens[k]; + } + oL[i] = o[0] + o[2] + o[4] + o[6]; oR[i] = o[1] + o[3] + o[5] + o[7]; + } + return [oL, oR]; +} + +// ── mix ────────────────────────────────────────────────────────────────── +// Her take is the record. Presence rides per section (send + air), her own +// harmonies ride per bar with 150 ms gain glides, the kit sits dark and low. +const HARM = {}; +for (const k of ["up3", "down3", "up5", "down6", "down8"]) { + const p = resolve(LANE, `src/vox/harm-${k}.wav`); + if (existsSync(p)) HARM[k] = readWav(p).L; +} +// per-sample automation from per-bar targets +const auto = (fn) => { + const a = new Float32Array(n), glide = 1 - Math.exp(-1 / (0.15 * SR)); + let cur = null; + for (let i = 0; i < n; i++) { + const t = FROM + i / SR, b = barOf(t) || (t < M.bars[0].t ? M.bars[0] : M.bars.at(-1)); + const tgt = fn(b); + cur = cur === null ? tgt : cur + (tgt - cur) * glide; + a[i] = cur; + } + return a; +}; +const sendA = auto((b) => ARR[sectionOf(b.n)].send); +const airA = auto((b) => ARR[sectionOf(b.n)].air); +const hg = {}, hp = {}; +for (const k of Object.keys(HARM)) { + hg[k] = auto((b) => (harmFor(b)[k] || [0, 0])[0]); + hp[k] = auto((b) => (harmFor(b)[k] || [0, 0])[1]); +} +const HDELAY = { up3: 0.018, up5: 0.022, down3: 0.026, down6: 0.03, down8: 0.012 }; // a breath apart, like real doubles + +const L = new Float32Array(n), R = new Float32Array(n); +const s0 = Math.floor(FROM * SR); +const sendL = new Float32Array(n), sendR = new Float32Array(n); +let dlp = 0, drp = 0, vlp = 0; +const dk = 1 - Math.exp(-2 * Math.PI * 4500 / SR); +const ak = 1 - Math.exp(-2 * Math.PI * 7000 / SR); +for (let i = 0; i < n; i++) { + const vi = s0 + i, v0 = vox.L[vi] || 0, hv = (halo.L[vi] || 0) * 0.14; + vlp += (v0 - vlp) * ak; + const v = v0 + (v0 - vlp) * airA[i] * 1.6; // air lift above 7 kHz + let hl = 0, hr = 0; + for (const k in HARM) { + const g = hg[k][i]; + if (g < 0.002) continue; + const x = (HARM[k][vi - Math.floor(HDELAY[k] * SR)] || 0) * g * 0.55, pn = hp[k][i]; + hl += x * Math.sqrt((1 - pn) / 2); hr += x * Math.sqrt((1 + pn) / 2); + } + dlp += (dL[i] - dlp) * dk; drp += (dR[i] - drp) * dk; + L[i] = v + hl + (gtr.L[vi] || 0) * 0.72 + dlp * 0.45 + sL[i] * 0.75 + hv * 0.6; + R[i] = v + hr + (gtr.R[vi] || 0) * 0.72 + drp * 0.45 + sR[i] * 0.75 + hv * 0.6; + const sd = sendA[i]; + sendL[i] = v * sd + hl * 0.35 + hv + sL[i] * 0.4 + dlp * 0.05 + (gtr.L[vi] || 0) * 0.06; + sendR[i] = v * sd + hr * 0.35 + hv + sR[i] * 0.4 + drp * 0.05 + (gtr.R[vi] || 0) * 0.06; +} +// the room: present but near — early reflections always on, a 2.2 s tail behind +const room = [[0.011, 0.16, 0], [0.017, 0.14, 1], [0.023, 0.11, 0], [0.031, 0.1, 1], [0.043, 0.07, 0], [0.053, 0.06, 1]]; +const [wL, wR] = fdn(sendL, sendR, { t60: 2.2, damp: 0.4, pre: 0.025 }); +for (let i = n - 1; i >= 0; i--) { + let el = 0, er = 0; + for (const [d, g, ch] of room) { const j = i - Math.floor(d * SR); if (j >= 0) { const x = (sendL[j] + sendR[j]) * g; if (ch) er += x; else el += x; } } + L[i] += wL[i] * 0.4 + el; R[i] += wR[i] * 0.4 + er; +} +const fade = Math.floor(1.0 * SR); +for (let i = 0; i < fade; i++) { const g = i / fade; L[i] *= g; R[i] *= g; L[n - 1 - i] *= g; R[n - 1 - i] *= g; } +let peak = 0; +for (let i = 0; i < n; i++) peak = Math.max(peak, Math.abs(L[i]), Math.abs(R[i])); +for (let i = 0; i < n; i++) { L[i] *= 0.89 / peak; R[i] *= 0.89 / peak; } + +const tag = flags.from || flags.to ? `v4-${Math.round(FROM)}-${Math.round(TO)}` : "v4"; +const wav = resolve(OUT, `sailor-song-${tag}.wav`); +writeWav(wav, L, R); +spawnSync("ffmpeg", ["-v", "error", "-y", "-i", wav, "-b:a", "256k", wav.replace(/\.wav$/, ".mp3")]); +console.log(`✓ ${(n / SR).toFixed(1)}s → ${wav.replace(/\.wav$/, ".mp3")}`); diff --git a/pop/sailor-song/bin/replay-guitar.mjs b/pop/sailor-song/bin/replay-guitar.mjs new file mode 100644 index 0000000000..2f963eccdd --- /dev/null +++ b/pop/sailor-song/bin/replay-guitar.mjs @@ -0,0 +1,82 @@ +#!/usr/bin/env node +// replay-guitar.mjs — re-play her guitar part on pop/guitar/c/strum, bar +// for bar, so the arrangement can blend, double or replace her take. +// +// Everything comes from her: bar starts + lengths (measures.json, anchored +// on her strum motif), the chord she held, and her hand — X..XX..X in +// 8ths → "D..DD..U". Voicings are her actual capo-4 shapes, sounding: +// Em shape 022000 → G#m 44 51 56 59 63 68 +// Cmaj7 x32000 + open low string → Emaj7/G# 44 52 56 59 63 68 +// G shape 320003 → B 47 51 54 59 63 71 +// Each bar renders at its own tempo (a bar is whatever length she played) +// and overlap-adds at her downbeat; a short tail lets strings ring into +// the next bar's choke. +// +// node pop/sailor-song/bin/replay-guitar.mjs +// → src/vox/replay-acoustic.wav, src/vox/replay-electric.wav (48 k stereo) + +import { readFileSync, writeFileSync, mkdirSync, rmSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const LANE = resolve(HERE, ".."); +const STRUM = resolve(LANE, "../guitar/c/strum"); +const TMP = resolve(LANE, "src/replay-tmp"); +const SR = 48_000; +mkdirSync(TMP, { recursive: true }); + +const M = JSON.parse(readFileSync(resolve(LANE, "measures.json"), "utf8")); +const TOTAL = 179.2; +const VOICING = { + "G#m": "44,51,56,59,63,68", + Emaj7: "44,52,56,59,63,68", + B: "47,51,54,59,63,71", +}; + +function readF32(path) { + const b = readFileSync(path); + let p = 12, data; + while (p < b.length) { + const id = b.toString("ascii", p, p + 4), sz = b.readUInt32LE(p + 4); + if (id === "data") data = b.subarray(p + 8, p + 8 + sz); + p += 8 + sz + (sz & 1); + } + const n = data.length / 8, L = new Float32Array(n), R = new Float32Array(n); + for (let i = 0; i < n; i++) { L[i] = data.readFloatLE(i * 8); R[i] = data.readFloatLE(i * 8 + 4); } + return { L, R }; +} +function writeF32(path, L, R) { + const n = L.length, b = Buffer.alloc(44 + n * 8); + b.write("RIFF", 0); b.writeUInt32LE(36 + n * 8, 4); b.write("WAVEfmt ", 8); + b.writeUInt32LE(16, 16); b.writeUInt16LE(3, 20); b.writeUInt16LE(2, 22); + b.writeUInt32LE(SR, 24); b.writeUInt32LE(SR * 8, 28); b.writeUInt16LE(8, 32); + b.writeUInt16LE(32, 34); b.write("data", 36); b.writeUInt32LE(n * 8, 40); + for (let i = 0; i < n; i++) { b.writeFloatLE(L[i], 44 + i * 8); b.writeFloatLE(R[i], 48 + i * 8); } + writeFileSync(path, b); +} + +for (const kind of ["acoustic", "electric"]) { + const n = Math.ceil(TOTAL * SR), L = new Float32Array(n), R = new Float32Array(n); + for (const bar of M.bars) { + const nbeat = bar.beats.length - 1; + // one 4/4 "bar" in strum's clock = this bar's real length + const bpm = (60 * 4) / bar.dur; + const pattern = nbeat === 4 ? "D..DD..U" : ("D..DD..U" + "D.").slice(0, 2 * nbeat).padEnd(2 * nbeat, "."); + const out = resolve(TMP, `${kind}-${bar.n}.wav`); + const args = ["--chord", VOICING[bar.chord] || VOICING["G#m"], "--pattern", pattern, + "--bpm", bpm.toFixed(3), "--bars", "1", "--tail", "0.5", "--human", "0.25", + "--force", "0.7", "--seed", String(bar.n), "--out", out]; + if (kind === "electric") args.push("--electric", "--drive", "0.25"); + const r = spawnSync(STRUM, args, { encoding: "utf8" }); + if (r.status !== 0) { console.error(r.stderr); process.exit(1); } + const w = readF32(out), s0 = Math.floor(bar.t * SR); + // strum peak-normalizes each render to 0.9; bring every bar to one level + for (let i = 0; i < w.L.length && s0 + i < n; i++) { L[s0 + i] += w.L[i] * 0.5; R[s0 + i] += w.R[i] * 0.5; } + } + const dst = resolve(LANE, `src/vox/replay-${kind}.wav`); + writeF32(dst, L, R); + console.log(`✓ ${kind}: ${M.bars.length} bars → ${dst}`); +} +rmSync(TMP, { recursive: true, force: true }); diff --git a/pop/sailor-song/bin/score-video.mjs b/pop/sailor-song/bin/score-video.mjs new file mode 100644 index 0000000000..a203bc3f7f --- /dev/null +++ b/pop/sailor-song/bin/score-video.mjs @@ -0,0 +1,380 @@ +#!/usr/bin/env node +// score-video.mjs — the /pop score video for sailor s, wannadash's shape +// (pop/cult/bin/score-video.mjs) with a fourth band for the singalong: +// +// · SECTION RIBBON — the whole record at a glance, playhead sweeping +// · PIANO ROLL — every scored hit from the C engine's receipt, one lane +// per voice, scrolling, flashing on the strike +// · LYRIC — the line she is singing, the sung word lit, the next +// line waiting under it (src/words-record.json, local) +// · WAVEFORM — the master, filled up to the playhead +// +// The receipt (out/sailor-song-v7.events.json) is in full-take time and the +// record starts at its startSec, so OFFSET = startSec. Bars come from the +// receipt's real bar times (her clock), not from tempo arithmetic. +// +// Dependency-free: raw RGB frames piped into ffmpeg; labels in the 5x7 font. +// +// node pop/sailor-song/bin/score-video.mjs # newest sailor-song-v*.mp3 +// node pop/sailor-song/bin/score-video.mjs --audio out/sailor-song-v7-master.wav +// node pop/sailor-song/bin/score-video.mjs --fps 30 --width 1920 --height 1080 + +import { spawn, execFileSync } from "node:child_process"; +import { existsSync, readFileSync, readdirSync } from "node:fs"; +import { dirname, resolve, basename } from "node:path"; +import { fileURLToPath } from "node:url"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const LANE = resolve(HERE, ".."); +const OUT = resolve(LANE, "out"); +const arg = (k, d = null) => { + const i = process.argv.indexOf(`--${k}`); + return i >= 0 && process.argv[i + 1] && !process.argv[i + 1].startsWith("--") + ? process.argv[i + 1] : (process.argv.includes(`--${k}`) ? true : d); +}; + +function newestCut() { + const mp3s = readdirSync(OUT).filter((f) => /^sailor-song-v\d+\.mp3$/.test(f)); + if (!mp3s.length) throw new Error(`no sailor-song mp3 in ${OUT}`); + return mp3s.map((f) => resolve(OUT, f)) + .sort((a, b) => execFileSync("stat", ["-f%m", b]) - execFileSync("stat", ["-f%m", a]))[0]; +} +const audio = resolve(arg("audio") || newestCut()); +const stem = basename(audio).replace(/\.(mp3|wav|flac)$/, "").replace(/-master$/, ""); +const receiptPath = [arg("events"), resolve(OUT, `${stem}.events.json`)].filter(Boolean).map((p) => resolve(p)).find(existsSync); +const R = receiptPath ? JSON.parse(readFileSync(receiptPath, "utf8")) : {}; +if (!receiptPath) console.warn("! no events receipt found — drawing audio only"); +const wordsPath = resolve(LANE, "src/words-record.json"); +const WORDS = existsSync(wordsPath) ? JSON.parse(readFileSync(wordsPath, "utf8")) : []; +const lyricLines = existsSync(resolve(LANE, "src/lyrics-sung.txt")) + ? readFileSync(resolve(LANE, "src/lyrics-sung.txt"), "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("[")) : []; + +const W = Number(arg("width", 1280)), H = Number(arg("height", 720)); +const FPS = Number(arg("fps", 30)); +const LYRICS = !!arg("lyrics"); // v19: the lyric check — only the words, over her own waveform +const outPath = resolve(arg("out") || resolve(OUT, `${stem}-${LYRICS ? "lyrics" : "score"}.mp4`)); + +const probe = (p, k) => execFileSync("ffprobe", ["-v", "error", "-show_entries", `format=${k}`, "-of", "csv=p=0", p], { encoding: "utf8" }).trim(); +const DUR = Number(probe(audio, "duration")); +const OFFSET = Number(arg("offset", R.startSec ?? 0)); + +const PEAKS = W * 2; +const pcm = execFileSync("ffmpeg", ["-v", "error", "-i", audio, "-ac", "1", "-ar", "8000", "-f", "f32le", "-"], { maxBuffer: 1 << 30 }); +const samples = new Float32Array(pcm.buffer, pcm.byteOffset, pcm.length / 4); +const wave = new Float32Array(PEAKS); +for (let i = 0; i < PEAKS; i++) { + const a = Math.floor((i / PEAKS) * samples.length), b = Math.floor(((i + 1) / PEAKS) * samples.length); + let m = 0; for (let j = a; j < b; j++) { const v = Math.abs(samples[j]); if (v > m) m = v; } + wave[i] = m; +} +let wmax = 0; for (const v of wave) if (v > wmax) wmax = v; +if (wmax > 0) for (let i = 0; i < PEAKS; i++) wave[i] /= wmax; + +// her voice alone, in record time: the edited lead stem (cut time = record time + startSec), as a 200 Hz envelope +const ENV_HZ = 200; +let venv = new Float32Array(0); +if (LYRICS) { + const vp = ["vocals-natural", "vocals-aesthetivox"].map((n) => resolve(LANE, `src/vox/cut/${n}.wav`)).find(existsSync); + const raw = execFileSync("ffmpeg", ["-v", "error", "-i", vp, "-ac", "1", "-ar", "8000", "-f", "f32le", "-"], { maxBuffer: 1 << 30 }); + const v = new Float32Array(raw.buffer, raw.byteOffset, raw.length / 4), hop = 8000 / ENV_HZ; + venv = new Float32Array(Math.floor(v.length / hop)); + for (let i = 0; i < venv.length; i++) { let m = 0; for (let j = i * hop; j < (i + 1) * hop; j++) m = Math.max(m, Math.abs(v[j])); venv[i] = m; } + let vm = 0; for (const x of venv) vm = Math.max(vm, x); for (let i = 0; i < venv.length; i++) venv[i] = Math.sqrt(venv[i] / (vm || 1)); // sqrt: quiet singing still reads + console.log(` voice ${vp}`); +} +const envAt = (tAbs) => venv[Math.floor(tAbs * ENV_HZ)] || 0; // tAbs is cut time + +// ── the score ───────────────────────────────────────────────────────── +const sections = (R.sections || []).map((v) => ({ name: v.name, a: v.start, b: v.end })).filter((s) => s.b > s.a).sort((a, b) => a.a - b.a); +// beats per bar come from the chart the engine played (the receipt's bars carry only t/dur) +const chartPath = ["measures.cut.json", "measures.reg.json", "measures.json"].map((f) => resolve(LANE, f)).find(existsSync); +const chartBars = chartPath ? JSON.parse(readFileSync(chartPath, "utf8")).bars : []; +const bars = (R.bars || []).slice().sort((a, b) => a.t - b.t).map((b) => { + const cb = chartBars.find((c) => Math.abs(c.t - b.t) < 0.02); + return { ...b, beats: cb ? cb.beats : [0, 1, 2, 3, 4].map((j) => b.t + (j * b.dur) / 4) }; +}); +const events = (R.events || []).filter((e) => Number.isFinite(e.t)); +const barAt = (t) => { let n = 0; for (const b of bars) { if (t >= b.t) n = b.n; else break; } return n; }; +const chordAt = (t) => { let c = ""; for (const b of bars) { if (t >= b.t) c = b.chord; else break; } return c; }; +const bpmAt = (t) => { for (const b of bars) if (t >= b.t && t < b.t + b.dur) return Math.round(4 * 60 / b.dur); return 0; }; + +// lane order is fixed so versions compare +const LANES = ["kick", "snare", "clap", "hat", "block", "shaker", "tamb", "conga", "bass", "808", "sub", "pad", "high", "descant", "arp", "vib", "bell", "hook", "impact", "gtr", "vox"]; +const laneOf = (e) => { const v = (e.voice || "").toLowerCase(); for (let i = 0; i < LANES.length; i++) if (v === LANES[i] || v.includes(LANES[i])) return i; return LANES.length - 1; }; +const laneColor = (i) => { + const t = i / (LANES.length - 1); + if (i < 8) return [40 + 100 * t * 2, 110 + 50 * t, 230 - 40 * t]; // the kit: blues + if (i < 14) return [190 - 60 * t, 80 + 60 * t, 190]; // the bed: violets + if (i < 19) return [230, 150 - 50 * t, 40]; // the AC voices: golds + return [230, 60, 140]; // her: pink +}; + +// ── framebuffer ─────────────────────────────────────────────────────── +const fb = Buffer.alloc(W * H * 3); +const px = (x, y, r, g, b, a = 1) => { + x |= 0; y |= 0; if (x < 0 || y < 0 || x >= W || y >= H) return; + const o = (y * W + x) * 3; + fb[o] = fb[o] * (1 - a) + r * a; fb[o + 1] = fb[o + 1] * (1 - a) + g * a; fb[o + 2] = fb[o + 2] * (1 - a) + b * a; +}; +const rect = (x, y, w, h, r, g, b, a = 1) => { for (let j = 0; j < h; j++) for (let i = 0; i < w; i++) px(x + i, y + j, r, g, b, a); }; + +const FONT = { + A:"01110100011000111111100011000110001",B:"11110100011000111110100011000111110",C:"01110100011000010000100001000101110", + D:"11110100011000110001100011000111110",E:"11111100001000011110100001000011111",F:"11111100001000011110100001000010000", + G:"01110100011000010011100011000101111",H:"10001100011000111111100011000110001",I:"11111001000010000100001000010011111", + J:"00111000010000100001100011000101110",K:"10001100101010011000101001001010001",L:"10000100001000010000100001000011111", + M:"10001110111011110101100011000110001",N:"10001110011010110011100011000110001",O:"01110100011000110001100011000101110", + P:"11110100011000111110100001000010000",Q:"01110100011000110001101011001001101",R:"11110100011000111110101001001010001", + S:"01111100001000001110000011000011110",T:"11111001000010000100001000010000100",U:"10001100011000110001100011000101110", + V:"10001100011000110001100010101000100",W:"10001100011000110101101011101110001",X:"10001100010101000100010101000110001", + Y:"10001100010101000100001000010000100",Z:"11111000010001000100010001000011111", + 0:"01110100111010110011100011000101110",1:"00100011000010000100001000010001110",2:"01110100010000100110010001000111111", + 3:"11111000100010000010000011000101110",4:"00010001100101010010111110001000010",5:"11111100001111000001000011000101110", + 6:"00110010001000011110100011000101110",7:"11111000010001000100001000010000100",8:"01110100011000101110100011000101110", + 9:"01110100011000101111000010001001100",":":"00000001000010000000000010000100000"," ":"00000000000000000000000000000000000", + "-":"00000000000000001111100000000000000",".":"00000000000000000000000000110001100","'":"00100001000010000000000000000000000", + "/":"00001000100010001000100010000000000","+":"00000001000010001111100010000100000","(":"00010001000100001000010000100000010", + ")":"01000001000001000010000100010001000",",":"00000000000000000000000001100010010","?":"01110100010000100010001000000000100", + "!":"00100001000010000100001000000000100","#":"01010010101111101010111110101001010","@":"01110100011011110101101111000001110", + "\"":"01010010100000000000000000000000000",";":"00000001000010000000000010000100100", +}; +function text(s, x, y, r, g, b, scale = 2, a = 1) { + let cx = x; + for (const ch of String(s).toUpperCase()) { + const gl = FONT[ch]; + if (gl) for (let j = 0; j < 7; j++) for (let i = 0; i < 5; i++) if (gl[j * 5 + i] === "1") rect(cx + i * scale, y + j * scale, scale, scale, r, g, b, a); + cx += 6 * scale; + } + return cx - x; +} +const textW = (s, scale = 2) => String(s).length * 6 * scale; + +// ── lyric lines from the record-time words ──────────────────────────── +// words carry no line ids: rebuild the lines by walking lyrics-sung.txt's word counts +const lines = []; +{ let k = 0; + for (const l of lyricLines) { + const toks = l.split(/\s+/).filter(Boolean); const ws = WORDS.slice(k, k + toks.length); k += toks.length; + if (ws.length) lines.push({ text: l, words: ws, a: ws[0].fromMs / 1000, b: ws[ws.length - 1].toMs / 1000 }); + } + if (k !== WORDS.length) console.warn(`! lyric/word count drift: ${k} lyric words vs ${WORDS.length} timed`); + lines.sort((a, b) => a.a - b.a); +} + +// ── layout ──────────────────────────────────────────────────────────── +const PAD = 28; +const RIB_Y = 96, RIB_H = 54; +const ROLL_Y = RIB_Y + RIB_H + 34; +const LYR_H = 96; // the lyric band +const TRAY_H = 34; // v18: the word tray — each sung word as a block, beat clicks under +const WAVE_H = 84, WAVE_Y = H - 30 - WAVE_H; +const LYR_Y = WAVE_Y - 18 - LYR_H; +const TRAY_Y = LYR_Y - 10 - TRAY_H; +const ROLL_H = TRAY_Y - 12 - ROLL_Y; +const PLOT_X = PAD, PLOT_W = W - PAD * 2; +const WINDOW = Number(arg("window", 12)); +const NOW_X = PLOT_X + PLOT_W * 0.34; +const T0 = OFFSET, T1 = OFFSET + DUR; +const xOfAbs = (t) => PLOT_X + ((t - T0) / DUR) * PLOT_W; +const title = arg("title", "sailor s"), artist = arg("artist", "s@ge"); + +function drawFrame(fi) { + const tRel = fi / FPS, tAbs = OFFSET + tRel; + for (let y = 0; y < H; y++) rect(0, y, W, 1, 250 - 8 * (y / H), 247 - 8 * (y / H), 242 - 10 * (y / H)); // light mode: warm paper + + // header + text(`${title} - ${artist}`, PAD, 26, 40, 30, 45, 3); + const clock = `${String(Math.floor(tRel / 60)).padStart(2, "0")}:${String(Math.floor(tRel % 60)).padStart(2, "0")}`; + const total = `${String(Math.floor(DUR / 60)).padStart(2, "0")}:${String(Math.floor(DUR % 60)).padStart(2, "0")}`; + text(`${clock} / ${total}`, W - PAD - textW(`${clock} / ${total}`, 3), 26, 120, 110, 130, 3); + text(`BAR ${barAt(tAbs)} ${bpmAt(tAbs) || "-"} BPM ${chordAt(tAbs) || ""} G# MINOR +13C`, PAD, 60, 130, 120, 140, 2); + + // section ribbon + rect(PLOT_X, RIB_Y, PLOT_W, RIB_H, 232, 226, 222); + let current = null; + sections.forEach((s, si) => { + const x0 = xOfAbs(Math.max(s.a, T0)), x1 = xOfAbs(Math.min(s.b, T1)); + if (x1 <= PLOT_X || x0 >= PLOT_X + PLOT_W) return; + const live = tAbs >= s.a && tAbs < s.b; if (live) current = s; + const hue = si / Math.max(1, sections.length - 1); + rect(x0, RIB_Y, Math.max(1, x1 - x0 - 1), RIB_H, 220 - 90 * hue, 90 + 50 * hue, 140 + 70 * hue, live ? 0.9 : 0.3); + if (x1 - x0 > textW(s.name, 1) + 8) text(s.name.slice(0, 12), x0 + 5, RIB_Y + RIB_H / 2 - 4, 255, 255, 255, 1, live ? 1 : 0.7); + }); + rect(PLOT_X, RIB_Y, Math.max(0, xOfAbs(tAbs) - PLOT_X), RIB_H, 0, 0, 0, 0.08); + rect(xOfAbs(tAbs) - 1, RIB_Y - 6, 3, RIB_H + 12, 30, 20, 40, 0.95); + if (current) text(current.name, PAD, RIB_Y + RIB_H + 10, 50, 40, 60, 2); + + // piano roll + const wA = tAbs - (NOW_X - PLOT_X) / PLOT_W * WINDOW, wB = wA + WINDOW; + const xOfWin = (t) => PLOT_X + ((t - wA) / WINDOW) * PLOT_W; + const laneH = ROLL_H / LANES.length; + for (let i = 0; i < LANES.length; i++) { + const y = ROLL_Y + i * laneH; + rect(PLOT_X, y, PLOT_W, Math.max(1, laneH - 1), 238, 233, 228, i % 2 ? 0.6 : 0.95); + text(LANES[i].slice(0, 8), PLOT_X + 4, y + laneH / 2 - 3, 150, 140, 160, 1); + } + for (const b of bars) { + if (b.t < wA - 1 || b.t > wB) continue; + const x = xOfWin(b.t); if (x < PLOT_X || x > PLOT_X + PLOT_W) continue; + rect(x, ROLL_Y, 1, ROLL_H, 120, 110, 140, b.n % 4 === 1 ? 0.5 : 0.2); + } + for (const e of events) { + if (e.t > wB || (e.t + (e.dur || 0.12)) < wA) continue; + const li = laneOf(e), y = ROLL_Y + li * laneH; + const x0 = xOfWin(e.t), x1 = xOfWin(e.t + Math.max(e.dur || 0.10, 0.05)); + const [r, g, b] = laneColor(li); + const hot = Math.max(0, 1 - Math.abs(tAbs - e.t) / 0.22); + const gain = Math.min(1, (e.gain ?? 0.6) * 1.4); + const h = Math.max(2, (laneH - 4) * (0.4 + 0.6 * gain)); + rect(x0, y + (laneH - h) / 2, Math.max(2, x1 - x0), h, r, g, b, 0.45 + 0.5 * gain); + if (hot > 0) rect(x0 - 1, y + 1, Math.max(3, x1 - x0 + 2), laneH - 2, 40, 30, 50, 0.45 * hot); + } + rect(NOW_X - 1, ROLL_Y - 8, 2, ROLL_H + 16, 30, 20, 40, 0.9); + + // word tray: the scrolling window again — beat clicks as ticks, every word a block with its text + rect(PLOT_X, TRAY_Y, PLOT_W, TRAY_H, 236, 231, 226, 0.95); + for (const b of bars) for (let j = 0; j < b.beats.length - 1; j++) { const tb = b.beats[j]; if (tb < wA || tb > wB) continue; + const x = xOfWin(tb); rect(x, TRAY_Y + TRAY_H - (j === 0 ? 12 : 7), 1, j === 0 ? 12 : 7, 120, 40, 90, j === 0 ? 0.9 : 0.5); } + for (const w of WORDS) { + const a = OFFSET + w.fromMs / 1000, b = OFFSET + w.toMs / 1000; + if (b < wA || a > wB) continue; + const x0 = xOfWin(a), x1 = xOfWin(b), now = tAbs >= a && tAbs < b, sung = tAbs >= b; + rect(x0, TRAY_Y + 3, Math.max(2, x1 - x0 - 1), TRAY_H - 18, now ? 220 : sung ? 90 : 170, now ? 40 : sung ? 70 : 160, now ? 90 : sung ? 110 : 175, now ? 0.95 : 0.7); + if (x1 - x0 > textW(w.text, 1) + 4) text(w.text.replace(/[^\w' ]/g, ""), x0 + 2, TRAY_Y + 7, now ? 255 : 30, now ? 245 : 20, now ? 240 : 40, 1, 0.95); + } + rect(NOW_X - 1, TRAY_Y - 2, 2, TRAY_H + 4, 30, 20, 40, 0.9); + + lyricBand(tRel, LYR_Y, LYR_H, 4); + + // waveform + const half = WAVE_H / 2, mid = WAVE_Y + half; + for (let i = 0; i < PLOT_W; i++) { + const v = wave[Math.floor((i / PLOT_W) * PEAKS)] || 0, h = Math.max(1, v * half * 0.95); + const played = PLOT_X + i <= xOfAbs(tAbs); + rect(PLOT_X + i, mid - h, 1, h * 2, played ? 220 : 200, played ? 40 : 190, played ? 90 : 200, played ? 0.95 : 0.6); + } + rect(xOfAbs(tAbs) - 1, WAVE_Y - 4, 2, WAVE_H + 8, 30, 20, 40, 0.9); + return fb; +} + +// the line being sung (or the next one coming), the sung word lit +function lyricBand(tRel, LYR_Y, LYR_H, maxScale) { + rect(PLOT_X, LYR_Y, PLOT_W, LYR_H, 244, 240, 236, 0.9); + // v19: the line that started most recently (0.4 s early), else the first one coming — never flips back + let li = -1; lines.forEach((l, k) => { if (tRel >= l.a - 0.4) li = k; }); + if (li < 0) li = 0; + if (li >= 0) { + const L = lines[li], next = lines[li + 1]; + let scale = maxScale; while (scale > 2 && textW(L.text, scale) > PLOT_W - 24) scale--; + const fadeIn = Math.min(1, Math.max(0, (tRel - (L.a - 1.0)) / 0.5)), fadeOut = tRel > L.b + 1.0 ? Math.max(0, 1 - (tRel - L.b - 1.0) / 0.6) : 1; + const alpha = Math.min(fadeIn, fadeOut); + let cx = PLOT_X + (PLOT_W - textW(L.text, scale)) / 2; + const toks = L.text.split(/\s+/); + toks.forEach((tok, k) => { + const w = L.words[k]; const a = w ? w.fromMs / 1000 : 0, b = w ? w.toMs / 1000 : 0; + const sung = tRel >= b, now = tRel >= a && tRel < b; + const wpx = textW(tok, scale) - 1 * scale; + // the unsung word in grey, the sung word in ink, the word being sung wiped + // left to right in red — piecewise through its whisper tokens when it has them + text(tok, cx, LYR_Y + 16, sung ? 40 : 150, sung ? 30 : 140, sung ? 45 : 150, scale, alpha * (sung ? 1 : 0.7)); + if (now) { + let frac = (tRel - a) / Math.max(0.01, b - a); + const T = w.tokens || []; + if (T.length > 1) { const tot = tok.length; let done = 0; frac = 1; + for (let q = 0; q < T.length; q++) { const ta = T[q].fromMs / 1000, tb = T[q].toMs / 1000, len = T[q].text.length / tot; + if (tRel < ta) { frac = done; break; } if (tRel < tb) { frac = done + len * (tRel - ta) / Math.max(0.01, tb - ta); break; } done += len; } } + const wipe = Math.max(0, Math.min(1, frac)) * wpx; + // redraw the wiped part in red by clipping: draw the word again, then paper over the unsung remainder + text(tok, cx, LYR_Y + 16, 220, 40, 90, scale, alpha); + rect(cx + wipe, LYR_Y + 12, Math.max(0, wpx - wipe + 2), 7 * scale + 8, 244, 240, 236, 1); + text(tok, cx, LYR_Y + 16, 150, 140, 150, scale, alpha * 0.7 * 0); // (nothing) — keep alignment + // the grey remainder back on top of the paper patch + { let ccx = cx; for (const ch of tok.toUpperCase()) { const gl = FONT[ch]; if (gl) for (let jj = 0; jj < 7; jj++) for (let ii = 0; ii < 5; ii++) if (gl[jj * 5 + ii] === "1") { const gx = ccx + ii * scale; if (gx + scale > cx + wipe) rect(gx, LYR_Y + 16 + jj * scale, scale, scale, 150, 140, 150, alpha * 0.7); } ccx += 6 * scale; } } + } + cx += textW(tok, scale) + 6 * scale; + }); + if (next) text(next.text, PLOT_X + (PLOT_W - textW(next.text, 2)) / 2, LYR_Y + LYR_H - 24, 120, 110, 130, 2, 0.6 * alpha); + } +} + +// ── the lyric check (loner's review-score lanes): every word is a BLOCK on its own track, +// rotating through LANES_N rows so neighbours never share one; each block carries the word and +// her own waveform inside it; the grey strip on top is the whole voice, so gaps and bleed show +const LWIN = Number(arg("window", 7)), LANES_N = Number(arg("lanes", 4)); +function drawLyrics(fi) { + const tRel = fi / FPS, tAbs = OFFSET + tRel; + for (let y = 0; y < H; y++) rect(0, y, W, 1, 250 - 8 * (y / H), 247 - 8 * (y / H), 242 - 10 * (y / H)); + text(`${title} - lyric check`, PAD, 22, 40, 30, 45, 3); + const clock = `${String(Math.floor(tRel / 60)).padStart(2, "0")}:${String((tRel % 60).toFixed(1)).padStart(4, "0")}`; + text(clock, W - PAD - textW(clock, 3), 22, 120, 110, 130, 3); + text(`BAR ${barAt(tAbs)} HER VOICE ONLY - ONE BLOCK PER WORD (FORCED ALIGNMENT)`, PAD, 52, 130, 120, 140, 2); + const wA = tRel - LWIN * 0.35, xOf = (t) => PLOT_X + ((t - wA) / LWIN) * PLOT_W, tOf = (x) => wA + ((x - PLOT_X) / PLOT_W) * LWIN; + // the whole voice, thin, on top + const VY = 84, VH = 60, vmid = VY + VH / 2; + rect(PLOT_X, VY, PLOT_W, VH, 236, 231, 226, 0.95); + for (let i = 0; i < PLOT_W; i++) { const h = Math.max(1, envAt(tOf(PLOT_X + i) + OFFSET) * (VH / 2 - 3)); rect(PLOT_X + i, vmid - h, 1, h * 2, 150, 140, 160, 0.8); } + // the word lanes + const LY = VY + VH + 12, LH = 64, LG = 6; + for (let r = 0; r < LANES_N; r++) rect(PLOT_X, LY + r * (LH + LG), PLOT_W, LH, 238, 233, 228, 0.9); + const PAL = [[120, 70, 190], [60, 110, 200], [30, 150, 140], [200, 120, 40], [170, 60, 150]]; + WORDS.forEach((w, k) => { + const a = w.fromMs / 1000, b = w.toMs / 1000; + if (b < wA || a > wA + LWIN) return; + const r = k % LANES_N, y0 = LY + r * (LH + LG), mid = y0 + LH / 2; + const x0 = Math.max(PLOT_X, xOf(a)), x1 = Math.min(PLOT_X + PLOT_W, xOf(b)); + const now = tRel >= a && tRel < b, sung = tRel >= b; + const [cr, cg, cb] = now ? [220, 40, 90] : PAL[k % PAL.length]; + rect(x0, y0 + 2, Math.max(2, x1 - x0), LH - 4, cr, cg, cb, now ? 0.28 : 0.14); + for (let x = Math.ceil(x0); x < x1; x++) { const h = Math.max(1, envAt(tOf(x) + OFFSET) * (LH / 2 - 8)); rect(x, mid - h + 6, 1, h * 2 - 6, cr, cg, cb, sung || now ? 0.9 : 0.55); } + rect(x0, y0 + 2, 2, LH - 4, cr, cg, cb, 0.95); rect(x1 - 2, y0 + 2, 2, LH - 4, cr, cg, cb, 0.6); + text(w.text.replace(/[^\w' ?,]/g, ""), x0 + 5, y0 + 6, now ? 200 : 40, now ? 30 : 30, now ? 80 : 50, 2, 0.95); + }); + rect(xOf(tRel) - 1, VY - 6, 3, LY + LANES_N * (LH + LG) - VY + 6, 30, 20, 40, 0.95); + lyricTicker(tRel, xOf(tRel), H - 24 - 110, 110); + return fb; +} + +// one stream of words scrolling right to left, the sung word crossing the playhead as she sings it +const TSC = 4, TGAP = 6 * TSC; +const REC = WORDS.slice().sort((a, b) => a.fromMs - b.fromMs); // record order: verse 2 plays before chorus 1 now +const tickX = []; { let x = 0; for (const w of REC) { tickX.push(x); x += textW(w.text, TSC) + TGAP; } } +function lyricTicker(tRel, headX, Y, Hh) { + rect(PLOT_X, Y, PLOT_W, Hh, 244, 240, 236, 0.9); + let k = REC.findIndex((w) => w.fromMs / 1000 > tRel) - 1; if (k < -1) k = REC.length - 1; + let pos; // stream x under the playhead + if (k < 0) pos = tickX[0] - (REC[0].fromMs / 1000 - tRel) * 120; + else { const w = REC[k], a = w.fromMs / 1000, nb = k + 1 < REC.length ? REC[k + 1].fromMs / 1000 : a + 1; + const x0 = tickX[k], x1 = k + 1 < REC.length ? tickX[k + 1] : x0 + textW(w.text, TSC); + pos = x0 + (x1 - x0) * Math.min(1, (tRel - a) / Math.max(0.05, nb - a)); } + const ty = Y + (Hh - 7 * TSC) / 2; + REC.forEach((w, q) => { + const x = headX + tickX[q] - pos, ww = textW(w.text, TSC); + if (x > PLOT_X + PLOT_W || x + ww < PLOT_X) return; + const a = w.fromMs / 1000, b = w.toMs / 1000, now = tRel >= a && tRel < b, sung = tRel >= b; + const [r, g, bl] = now ? [220, 40, 90] : sung ? [40, 30, 45] : [160, 150, 160]; + text(w.text, x, ty, r, g, bl, TSC, now || sung ? 1 : 0.75); + rect(x, ty + 7 * TSC + 6, Math.max(2, (b - a) * 60), 3, r, g, bl, 0.6); // its sung length, as a bar + }); + rect(PLOT_X, Y, 2, Hh, 244, 240, 236, 1); rect(headX - 1, Y + 6, 3, Hh - 12, 30, 20, 40, 0.9); +} + +// ── encode ──────────────────────────────────────────────────────────── +const frames = Math.ceil(DUR * FPS); +console.log(`▸ score video · ${W}x${H}@${FPS} · ${frames} frames · ${DUR.toFixed(1)}s`); +console.log(` audio ${audio}`); +console.log(` receipt ${receiptPath || "(none)"} · ${events.length} events · ${sections.length} sections · ${lines.length} lyric lines`); +console.log(` offset ${OFFSET}s into the take`); +const ff = spawn("ffmpeg", ["-v", "error", "-y", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", `${W}x${H}`, "-r", String(FPS), "-i", "-", + "-i", audio, "-map", "0:v", "-map", "1:a", "-c:v", "libx264", "-preset", "medium", "-crf", "19", "-pix_fmt", "yuv420p", + "-c:a", "aac", "-b:a", "320k", "-movflags", "+faststart", "-shortest", outPath], { stdio: ["pipe", "inherit", "inherit"] }); +let i = 0; +function pump() { + while (i < frames) { + const buf = LYRICS ? drawLyrics(i++) : drawFrame(i++); + if (!ff.stdin.write(buf)) { ff.stdin.once("drain", pump); return; } + if (i % (FPS * 10) === 0) process.stdout.write(`\r ${((i / frames) * 100).toFixed(0)}% `); + } + ff.stdin.end(); process.stdout.write("\r 100% \n"); +} +pump(); +ff.on("close", (code) => { if (code !== 0) { console.error("ffmpeg failed"); process.exit(1); } console.log(`✓ ${outPath}`); }); diff --git a/pop/sailor-song/bin/sister.py b/pop/sailor-song/bin/sister.py new file mode 100644 index 0000000000..de3311eafa --- /dev/null +++ b/pop/sailor-song/bin/sister.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""sister.py — a sister for s@ge: her line, another voice, other octaves. + +The harmony stems (aesthetivox.py) keep her formants, so they are HER at +another pitch. A sister keeps the line but changes the singer: the WORLD +spectral envelope (cheaptrick) is warped along the frequency axis — a +smaller vocal tract for the high sister (formants up), a larger one for the +low sister (formants down) — the aperiodicity follows, the f0 moves an +octave, the vibrato is her own rate but her own depth is replaced with the +sister's, and the whole line is a few milliseconds behind her, like a second +person breathing with her. Vowels only where she is voiced; her consonants +stay in the lead. + + sister-high.wav +12 st, formants x1.09, vibrato 5.6 Hz / 0.18 st, 14 ms late + sister-low.wav -12 st, formants x0.94, vibrato 4.4 Hz / 0.12 st, 22 ms late + + pop/.venv/bin/python pop/sailor-song/bin/sister.py → src/vox/reg/sister-{high,low}.wav +""" +import os, warnings +import numpy as np, soundfile as sf +with warnings.catch_warnings(): + warnings.simplefilter("ignore"); import pyworld as pw + +import sys +LANE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +STEMS = sys.argv[sys.argv.index("--stems") + 1] if "--stems" in sys.argv else "reg" +REG = os.path.join(LANE, "src/vox", STEMS) +FP = 5.0 +SISTERS = { + "high": dict(st=+12, formant=1.09, vib_hz=5.6, vib_st=0.18, late_ms=14, gain=0.9), + "low": dict(st=-12, formant=0.94, vib_hz=4.4, vib_st=0.12, late_ms=22, gain=0.9), +} + +x, fs = sf.read(os.path.join(REG, "vocals-aesthetivox.wav")) +if x.ndim > 1: x = x.mean(1) +x = x.astype(np.float64) +print("WORLD on her lead …", flush=True) +f0, t = pw.harvest(x, fs, f0_floor=150, f0_ceil=900, frame_period=FP) +f0 = pw.stonemask(x, f0, t, fs) +fft = pw.get_cheaptrick_fft_size(fs, f0_floor=150.0) +sp = pw.cheaptrick(x, f0, t, fs, fft_size=fft, f0_floor=150.0) +apr = pw.d4c(x, f0, t, fs, fft_size=fft) +voiced = f0 > 0 +runs = np.diff(np.concatenate([[0], voiced.astype(int), [0]])) +for s0, s1 in zip(np.where(runs == 1)[0], np.where(runs == -1)[0]): + if (s1 - s0) * FP < 60: voiced[s0:s1] = False +nf, nb = sp.shape +bins = np.arange(nb) + +def warp(env, k): + """Resample the envelope along frequency: bin i of the sister reads bin i/k of hers.""" + src = np.clip(bins / k, 0, nb - 1) + lo = np.floor(src).astype(int); hi = np.minimum(lo + 1, nb - 1); fr = src - lo + return env[:, lo] * (1 - fr) + env[:, hi] * fr + +# her vibrato is smoothed away and the sister's put on: a 150 ms running mean of f0 in cents +m = np.where(voiced, 12 * np.log2(np.maximum(f0, 1) / 440) + 69, np.nan) +k = int(150 / FP) +ms = np.copy(m) +for i in range(nf): + a0, a1 = max(0, i - k // 2), min(nf, i + k // 2 + 1); seg = m[a0:a1]; seg = seg[np.isfinite(seg)] + ms[i] = seg.mean() if len(seg) else np.nan +tt = np.arange(nf) * FP / 1000 +for name, S in SISTERS.items(): + vib = S["vib_st"] * np.sin(2 * np.pi * S["vib_hz"] * tt + 1.3) + mm = np.where(np.isfinite(ms), ms, np.nan) + S["st"] + vib + fz = np.where(voiced & np.isfinite(mm), 440 * 2 ** ((mm - 69) / 12), 0.0) + # keep f0 continuous through gaps (WORLD pops on 0 → tone); silence comes from the gate below + last = 0.0 + for i in range(nf): + if fz[i] > 0: last = fz[i] + elif last > 0: fz[i] = last + fz = np.where(fz > 0, fz, 220.0) + spw = warp(sp, S["formant"]); apw = warp(apr, S["formant"]) + print(f" sister {name}: synthesizing …", flush=True) + y = pw.synthesize(np.ascontiguousarray(fz), np.ascontiguousarray(spw), np.ascontiguousarray(apw), fs, frame_period=FP)[:len(x)] + gate = np.repeat(voiced.astype(float), int(fs * FP / 1000))[:len(y)] + if len(gate) < len(y): gate = np.pad(gate, (0, len(y) - len(gate))) + w = int(0.08 * fs); gate = np.convolve(gate, np.ones(w) / w, mode="same") # v12: 80 ms swells + y = y * gate + late = int(S["late_ms"] / 1000 * fs); y = np.concatenate([np.zeros(late), y])[:len(x)] + y *= S["gain"] * 0.5 / (np.max(np.abs(y)) + 1e-9) + sf.write(os.path.join(REG, f"sister-{name}.wav"), y.astype(np.float32), fs) +# ── the hum: long vowels held per bar from her own sustained frames ────── +# For each bar, the frame in her line with the most low-band energy (a wide +# open vowel) is FROZEN and held for the whole bar at the sister-low octave of +# the chord's root or fifth; in the verses the envelope is rolled off above +# 1.2 kHz so it reads as a closed-mouth hum, in the choruses it opens. +import json +M = json.load(open(os.path.join(LANE, "measures.reg.json" if STEMS == "reg" else "measures.json")))["bars"] +PCS = {"G#m": (8, 3), "Emaj7": (8, 3), "B": (11, 6)} # root, fifth +def mtof(m): return 440.0 * 2 ** ((m + 0.13 - 69) / 12) +freqs = np.linspace(0, fs / 2, nb) +lowband = (freqs > 200) & (freqs < 900) +energy = sp[:, lowband].mean(1) * voiced +nfh = int((M[-1]["t"] + M[-1]["dur"]) * 1000 / FP) + 2 +sph = np.zeros((nfh, nb)); aph = np.ones((nfh, nb)); fh = np.zeros(nfh) +roll = 1 / (1 + (freqs / 1200.0) ** 4) # the hum's closed mouth +vibh = 0.10 * np.sin(2 * np.pi * 4.2 * np.arange(nfh) * FP / 1000) +for i, b in enumerate(M): + a0, a1 = int(b["t"] * 1000 / FP), min(nfh, int((b["t"] + b["dur"]) * 1000 / FP)) + seg = energy[a0:min(a1, nf)] + j = a0 + int(np.argmax(seg)) if len(seg) and seg.max() > 0 else None + if j is None: # she is silent here: hold the last vowel + prev = [q for q in range(a0 - 1, 0, -1) if voiced[q]] + j = prev[0] if prev else None + if j is None: continue + root, fifth = PCS[b["chord"]] + midi = 44 + ((root if b["n"] % 4 < 2 else fifth) - 8) % 12 # G#2 / D#3 region, under her + if midi > 51: midi -= 12 + open_mouth = 1.0 if b["n"] >= 52 else 0.0 + env_row = sp[j] * (roll * (1 - open_mouth) + open_mouth) + for fr in range(a0, a1): + sph[fr] = env_row; aph[fr] = apr[j]; fh[fr] = mtof(midi) * 2 ** (vibh[fr] / 12) +for i in range(1, nfh): # 100 ms glide at bar lines + if fh[i] > 0 and fh[i - 1] > 0: fh[i] = fh[i - 1] + (fh[i] - fh[i - 1]) / 20 +fh = np.where(fh > 0, fh, 110.0) +print(" sister hum: synthesizing …", flush=True) +y = pw.synthesize(np.ascontiguousarray(fh), np.ascontiguousarray(sph), np.ascontiguousarray(aph), fs, frame_period=FP) +on = np.repeat((sph.sum(1) > 0).astype(float), int(fs * FP / 1000))[:len(y)] +if len(on) < len(y): on = np.pad(on, (0, len(y) - len(on))) +w = int(0.25 * fs); on = np.convolve(on, np.ones(w) / w, mode="same") # slow swell in and out +y = y * on; y *= 0.5 / (np.max(np.abs(y)) + 1e-9) +sf.write(os.path.join(REG, "sister-hum.wav"), y.astype(np.float32), fs) +print("✓", REG) diff --git a/pop/sailor-song/bin/splice.mjs b/pop/sailor-song/bin/splice.mjs new file mode 100644 index 0000000000..7aa1bd3466 --- /dev/null +++ b/pop/sailor-song/bin/splice.mjs @@ -0,0 +1,223 @@ +#!/usr/bin/env node +// splice.mjs — the ARRANGEMENT of her take: the record is a sequence of +// SEGMENTS of the regularized stems, in any order, joined with short +// equal-power seams (12 ms on her voice, 80 ms on the sustained stems so a +// strum or a held vowel never clicks). The music around her is scored by the +// engine on the re-sequenced chart, so it is continuous by construction — +// only her own stems carry seams, and every seam sits on a beat. +// +// v13 form: verse 1 · verse 2 · chorus 1 · chorus 2 · break · bridge · outro +// A open → bar 27 downbeat (intro + verse 1) +// B bar 43 beat 3 → chorus 2's "Oh" (verse 2, on its "And" pickup) +// C bar 27 → bar 43 beat 3 (chorus 1, on its "Oh, won't you") +// D chorus 2's "Oh" → end (chorus 2 · break · bridge · outro) +// B opens with a 2-beat pickup bar and closes on one; C's closing half bar +// ("so long") and D's opening half bar ("Oh, won't you") make one full bar. +// +// Anchors: { bar, beat } (chart bar number, beat 1-based) or +// { word, next, afterSec, beforeSec, lead } (a sung onset from +// src/words-aligned.json, take time, mapped through lock + reg). +// +// Reads src/vox/reg/*.wav, measures.reg.json, src/words-aligned.json, the maps +// Writes src/vox/cut/*.wav, src/vox/cut/segmap.txt ("from to offset" in reg s), +// measures.cut.json (bars in record order, numbers kept) +// +// node pop/sailor-song/bin/splice.mjs + +import { readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const LANE = resolve(HERE, ".."); +const VOX = resolve(LANE, "src/vox"); +const REG = resolve(VOX, "reg"), OUT = resolve(VOX, "cut"); +mkdirSync(OUT, { recursive: true }); +const SR = 48000; + +const M = JSON.parse(readFileSync(resolve(LANE, "measures.reg.json"), "utf8")); +const W = JSON.parse(readFileSync(resolve(LANE, "src/words-aligned.json"), "utf8")); +const through = (path) => { const m = readFileSync(path, "utf8").trim().split("\n").map((l) => l.split(" ").map((x) => Number(x) / SR)); + return (t) => { let k = m.findIndex(([s]) => s > t); if (k <= 0) k = 1; const [s0, d0] = m[k - 1], [s1, d1] = m[k]; return d0 + ((t - s0) / (s1 - s0)) * (d1 - d0); }; }; +const lock = through(resolve(VOX, "locked/timemap.txt")), reg = through(resolve(REG, "timemap.txt")); +const regOf = (takeSec) => reg(lock(takeSec)); +const norm = (s) => s.toLowerCase().replace(/[^a-z']/g, ""); +const END = M.bars.at(-1).t + M.bars.at(-1).dur + 0.5; + +// v17: "me?" runs straight into "Oh" with no breath — the split is the tuned-note onset of +// "Oh" (her pitch change), found from vox-notes.json in the 0.3 s around the word's start +// v18: her "me?" is a three-note question (G#4 F#4 G#4); "Oh" is the D#4 that follows — cut on that pitch +const OH1 = { noteNear: { word: "Oh", next: "won't", afterSec: 55, beforeSec: 62 }, window: 0.5, pitch: "D#4", lead: 0.012 }; +const AND = { word: "And", next: "lately", afterSec: 88, beforeSec: 96, lead: 0.02, voiceOnset: true }; // v19d: placed on her voice, not the word stamp +const OH2 = { word: "Oh", next: "won't", afterSec: 105, beforeSec: 112, lead: 0.05 }; +// v14: A runs to just before chorus 1's "Oh" so "me?" finishes; B keeps two beats of +// guitar before "And" but her VOICE is held until "And" (voiceFrom), so verse 1's last +// word and verse 2's first never overlap; C runs through all of "so long". +// v16: a proper bar between "me?" and "And lately" — one voice-free bar of her guitar +// from the intro (bar 10, G#m), carried as bar 26 so the beds hold verse 1's state — +// then verse 2 on its one-beat "And" pickup. +// v19: no inserted guitar bar. By ear her "me?" keeps going through the held D#4 that +// whisper labels "Oh" — v18 cut at that D#4 and chorus 1 (which opens on the bar-27 +// downbeat, inside "me?") replayed the rest of it after verse 2. Now verse 1 runs to +// the next pitch change (the C#4 into "won't"), finishing "me" with a short release, and +// chorus 1's voice is held until that same point. +// v19c: MMS forced alignment (bin/forced-align.py) against her pitch notes settles it: "to" is +// the G#4 at take 58.37, "me?" runs F#4 → A4 → the D#4 at 59.27–59.56, and "Oh" is the NEXT +// D#4 attack at 59.77 (whisper-1 agrees, 59.72). The voice never breaks between them; the +// quietest 10 ms is take 59.66 = reg 59.182. Verse 1 ends there with a short release on +// "me?", and chorus 1's voice opens from the same point. Reg seconds (bar 27 is unlifted). +const ME_END = 59.182, OH_START = 59.19; +const SEGMENTS = [ + { name: "A intro+verse1", from: 0, to: ME_END, tail: { grab: 0.2, before: 0.12, len: 0.9, curve: 1.0 } }, // v19d: "me?" cut off at 0:33 — a longer release from the steady D#4 + // v19: a breath before verse 2 kicks off — verse 1's last full bar of her guitar, + // slowed a fifth (screwed, 7 st down — in key; 0.78 sat between keys and read "werd") and stuttered on 8ths and 16ths into the pickup; + // her voice only finishes "me" over it; charted as an intro bar so the kit drops out + { name: "S screw", screw: { bar: 26, rate: 2 ** (-7 / 12), lenOfBar: 44, beats: 2 }, asBar: 26 }, // v19d: half a bar, and the kit keeps going (was a whole intro bar) + { name: "B verse2", from: { bar: 43, beat: 4 }, to: OH2, voiceFrom: AND }, + { name: "C chorus1", from: { bar: 27, beat: 1 }, to: AND, voiceFrom: OH_START }, + { name: "D chorus2..end", from: OH2, to: END }, +]; +const NOTES = JSON.parse(readFileSync(resolve(LANE, "vox-notes.json"), "utf8")).notes.map((n) => ({ t: regOf(n.t), note: n.note })).sort((x, y) => x.t - y.t); +// the first 10 ms, from 0.35 s before the stamp to 0.5 s after, where her voice rises out of +// a gap into singing (the regularized lead stem): quiet below 12% of the window's peak, then +// two frames above 30% of it +let _lead = null; +function voiceOnset(t) { + _lead ??= readWav(resolve(REG, ["vocals-natural.wav", "vocals-aesthetivox.wav"].find((f) => readdirSync(REG).includes(f))))[0]; + const F = Math.round(0.01 * SR), a = Math.round((t - 0.35) * SR), n = Math.round(0.85 * SR / F); + const rms = Array.from({ length: n }, (_, k) => { let e = 0; for (let i = 0; i < F; i++) { const v = _lead[a + k * F + i] || 0; e += v * v; } return Math.sqrt(e / F); }); + const peak = Math.max(...rms); + for (let k = 1; k < n - 1; k++) if (rms[k - 1] < 0.12 * peak && rms[k] >= 0.3 * peak && rms[k + 1] >= 0.3 * peak) return (a + k * F) / SR; + return t; +} +const anchor = (a) => { + if (typeof a === "number") return a; + if (a === 1e9) return 1e9; + if (a.noteNear) { const w = anchor({ ...a.noteNear, lead: 0 }); const c = NOTES.filter((n) => Math.abs(n.t - w) <= (a.window || 0.3) && (!a.pitch || n.note === a.pitch)).map((n) => n.t); + if (!c.length) throw new Error("no note onset near " + JSON.stringify(a.noteNear)); return c.reduce((p, t) => Math.abs(t - w) < Math.abs(p - w) ? t : p) - (a.lead || 0); } + if (a.bar) { const b = M.bars.find((x) => x.n === a.bar); if (!b) throw new Error(`no bar ${a.bar}`); return b.beats[a.beat - 1]; } + const wi = W.findIndex((w, i) => norm(w.text) === norm(a.word) && W[i + 1] && norm(W[i + 1].text) === norm(a.next) && w.fromMs / 1000 > a.afterSec && w.fromMs / 1000 < a.beforeSec); + if (wi < 0) throw new Error(`word not found: ${JSON.stringify(a)}`); + const t = regOf(W[wi].fromMs / 1000); + return (a.voiceOnset ? voiceOnset(t) : t) - (a.lead || 0); +}; +let offset = 0; +const barBy = (n) => { const b = M.bars.find((x) => x.n === n); if (!b) throw new Error(`no bar ${n}`); return b; }; +const segs = SEGMENTS.map((s) => { if (s.screw) { const src = barBy(s.screw.bar), len = barBy(s.screw.lenOfBar).dur * (s.screw.beats || 4) / 4; // bar 44 is already on the lifted clock + const seg = { ...s, from: src.t, to: src.t + len, offset, voiceFrom: null, chord: src.chord }; offset += len; return seg; } + const from = anchor(s.from), to = anchor(s.to); const seg = { ...s, from, to, offset, voiceFrom: s.voiceFrom ? anchor(s.voiceFrom) : null }; offset += to - from; return seg; }); +for (const s of segs) console.log(`${s.name.padEnd(16)} ${s.from.toFixed(3)} → ${s.to.toFixed(3)} (${(s.to - s.from).toFixed(2)} s) at ${s.offset.toFixed(3)}`); +writeFileSync(resolve(OUT, "segmap.txt"), segs.map((s) => `${s.from.toFixed(4)} ${s.to.toFixed(4)} ${s.offset.toFixed(4)}`).join("\n") + "\n"); +const recOf = (t) => { for (const s of segs) if (!s.screw && t >= s.from && t < s.to) return t - s.from + s.offset; return null; }; + +// ── the chart: bars in record order, partial bars truncated at the seams ── +const bars = []; +for (const s of segs) { + if (s.screw) { const d = s.to - s.from, src = barBy(s.screw.bar); + bars.push({ ...src, n: s.asBar, t: +s.offset.toFixed(3), dur: +d.toFixed(3), beats: Array.from({ length: (s.screw.beats || 4) + 1 }, (_, k) => +(s.offset + d * k / (s.screw.beats || 4)).toFixed(3)), strum: [] }); continue; } + for (const b of M.bars) { + const t0 = b.t, t1 = b.t + b.dur; + if (t1 <= s.from || t0 >= s.to) continue; + const inside = b.beats.filter((t) => t >= s.from - 1e-6 && t < s.to + 1e-6); + const beats = inside.map((t) => +(t - s.from + s.offset).toFixed(3)); + if (beats.length && beats[0] > +(Math.max(t0, s.from) - s.from + s.offset).toFixed(3) + 1e-6) beats.unshift(+(Math.max(t0, s.from) - s.from + s.offset).toFixed(3)); + const endT = +(Math.min(t1, s.to) - s.from + s.offset).toFixed(3); + if (!beats.length || beats.at(-1) < endT - 1e-6) beats.push(endT); + if (beats.length < 2) continue; // v19d: a one-beat pickup is a bar too (its kick keeps the pulse) + const cutL = t0 < s.from, cutR = t1 > s.to; + bars.push({ ...b, n: s.asBar ?? b.n, t: beats[0], dur: +(endT - beats[0]).toFixed(3), beats, strum: cutL ? b.strum.slice(-(beats.length - 1) * 2) : cutR ? b.strum.slice(0, (beats.length - 1) * 2) : b.strum }); + } +} +const cut = { ...M, cut: { segments: segs.map((s) => ({ name: s.name, from: +s.from.toFixed(3), to: +s.to.toFixed(3), offset: +s.offset.toFixed(3) })), totalSec: +offset.toFixed(3) }, + beats: { ...M.beats, snapped: M.beats.snapped.map(recOf).filter((t) => t !== null).sort((a, b) => a - b) }, + bars, strumList: M.strumList.map((x) => ({ ...x, t: recOf(x.t) })).filter((x) => x.t !== null).sort((a, b) => a.t - b.t).map((x) => ({ ...x, t: +x.t.toFixed(3) })) }; +writeFileSync(resolve(LANE, "measures.cut.json"), JSON.stringify(cut, null, 1)); +console.log(`chart: ${M.bars.length} → ${bars.length} bars in record order (${bars.filter((b) => b.beats.length < 5).map((b) => `${b.n}:${b.beats.length - 1}`).join(" ")} short), ${offset.toFixed(1)} s`); + +// ── the stems ───────────────────────────────────────────────────────── +function readWav(p) { + const buf = readFileSync(p); let i = 12, fmt = 1, ch = 1, sr = SR, bits = 16, data = null; + while (i < buf.length - 8) { const id = buf.toString("ascii", i, i + 4), size = buf.readUInt32LE(i + 4), body = i + 8; + if (id === "fmt ") { fmt = buf.readUInt16LE(body); ch = buf.readUInt16LE(body + 2); sr = buf.readUInt32LE(body + 4); bits = buf.readUInt16LE(body + 14); } + else if (id === "data") { data = { body, size }; break; } + i = body + size + (size & 1); } + if (!data || sr !== SR) throw new Error(`bad wav ${p}`); + const bps = bits / 8, frames = Math.floor(data.size / (bps * ch)); const out = Array.from({ length: ch }, () => new Float32Array(frames)); + for (let f = 0; f < frames; f++) for (let c = 0; c < ch; c++) { const o = data.body + (f * ch + c) * bps; + out[c][f] = fmt === 3 ? buf.readFloatLE(o) : bits === 24 ? ((buf.readUInt8(o) | (buf.readUInt8(o + 1) << 8) | (buf.readInt8(o + 2) << 16)) / 8388608) : buf.readInt16LE(o) / 32768; } + return out; +} +function writeWav(p, chans) { + const ch = chans.length, n = chans[0].length, buf = Buffer.alloc(44 + n * ch * 4); + buf.write("RIFF", 0); buf.writeUInt32LE(36 + n * ch * 4, 4); buf.write("WAVE", 8); buf.write("fmt ", 12); buf.writeUInt32LE(16, 16); + buf.writeUInt16LE(3, 20); buf.writeUInt16LE(ch, 22); buf.writeUInt32LE(SR, 24); buf.writeUInt32LE(SR * ch * 4, 28); buf.writeUInt16LE(ch * 4, 32); buf.writeUInt16LE(32, 34); + buf.write("data", 36); buf.writeUInt32LE(n * ch * 4, 40); + for (let f = 0; f < n; f++) for (let c = 0; c < ch; c++) buf.writeFloatLE(chans[c][f], 44 + (f * ch + c) * 4); + writeFileSync(p, buf); +} +// the seam: each segment fades in over xf and the previous fades out over the same span (equal power), overlapping +function assemble(x, xfSec, voice) { + const total = Math.round(offset * SR), out = new Float32Array(total), XF = Math.round(xfSec * SR); + for (const s of segs) { + const a = Math.round(s.from * SR), b = Math.round(s.to * SR), o = Math.round(s.offset * SR); + if (s.screw) { if (!voice) screw(x, out, s); continue; } + for (let i = 0; i < b - a && o + i < total; i++) { + const src = a + i < x.length ? x[a + i] : 0; + let gin = s.offset > 0 && i < XF ? Math.sqrt(0.5 - 0.5 * Math.cos(Math.PI * i / XF)) : 1; + if (voice && s.voiceFrom !== null) { const v0 = Math.round((s.voiceFrom - s.from) * SR), VF = Math.round(0.012 * SR); // her voice held until voiceFrom + gin *= i < v0 - VF ? 0 : i < v0 ? 0.5 - 0.5 * Math.cos(Math.PI * (i - (v0 - VF)) / VF) : 1; } + out[o + i] += src * gin; + } + if (voice && s.tail) freezeTail(x, out, b, o + (b - a), s.tail); + // the previous segment's tail rides under this one's head (voice: a 40 ms release, not a hard 12) + const XO = voice ? Math.round(0.04 * SR) : XF; + const prevTail = segs.find((p) => (p.tail || p.screw) && Math.abs(p.offset + (p.to - p.from) - s.offset) < 1e-6); + if (s.offset > 0 && !(voice && prevTail) && !(prevTail && prevTail.screw)) for (let i = 0; i < XO && o + i < total && a - XO + i >= 0; i++) { + const prev = segs.find((p) => Math.abs(p.offset + (p.to - p.from) - s.offset) < 1e-6); + if (!prev) break; + const pb = Math.round(prev.to * SR); const src = pb + i < x.length ? x[pb + i] : 0; + out[o + i] += src * Math.sqrt(0.5 + 0.5 * Math.cos(Math.PI * i / XO)); + } + } + return out; +} +// a held release for a note that never ends in the take: overlapping Hann grains read from +// the last `grab` seconds before the cut (so pitch and timbre stay hers), crossfaded in +// from the real signal and faded out over `len` with a gentle curve. +// chopped and screwed: the source bar read at `rate` (slower and lower, linear interp), +// the first five eighths played through, then the sixth eighth stuttered twice and the +// last quarter as four 16th repeats of one slice, each chunk with 6 ms fades +function screw(x, out, s) { + const o = Math.round(s.offset * SR), n = Math.round((s.to - s.from) * SR), a = Math.round(s.from * SR), r = s.screw.rate; + const slow = new Float32Array(n); + for (let i = 0; i < n; i++) { const p = a + i * r, k = Math.floor(p), f = p - k; slow[i] = (x[k] || 0) * (1 - f) + (x[k + 1] || 0) * f; } + const e8 = Math.floor(n / 8), F = Math.round(0.006 * SR); + const chunks = [[0, 5 * e8], [5 * e8, e8], [5 * e8, e8], [6 * e8, e8 / 2], [6 * e8, e8 / 2], [6 * e8, e8 / 2], [6 * e8, e8 / 2]]; + let at = 0; + chunks.forEach(([from, len], c) => { len = Math.round(len); const g = c < 3 ? 1 : 0.9 - 0.12 * (c - 3); + for (let i = 0; i < len && at + i < n; i++) { const fade = Math.min(1, i / F, (len - i) / F); out[o + at + i] += slow[from + i] * fade * g; } + at += len; }); +} +function freezeTail(x, out, cutAt, at, { grab, len, before = 0, curve = 1.6 }) { + const G = Math.round(0.09 * SR), HOP = Math.round(0.0225 * SR), N = Math.round(len * SR), B = Math.round(grab * SR); + const base = cutAt - Math.round(before * SR) - B, XIN = Math.round(0.03 * SR); // `before`: grab clear of the move into the next word + let seed = 7; const rand = () => (seed = (seed * 16807) % 2147483647) / 2147483647; + const tail = new Float32Array(N + G); + for (let g = 0; g * HOP < N; g++) { + const src = base + Math.floor(rand() * Math.max(1, B - G)); + for (let i = 0; i < G; i++) tail[g * HOP + i] += (x[src + i] || 0) * (0.5 - 0.5 * Math.cos(2 * Math.PI * i / G)) * 0.5; + } + for (let i = 0; i < N && at + i < out.length; i++) { + const fade = Math.pow(1 - i / N, curve), inn = i < XIN ? i / XIN : 1; + out[at + i] += tail[i] * fade * inn; // the take past the cut is "Oh": never used + } + // the real vowel fades under the grains' entrance so there is no step at the cut + for (let i = 0; i < XIN && at - XIN + i >= 0; i++) out[at - XIN + i] *= 1 - 0.35 * (i / XIN); +} +for (const f of readdirSync(REG).filter((f) => f.endsWith(".wav"))) { + const sustained = /guitar|replay|hum|choir|jeffrey/.test(f); + const chans = readWav(resolve(REG, f)).map((x) => assemble(x, sustained ? 0.08 : 0.012, !sustained)); + writeWav(resolve(OUT, f), chans); + console.log(` ${sustained ? "80ms" : "12ms"} ${f}`); +} +console.log(`✓ ${OUT}`); diff --git a/pop/sailor-song/bin/sylla-collect.py b/pop/sailor-song/bin/sylla-collect.py new file mode 100644 index 0000000000..50403ee668 --- /dev/null +++ b/pop/sailor-song/bin/sylla-collect.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +"""sylla-collect.py — SyllaWizard's saved boxes → src/word-bounds.json (take seconds). + +Reads each line's saved syllables (src/sylla/sylls-NN.json: label, wi, fromMs, toMs, +ms into the clip) and adds the clip's offset back. A word's bounds are the span of +its syllables. Lines never opened keep the aligner's bounds. + pop/.venv/bin/python pop/sailor-song/bin/sylla-collect.py +""" +import json, os +LANE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SRC = os.path.join(LANE, "src"); SY = os.path.join(SRC, "sylla") +words = json.load(open(os.path.join(SRC, "words-aligned.json"))) +spec = json.load(open(os.path.join(SY, "spec.json"))) +out, lines = [], 0 +for t in spec["takes"]: + seed = json.load(open(os.path.join(SY, f"words-{t['id']}.json"))) + sp = os.path.join(SY, f"sylls-{t['id']}.json") + if not os.path.exists(sp): continue + lines += 1; off = seed["offsetSec"] + by = {} + for s in json.load(open(sp))["sylls"]: by.setdefault(s["wi"], []).append(s) + for wi, ss in by.items(): + if wi >= len(seed["wordIndex"]): continue + i = seed["wordIndex"][wi] + out.append({"i": i, "text": words[i]["text"], "from": round(off + min(s["fromMs"] for s in ss) / 1000, 3), "to": round(off + max(s["toMs"] for s in ss) / 1000, 3)}) +json.dump({"_": "hand word bounds, TAKE seconds, from SyllaWizard via bin/sylla-collect.py; applied last by bin/word-times.py", "words": sorted(out, key=lambda w: w["i"])}, + open(os.path.join(SRC, "word-bounds.json"), "w"), indent=1) +print(f"{len(out)} words from {lines} drawn lines → src/word-bounds.json") diff --git a/pop/sailor-song/bin/sylla-prep.py b/pop/sailor-song/bin/sylla-prep.py new file mode 100644 index 0000000000..b784115d26 --- /dev/null +++ b/pop/sailor-song/bin/sylla-prep.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +"""sylla-prep.py — sailor-song's words into SyllaWizard (sylla-wizard/, --spec mode). + +One take per sung lyric line: a clip of her natural lead stem around the line +(take time, 0.4 s either side), and a seed file with the line's current word +bounds (src/words-aligned.json, ms into the clip). SyllaWizard draws each word as +a box over the clip's spectrogram; drag its edges, then "Save boundaries". +bin/sylla-collect.py turns the saved boxes back into src/word-bounds.json (take +seconds), which bin/word-times.py applies last. + + pop/.venv/bin/python pop/sailor-song/bin/sylla-prep.py + swift run --package-path sylla-wizard SyllaWizard --spec pop/sailor-song/src/sylla/spec.json +A seed a take has already saved over (tool: SyllaWizard) is never overwritten. +"track" names the whole stem: SyllaWizard then shows one scrolling spectrogram of +the entire vocal with every line's boxes in place (pass --lines for one line per page). +""" +import json, os, subprocess +LANE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SRC = os.path.join(LANE, "src"); SY = os.path.join(SRC, "sylla"); os.makedirs(SY, exist_ok=True) +REPO = os.path.dirname(os.path.dirname(LANE)) +rel = lambda p: os.path.relpath(p, REPO) +words = json.load(open(os.path.join(SRC, "words-aligned.json"))) +lines = [l.strip() for l in open(os.path.join(SRC, "lyrics-sung.txt")) if l.strip() and not l.startswith("[")] +stem = os.path.join(SRC, "vox", "vocals-natural.wav") +takes, k = [], 0 +for n, line in enumerate(lines): + ws = list(range(k, k + len(line.split()))); k += len(ws) + ws = [i for i in ws if i < len(words)] + if not ws: continue + a = max(0.0, words[ws[0]]["fromMs"] / 1000 - 0.4); b = words[ws[-1]]["toMs"] / 1000 + 0.4 + tid = f"{n + 1:02d}" + clip = os.path.join(SY, f"line-{tid}.mp3"); fix = os.path.join(SY, f"words-{tid}.json") + subprocess.run(["ffmpeg", "-v", "error", "-y", "-ss", f"{a:.3f}", "-t", f"{b - a:.3f}", "-i", stem, "-ac", "1", "-c:a", "libmp3lame", "-q:a", "2", clip], check=True) + seed = {"take": tid, "offsetSec": round(a, 3), "wordIndex": ws, + "words": [{"text": words[i]["text"], "fromMs": words[i]["fromMs"] - round(a * 1000), "toMs": words[i]["toMs"] - round(a * 1000)} for i in ws]} + old = json.load(open(fix)) if os.path.exists(fix) else {} + if old.get("source", "").startswith("SyllaWizard"): + seed = {**old, "offsetSec": old.get("offsetSec", seed["offsetSec"]), "wordIndex": old.get("wordIndex", ws)} + json.dump(seed, open(fix, "w"), indent=1) + takes.append({"id": tid, "title": line, "date": f"{a // 60:.0f}:{a % 60:04.1f}", "audio": rel(clip), "fix": rel(fix), + "out": rel(os.path.join(SY, f"sylls-{tid}.json")), "drawn": rel(os.path.join(SY, f"drawn-{tid}.json"))}) +json.dump({"name": "sailor-song", "title": "sailor song lines", "track": rel(stem), "takes": takes}, open(os.path.join(SY, "spec.json"), "w"), indent=1) +print(f"{len(takes)} lines → {rel(os.path.join(SY, 'spec.json'))}") diff --git a/pop/sailor-song/bin/utterances.py b/pop/sailor-song/bin/utterances.py new file mode 100644 index 0000000000..0b74c6b91e --- /dev/null +++ b/pop/sailor-song/bin/utterances.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""utterances.py — every phrase she sings, as a clip. + +Her lead (the regularized aesthetivox stem — the record's own time base) is +gated on its energy: 10 ms RMS above the floor, a breath is a gap of ≥ GAP s, +a phrase must hold ≥ MIN s. Each phrase gets its words (src/words-aligned.json +mapped through the lock and regularize maps) and becomes an utterance: + + { id, start, end, dur, text, words: [{text, start, end}] } (reg seconds) + +That list is what the arrangement (bin/splice.mjs) cuts on, and what the +voice stems are clipped to — a phrase ends where HER voice ends, never where +a transcription guessed. Boundaries: start is walked back to the onset of +energy (≤ 60 ms), end is walked forward to where energy falls under the floor. + + pop/.venv/bin/python pop/sailor-song/bin/utterances.py [--gap 0.12] [--min 0.15] [--floor -46] + → src/utterances.json, and a printed table +""" +import argparse, json, os +import numpy as np, soundfile as sf + +ap = argparse.ArgumentParser() +ap.add_argument("--gap", type=float, default=0.12) +ap.add_argument("--min", type=float, default=0.15) +ap.add_argument("--floor", type=float, default=-46.0) # dBFS on the 10 ms RMS +ap.add_argument("--stems", default="reg") +a = ap.parse_args() +LANE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SRC = os.path.join(LANE, "src"); VOX = os.path.join(SRC, "vox") + +x, fs = sf.read(os.path.join(VOX, a.stems, "vocals-aesthetivox.wav")) +if x.ndim > 1: x = x.mean(1) +hop = fs // 100 +n = len(x) // hop +db = 20 * np.log10(np.sqrt((x[:n * hop].reshape(n, hop) ** 2).mean(1)) + 1e-9) +on = db > a.floor +# runs, merged across gaps shorter than --gap, dropped if shorter than --min +runs = []; i = 0 +while i < n: + if not on[i]: i += 1; continue + j = i + while j < n and on[j]: j += 1 + runs.append([i, j]); i = j +merged = [] +for r in runs: + if merged and (r[0] - merged[-1][1]) * 0.01 < a.gap: merged[-1][1] = r[1] + else: merged.append(r) +merged = [r for r in merged if (r[1] - r[0]) * 0.01 >= a.min] + +# words in reg time +def mapf(path): + m = np.array([[float(v) / 48000 for v in l.split()] for l in open(path).read().strip().split("\n")]) + return lambda t: float(np.interp(t, m[:, 0], m[:, 1])) +if a.stems == "reg": + lock = mapf(os.path.join(VOX, "locked/timemap.txt")); reg = mapf(os.path.join(VOX, "reg/timemap.txt")) + R = lambda t: reg(lock(t)) +else: + R = lambda t: t +W = [{"text": w["text"], "start": R(w["fromMs"] / 1000), "end": R(w["toMs"] / 1000)} for w in json.load(open(os.path.join(SRC, "words-aligned.json")))] + +utts = [] +for k, (i, j) in enumerate(merged): + s, e = i * 0.01, j * 0.01 + ws = [w for w in W if s - 0.08 <= (w["start"] + w["end"]) / 2 <= e + 0.08] + utts.append({"id": k, "start": round(s, 3), "end": round(e, 3), "dur": round(e - s, 3), "text": " ".join(w["text"] for w in ws), + "words": [{"text": w["text"], "start": round(w["start"], 3), "end": round(w["end"], 3)} for w in ws]}) +json.dump({"stems": a.stems, "floorDb": a.floor, "gapS": a.gap, "utterances": utts}, open(os.path.join(SRC, "utterances.json"), "w"), indent=1) +print(f"{len(utts)} utterances (floor {a.floor} dB, gap ≥ {a.gap} s)") +for u in utts: print(f" #{u['id']:2d} {u['start']:7.2f} → {u['end']:7.2f} ({u['dur']:4.2f}s) {u['text']}") diff --git a/pop/sailor-song/bin/word-times.py b/pop/sailor-song/bin/word-times.py new file mode 100644 index 0000000000..86658ef623 --- /dev/null +++ b/pop/sailor-song/bin/word-times.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python3 +"""word-times.py — sung-word timing as tight as the take allows. + + 1. whisper.cpp on the DRY vocal with per-token timestamps + whisper-cli -m ggml-small.bin -f dry16.wav -l en -ml 1 -sow -ojf + (one segment per word; every token inside carries its own offset, so a + word like "Hathaway" has three timed pieces — that is what drives the + partial highlight). + 2. The true lyric (src/lyrics-sung.txt) is aligned onto whisper's word + sequence by Needleman–Wunsch (pop/bin/mfa-align.mjs's move, re-done here + so the tokens ride along): substitutions keep the slot, insertions split + the neighbour's slot, deletions drop the slot. + 3. Every word onset is SNAPPED to her nearest vocal onset — librosa onset + detection on the dry stem, backtracked to the attack — within ±150 ms + (whisper's boundaries after a breath run ~250 ms early), monotonic, and + a word never shorter than 60 ms. Its end is the next word's onset or the + stem's energy fall, whichever comes first. + 4. Take time → record time through the lock and regularize maps minus the + record's startSec (read from the newest events receipt). + +Writes src/words-aligned.json (take time, with tokens) and src/words-record.json +(record time, with tokens) — both local (src/ is gitignored: the lyric is Gigi +Perez's). Prints a placement check: how many onsets sit within 40 ms of a +detected vocal onset, before and after. + + pop/.venv/bin/python pop/sailor-song/bin/word-times.py [--whisper-json path] [--snap-ms 150] +""" +import argparse, glob, json, os, re, subprocess, sys +import numpy as np, librosa, soundfile as sf + +ap = argparse.ArgumentParser() +ap.add_argument("--whisper-json", default=None) +ap.add_argument("--shape", action="store_true", help="re-cut words on syllable bumps (off by default: bounds-study.py scored it 30/273 within 40 ms of the ear vs 61 for raw FA)") +ap.add_argument("--breath-ms", type=float, default=250, help="quiet this long ends a word before the next one; shorter dips belong to the word (the ear tiles words edge to edge)") +ap.add_argument("--fa", action="store_true", help="MMS forced alignment (bin/forced-align.py → .word-times/forced-align.json): the known lyric placed on her audio") +ap.add_argument("--openai", action="store_true", help="OpenAI whisper-1 word timestamps, primed with the lyric (loner's align.py move)") +ap.add_argument("--snap-ms", type=float, default=40, help="snap an onset to a detected attack this close (40 scored best against the hand bounds; 150 pulled onsets onto the wrong attack)") +ap.add_argument("--model", default=os.path.expanduser("~/.whisper-models/ggml-small.bin")) +a = ap.parse_args() +LANE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SRC = os.path.join(LANE, "src"); VOX = os.path.join(SRC, "vox") +DRY = os.path.join(VOX, "vocals-dry-48k.wav") +TMP = os.path.join(SRC, ".word-times"); os.makedirs(TMP, exist_ok=True) + +# ── 1. whisper with token times ────────────────────────────────────────── +wj = a.whisper_json +if not wj and not a.openai and not a.fa: + wav16 = os.path.join(TMP, "dry16.wav") + subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", DRY, "-ar", "16000", "-ac", "1", wav16], check=True) + # no -sns: with non-speech suppression the model drops her first line; the bracket tags are filtered below + subprocess.run(["whisper-cli", "-m", a.model, "-f", wav16, "-l", "en", "-ml", "1", "-sow", "-ojf", "-of", os.path.join(TMP, "dry16")], + check=True, capture_output=True) + wj = os.path.join(TMP, "dry16.json") +def openai_words(): + """whisper-1 on the dry stem with word timestamps, the true lyric as the prompt. + whisper.cpp -ml 1 returns SUB-WORD tokens (loner: "curled" → "cur" + "led") and every + label after a split slides by a syllable; whisper-1 returns whole words.""" + key = os.environ.get("OPENAI_API_KEY") + env = os.path.join(LANE, "..", "..", "aesthetic-computer-vault", ".devcontainer", "envs", "devcontainer.env") + if not key and os.path.exists(env): + for line in open(env): + if line.startswith("OPENAI_API_KEY="): key = line.split("=", 1)[1].strip().strip('"') + if not key: sys.exit("no OPENAI_API_KEY (env or vault)") + cache = os.path.join(TMP, "openai-whisper1.json") + if not os.path.exists(cache): + mp3 = os.path.join(TMP, "dry16.mp3") + subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", DRY, "-ar", "16000", "-ac", "1", "-b:a", "64k", mp3], check=True) + lyric = " ".join(l.strip() for l in open(os.path.join(SRC, "lyrics-sung.txt")) if l.strip() and not l.startswith("[")) + r = subprocess.run(["curl", "-s", "https://api.openai.com/v1/audio/transcriptions", "-H", f"Authorization: Bearer {key}", + "-F", f"file=@{mp3}", "-F", "model=whisper-1", "-F", "language=en", "-F", "response_format=verbose_json", + "-F", f"prompt={lyric[:800]}", "-F", "timestamp_granularities[]=word"], capture_output=True, text=True, check=True) + d = json.loads(r.stdout) + if "words" not in d: sys.exit(f"whisper-1 failed: {r.stdout[:300]}") + json.dump(d, open(cache, "w"), indent=1) + d = json.load(open(cache)) + return [{"text": w["word"], "from": w["start"], "to": max(w["end"], w["start"] + 0.06), + "tokens": [{"text": w["word"], "from": w["start"], "to": max(w["end"], w["start"] + 0.06)}]} for w in d["words"]] + +hyp = [] +W = [] if (a.openai or a.fa) else json.load(open(wj))["transcription"] +if a.openai: hyp = openai_words() +if a.fa: hyp = [{k: w[k] for k in ("text", "from", "to", "tokens")} for w in json.load(open(os.path.join(TMP, "forced-align.json")))] +for seg in W: + txt = seg["text"].strip() + if not txt or txt.startswith("[") or txt.startswith("(") or txt in ("]", ")") or txt.lower() in ("silence", "music"): continue + if seg["offsets"]["to"] - seg["offsets"]["from"] > 3000: continue # no sung word lasts three seconds + toks = [t for t in seg["tokens"] if not t["text"].startswith("[") and t["text"].strip()] + if not toks: continue + hyp.append({"text": txt, "from": seg["offsets"]["from"] / 1000, "to": seg["offsets"]["to"] / 1000, + "tokens": [{"text": t["text"].strip(), "from": t["offsets"]["from"] / 1000, "to": t["offsets"]["to"] / 1000} for t in toks]}) +# drop whisper words that sit in silence on the dry stem (hallucinations over the guitar-only bars) +_y, _sr = librosa.load(DRY, sr=22050, mono=True) +_rms = librosa.feature.rms(y=_y, frame_length=1024, hop_length=256)[0]; _rt = librosa.frames_to_time(np.arange(len(_rms)), sr=_sr, hop_length=256) +_floor = np.percentile(_rms[_rms > 0], 20) +def _loud(w): a0, a1 = np.searchsorted(_rt, w["from"]), np.searchsorted(_rt, w["to"] + 0.05); return len(_rms[a0:a1]) and _rms[a0:a1].max() > _floor * 2.5 +nraw = len(hyp); hyp = [w for w in hyp if _loud(w)] +print(f"whisper: {nraw} words, {len(hyp)} on voiced audio") + +# ── 2. align the true lyric onto whisper's words ───────────────────────── +lines = [l.strip() for l in open(os.path.join(SRC, "lyrics-sung.txt")) if l.strip() and not l.startswith("[")] +ref = [w for l in lines for w in l.split()] +norm = lambda s: re.sub(r"[^a-z0-9']", "", s.lower()) +def sim(x, y): + x, y = norm(x), norm(y) + if x == y: return 2.0 + if not x or not y: return -1.0 + # crude phonetic tolerance: shared prefix / edit distance + d = librosa.sequence.dtw if False else None + la, lb = len(x), len(y); dp = list(range(lb + 1)) + for i in range(1, la + 1): + prev, dp[0] = dp[0], i + for j in range(1, lb + 1): + cur = dp[j]; dp[j] = min(dp[j] + 1, dp[j - 1] + 1, prev + (x[i - 1] != y[j - 1])); prev = cur + return 1.5 - 2.5 * dp[lb] / max(la, lb) +n, m = len(ref), len(hyp) +S = np.zeros((n + 1, m + 1)); S[:, 0] = -np.arange(n + 1) * 0.8; S[0, :] = -np.arange(m + 1) * 0.8 +for i in range(1, n + 1): + for j in range(1, m + 1): + S[i, j] = max(S[i - 1, j - 1] + sim(ref[i - 1], hyp[j - 1]["text"]), S[i - 1, j] - 0.8, S[i, j - 1] - 0.8) +i, j, pairs = n, m, [] +while i > 0 or j > 0: + if i > 0 and j > 0 and abs(S[i, j] - (S[i - 1, j - 1] + sim(ref[i - 1], hyp[j - 1]["text"]))) < 1e-9: pairs.append((i - 1, j - 1)); i -= 1; j -= 1 + elif i > 0 and abs(S[i, j] - (S[i - 1, j] - 0.8)) < 1e-9: pairs.append((i - 1, None)); i -= 1 + else: j -= 1 +pairs.reverse() +words = [] +exact = sum(1 for ri, hj in pairs if hj is not None and norm(ref[ri]) == norm(hyp[hj]["text"])) +for ri, hj in pairs: + if hj is None: words.append({"text": ref[ri], "from": None, "to": None, "tokens": []}) + else: h = hyp[hj]; words.append({"text": ref[ri], "from": h["from"], "to": h["to"], "tokens": h["tokens"]}) +# insertions: split the neighbour's slot evenly +k = 0 +while k < len(words): + if words[k]["from"] is None: + j = k + while j < len(words) and words[j]["from"] is None: j += 1 + prev = words[k - 1] if k > 0 else None; nxt = words[j] if j < len(words) else None + a0 = prev["from"] if prev else (nxt["from"] - 0.3); a1 = nxt["from"] if nxt else (prev["to"] + 0.3 * (j - k)) + cnt = (j - k) + (1 if prev else 0); step = (a1 - a0) / cnt + if prev: prev["to"] = a0 + step + for q in range(k, j): words[q]["from"] = a0 + step * (q - k + (1 if prev else 0)); words[q]["to"] = words[q]["from"] + step; words[q]["tokens"] = [] + k = j + else: k += 1 +print(f"aligned: {len(words)} lyric words · {exact} exact · {sum(1 for w in words if not w['tokens'])} interpolated") + +# ── 3. snap onsets to her vocal onsets ─────────────────────────────────── +y, sr = librosa.load(DRY, sr=22050, mono=True) +env = librosa.onset.onset_strength(y=y, sr=sr, hop_length=256) +on = librosa.onset.onset_detect(onset_envelope=env, sr=sr, hop_length=256, backtrack=True, units="time", delta=0.05) +rms = librosa.feature.rms(y=y, frame_length=1024, hop_length=256)[0]; rt = librosa.frames_to_time(np.arange(len(rms)), sr=sr, hop_length=256) +floor = np.percentile(rms[rms > 0], 20) +def energy_at(t): return float(np.interp(t, rt, rms)) +def near_onset(t): + d = np.abs(on - t); k = int(np.argmin(d)); return float(on[k]), float(d[k]) +before = sum(1 for w in words if near_onset(w["from"])[1] <= 0.04) +snap = a.snap_ms / 1000 +# inside a continuous phrase there is no energy onset to snap to: those words snap to her +# tuned-note onsets instead (vox-notes.json, take time) — a word change is a pitch change +NOTE_T = np.array(sorted(n["t"] for n in json.load(open(os.path.join(LANE, "vox-notes.json")))["notes"])) +def near_note(t): d = np.abs(NOTE_T - t); k = int(np.argmin(d)); return float(NOTE_T[k]), float(d[k]) +n_energy = n_note = 0 +for i, w in enumerate(words): + o, d = near_onset(w["from"]) + if d <= 0.04: n_energy += 1 + elif d <= snap and energy_at(w["from"] - 0.06) < floor * 1.5: n_energy += 1 # a real onset out of a gap + else: + o2, d2 = near_note(w["from"]) + if d2 <= 0.12: o, d = o2, d2; n_note += 1 + if d <= snap: + shift = o - w["from"]; w["from"] = o + for tk in w["tokens"]: tk["from"] += shift; tk["to"] += shift +print(f"snapped: {n_energy} words to a vocal onset, {n_note} to a tuned-note onset") +# hand edits (src/word-edits.json): the ear wins over the tracker — a word's onset pinned to a take time +edits_path = os.path.join(SRC, "word-edits.json") +if os.path.exists(edits_path): + for e in json.load(open(edits_path))["edits"]: + cands = [w for w in words if w["text"] == e["text"] and abs(w["from"] - e["near"]) < 0.4] + if not cands: print(f" ! edit not matched: {e}"); continue + w = min(cands, key=lambda w: abs(w["from"] - e["near"])); shift = e["from"] - w["from"]; w["from"] = e["from"] + for tk in w["tokens"]: tk["from"] += shift; tk["to"] += shift + print(f" edit: {e['text']!r} → {e['from']:.3f} ({e.get('why', '')})") +# monotonic + minimum length + ends +for i, w in enumerate(words): + if i > 0 and w["from"] < words[i - 1]["from"] + 0.06: w["from"] = words[i - 1]["from"] + 0.06 +for i, w in enumerate(words): + nxt = words[i + 1]["from"] if i + 1 < len(words) else w["from"] + 1.0 + # v19: EVERY word runs on while her voice does — aligners end sung vowels early (the blocks + # stopped short of the waveform). From its aligned end, it continues until the stem has sat + # under the floor for a breath (--breath-ms), or until the next word starts, whichever is + # first. bounds-study.py on the hand pass: 267 of 268 word pairs butt together (gap ≤ 15 ms); + # the one real gap is > 350 ms. A dip shorter than a breath is inside the word. + breath_s = a.breath_ms / 1000 + t = max(w["to"] or w["from"], w["from"] + 0.08); quiet = 0.0 + while t < nxt - 0.01 and quiet < breath_s: + quiet = quiet + 0.01 if energy_at(t) < floor * 1.2 else 0.0; t += 0.01 + end = min(t - quiet, nxt - 0.01) + w["to"] = max(w["from"] + 0.06, end) + if w["tokens"]: + for tk in w["tokens"]: tk["from"] = min(max(tk["from"], w["from"]), w["to"]); tk["to"] = min(max(tk["to"], tk["from"]), w["to"]) + w["tokens"][-1]["to"] = w["to"] +# ── 3b. re-bound by SHAPE: syllable bumps in her energy, dealt out to the words ─────── +# v19: the aligner's edges sit inside a word's sound, so a fast short word ("to", "the", +# "you'd") could swallow its neighbour's bump. Here every phrase (words closer than 0.3 s) +# is re-cut on her own envelope: find the syllable bumps (peaks of the 10 ms RMS with at +# least 2.5 dB of dip on each side), estimate each word's syllables from its spelling, and +# deal the bumps out in order by DP (cost: onset distance to the aligned time + syllable +# mismatch). A word then runs from the dip before its first bump to the dip after its last. +if a.shape: + hop10 = int(sr * 0.01) + e10 = librosa.feature.rms(y=y, frame_length=hop10 * 3, hop_length=hop10)[0] + e10 = np.convolve(20 * np.log10(e10 + 1e-6), np.ones(3) / 3, "same"); t10 = np.arange(len(e10)) * 0.01 + def syllables(word): + w = re.sub(r"[^a-z]", "", word.lower()) + if not w: return 1 + n = len(re.findall(r"[aeiouy]+", w)) + if w.endswith("e") and not w.endswith(("le", "ee", "ye")) and n > 1: n -= 1 + return max(1, n) + def bumps_in(t0, t1): + i0, i1 = max(1, int(t0 / 0.01)), min(len(e10) - 1, int(t1 / 0.01)) + seg = e10[i0:i1] + if len(seg) < 5: return [] + loud = seg.max() - 24 + pk = [k for k in range(1, len(seg) - 1) if seg[k] >= seg[k - 1] and seg[k] > seg[k + 1] and seg[k] > loud] + out = [] # keep peaks with 2.5 dB of dip between them + for k in pk: + if out and (k - out[-1] < 7 or seg[out[-1]:k + 1].min() > min(seg[out[-1]], seg[k]) - 2.5): + if seg[k] > seg[out[-1]]: out[-1] = k + continue + out.append(k) + return [(i0 + k) * 0.01 for k in out] + def dip(ta, tb): + i0, i1 = int(ta / 0.01), max(int(ta / 0.01) + 1, int(tb / 0.01)) + return (i0 + int(np.argmin(e10[i0:i1]))) * 0.01 + # one alignment for the whole song: every syllable bump in her voice against every lyric + # word, in order. A word takes 1..6 consecutive bumps with no breath (> 0.35 s of quiet) + # inside; a bump that belongs to no word (an ad-lib, a hum, bleed) may be skipped at a + # cost. The aligned time only NUDGES (capped at 1 s), so a line the aligner packed into + # silence cannot drag its words there. + voiced = e10 > np.percentile(e10, 35) + B = [t for t in bumps_in(0.05, t10[-1] - 0.05) if voiced[int(t / 0.01)]] + def breath(k, j): # quiet gap between bumps k and j-1 (inclusive)? + return any(dip_gap(B[q], B[q + 1]) for q in range(k, j - 1)) + def dip_gap(ta, tb): + seg = voiced[int(ta / 0.01):int(tb / 0.01)]; run = best = 0 + for v in seg: run = 0 if v else run + 1; best = max(best, run) + return best * 0.01 > 0.35 + n, m, KMAX, SKIP = len(words), len(B), 6, 0.9 + syl = [syllables(w["text"]) for w in words] + INF = 1e18; D = np.full((n + 1, m + 1), INF); P = np.full((n + 1, m + 1), -1, int); D[0, :] = np.arange(m + 1) * SKIP + gaps = np.array([dip_gap(B[q], B[q + 1]) for q in range(m - 1)] + [False]) + for i in range(1, n + 1): + w = words[i - 1] + for j in range(1, m + 1): + best, arg = D[i, j - 1] + SKIP, -2 # skip bump j-1 + for k in range(max(0, j - KMAX), j): # word i takes bumps k .. j-1 + if D[i - 1, k] >= INF or gaps[k:j - 1].any(): continue + c = D[i - 1, k] + min(1.0, abs(B[k] - w["from"])) * 2.0 + abs((j - k) - syl[i - 1]) * 0.7 + if c < best: best, arg = c, k + D[i, j], P[i, j] = best, arg + take, i, j = [None] * n, n, int(np.argmin(D[n])) + while i > 0 and j > 0: + k = P[i, j] + if k == -2: j -= 1; continue + take[i - 1] = (k, j); i -= 1; j = k + moved = 0 + for q, w in enumerate(words): + if take[q] is None: continue + k, j = take[q] + start = dip(B[k - 1], B[k]) if k > 0 and not gaps[k - 1] else B[k] - 0.05 + end = dip(B[j - 1], B[j]) if j < m and not gaps[j - 1] else B[j - 1] + 0.12 + while end < t10[-1] - 0.02 and voiced[int(end / 0.01)] and (j >= m or end < B[j] - 0.03): end += 0.01 # ride the tail out + if abs(start - w["from"]) > 0.02: moved += 1 + w["from"], w["to"] = round(start, 3), round(max(end, start + 0.06), 3) + for tk in w["tokens"]: tk["from"], tk["to"] = w["from"], w["to"] + for q in range(1, n): # neighbours never overlap + if words[q]["from"] < words[q - 1]["to"]: words[q - 1]["to"] = words[q]["from"] + print(f"shape: {m} syllable bumps dealt to {n} words · {sum(t is None for t in take)} unplaced · {moved} starts moved") + +# hand pins win over the shape pass too (it re-deals every word) +if os.path.exists(EDITS := os.path.join(SRC, "word-edits.json")): + for e in json.load(open(EDITS))["edits"]: + c = [q for q, w in enumerate(words) if w["text"] == e["text"] and abs(w["from"] - e["near"]) < 1.5] + if not c: continue + q = min(c, key=lambda q: abs(words[q]["from"] - e["near"])); w = words[q] + w["from"] = e["from"]; w["to"] = max(w["to"], e["from"] + 0.06) + if q: words[q - 1]["to"] = min(words[q - 1]["to"], e["from"]) + for tk in w["tokens"]: tk["from"], tk["to"] = w["from"], w["to"] + print(f" pin after shape: {e['text']!r} → {e['from']:.3f}") +# bounds dragged by hand in SyllaWizard (bin/sylla-collect.py → word-bounds.json) win over everything +if os.path.exists(BND := os.path.join(SRC, "word-bounds.json")): + hand = json.load(open(BND))["words"] + for b in hand: + if b["i"] < len(words) and words[b["i"]]["text"] == b["text"]: + w = words[b["i"]]; w["from"], w["to"] = b["from"], b["to"] + for tk in w["tokens"]: tk["from"], tk["to"] = w["from"], w["to"] + print(f" hand bounds: {len(hand)} words from SyllaWizard") +after = sum(1 for w in words if near_onset(w["from"])[1] <= 0.04) +print(f"onsets within 40 ms of a vocal onset: {before}/{len(words)} → {after}/{len(words)}") + +# ── 4. take → record ───────────────────────────────────────────────────── +def mapf(path): + mm = np.array([[float(v) / 48000 for v in l.split()] for l in open(path).read().strip().split("\n")]) + return lambda t: float(np.interp(t, mm[:, 0], mm[:, 1])) +RAW = os.environ.get("SAILOR_CLOCK") == "raw" +lock = (lambda t: t) if RAW else mapf(os.path.join(VOX, "locked/timemap.txt")); reg = (lambda t: t) if RAW else mapf(os.path.join(VOX, "reg/timemap.txt")) +receipts = sorted(glob.glob(os.path.join(LANE, "out/sailor-song-v*.events.json")), key=os.path.getmtime) +start = json.load(open(receipts[-1]))["startSec"] if receipts else 0.0 +segmap = os.path.join(VOX, "cut/segmap.txt") +segs = [tuple(map(float, l.split())) for l in open(segmap).read().strip().split("\n")] if os.path.exists(segmap) and not RAW else [] +def cut_of(t): + if not segs: return t + for a0, b0, o in segs: + if a0 <= t < b0: return t - a0 + o + return None +def rec(t): + c = cut_of(reg(lock(t))); return None if c is None else c - start +def rec_end(t0, t1): + """a word's end, kept inside the segment its start is in (a word can straddle a seam)""" + r0 = reg(lock(t0)); r1 = reg(lock(t1)) + for a0, b0, o in segs: + if a0 <= r0 < b0: return min(r1, b0 - 0.01) - a0 + o - start if r1 >= a0 else r0 - a0 + o - start + 0.06 + return None if not segs else None if cut_of(r1) is None else cut_of(r1) - start +out_take = [{"text": w["text"], "fromMs": round(w["from"] * 1000), "toMs": round(w["to"] * 1000), + "tokens": [{"text": t["text"], "fromMs": round(t["from"] * 1000), "toMs": round(t["to"] * 1000)} for t in w["tokens"]]} for w in words] +out_rec = [{"text": w["text"], "fromMs": round(rec(w["from"]) * 1000), "toMs": round((rec_end(w["from"], w["to"]) if segs else (rec(w["to"]) if rec(w["to"]) is not None else rec(w["from"]) + 0.06)) * 1000), + "tokens": [{"text": t["text"], "fromMs": round(rec(t["from"]) * 1000), "toMs": round((rec(t["to"]) if rec(t["to"]) is not None else rec(t["from"])) * 1000)} + for t in w["tokens"] if rec(t["from"]) is not None]} for w in words if rec(w["from"]) is not None] +# (kept in lyric order: the video tools slice words per lyric line, then sort the lines by time) +if segs: print(f"arrangement: {len(segs)} segments, {len(words) - len(out_rec)} words outside them dropped") +json.dump(out_take, open(os.path.join(SRC, "words-aligned.json"), "w"), indent=1) +json.dump(out_rec, open(os.path.join(SRC, "words-record.json"), "w"), indent=1) +print(f"✓ {len(words)} words · record start {start:.3f}s (from {os.path.basename(receipts[-1]) if receipts else '-'}) · first word {out_rec[0]['fromMs']/1000:.2f}s · last ends {out_rec[-1]['toMs']/1000:.2f}s") diff --git a/pop/sailor-song/c/build.sh b/pop/sailor-song/c/build.sh new file mode 100755 index 0000000000..78dc717575 --- /dev/null +++ b/pop/sailor-song/c/build.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# build.sh — compile the sailor-song C engine (regenerates the chart first). +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +node "$HERE/../bin/chart.mjs" +cc -O2 -Wall -Wextra -o "$HERE/sailorremix" "$HERE/sailorremix.c" -lm +echo "✓ $HERE/sailorremix" diff --git a/pop/sailor-song/c/cut.sh b/pop/sailor-song/c/cut.sh new file mode 100755 index 0000000000..64ae7eff5b --- /dev/null +++ b/pop/sailor-song/c/cut.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# cut.sh — master "sailor song (outside remix)". Modeled on wannadash's +# pop/cult/bin/cut-release.sh: a translation premaster (32 Hz high-pass, +# phone-readable mids, a touch of top — no 2–5 kHz push, her belts are bright +# already), then the density stage (1.5:1 RMS glue at half mix, tanh soft-clip +# on the transients, ONE measured static gain, 4× oversampled true-peak +# limiter at 0.760). House target (pop/MASTERING.md): −10 ±1 LUFS, ≤ −2 dBTP. +# +# Outputs: out/sailor-song-v5-master.wav (24/48), .flac, .mp3 (320k, cover +# embedded — the file we send her). +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LANE="$(dirname "$HERE")" +OUT="$LANE/out" +V="${VERSION:-v19}" +FULL="${FULL:-$OUT/sailor-song-$V-full.wav}" +PRE="$OUT/.$V-premaster.wav" +MASTER="$OUT/sailor-song-$V-master.wav" +COVER="$LANE/cover/sailor-song-cover.jpg" # v6.2: clean, no title text +trap 'rm -f "$PRE"' EXIT + +echo "→ translation premaster" +ffmpeg -y -v error -i "$FULL" -af \ + "highpass=f=32,bass=g=-2.5:f=90:w=0.7,equalizer=f=220:t=q:w=0.9:g=0.6,\ +equalizer=f=900:t=q:w=0.85:g=1.6,treble=g=1.2:f=7500:w=0.6" -ar 48000 -c:a pcm_f32le "$PRE" + +density() { # $1 = static gain dB + echo "acompressor=threshold=0.20:ratio=1.5:attack=30:release=180:knee=4:link=maximum:detection=rms:mix=0.50,\ +volume=4.0dB,asoftclip=type=tanh:threshold=0.62:output=0.92,volume=${1}dB,\ +aresample=192000,alimiter=limit=${LIMIT:-0.730}:attack=4:release=120:asc=true:asc_level=0.35:level=false,aresample=48000" +} +measure() { ffmpeg -hide_banner -nostats -i "$1" -af "$2${2:+,}ebur128=peak=true:framelog=quiet" -f null - 2>&1 \ + | awk '/Summary/{s=1} s&&/ I:/{print $2; exit}'; } + +echo "→ density + loudness taken at the ceiling (measured, not guessed)" +GAIN2="${GAIN2:-6.0}" +for pass in 1 2 3; do + I=$(measure "$PRE" "$(density "$GAIN2")") + echo " pass $pass: GAIN2=${GAIN2} dB → ${I} LUFS" + awk -v i="$I" 'BEGIN{exit !(i > -10.3 && i < -9.7)}' && break + GAIN2=$(awk -v g="$GAIN2" -v i="$I" 'BEGIN{printf "%.2f", g + (-10 - i)}') +done +ffmpeg -y -v error -i "$PRE" -af "$(density "$GAIN2")" -ar 48000 -c:a pcm_s24le "$MASTER" + +echo "→ deliverables" +META=(-metadata title="sailor s" -metadata artist="s@ge" -metadata album_artist="s@ge" -metadata comment="cover of Gigi Perez — Sailor Song") +ffmpeg -y -v error -i "$MASTER" -c:a flac -compression_level 8 -sample_fmt s32 "${META[@]}" "$OUT/sailor-song-$V.flac" +if [ -f "$COVER" ]; then + ffmpeg -y -v error -i "$MASTER" -i "$COVER" -map 0:a -map 1:v -c:v mjpeg -disposition:v attached_pic \ + -c:a libmp3lame -b:a 320k -id3v2_version 3 "${META[@]}" "$OUT/sailor-song-$V.mp3" +else + ffmpeg -y -v error -i "$MASTER" -c:a libmp3lame -b:a 320k "${META[@]}" "$OUT/sailor-song-$V.mp3" +fi + +echo "→ verify" +ffmpeg -hide_banner -nostats -i "$MASTER" -af ebur128=peak=true:framelog=quiet -f null - 2>&1 | grep -E "^\s+(I|LRA|Peak):" +echo "✓ $OUT/sailor-song-$V.mp3" diff --git a/pop/sailor-song/c/sailor-chart.h b/pop/sailor-song/c/sailor-chart.h new file mode 100644 index 0000000000..bacb448ac7 --- /dev/null +++ b/pop/sailor-song/c/sailor-chart.h @@ -0,0 +1,390 @@ +// sailor-chart.h — GENERATED by bin/chart.mjs; do not edit. +// Her clock and her melody. Bars are anchored on her strum motif +// (bin/measures.py); beats are snapped to her strums. Notes are her tuned +// line (bin/aesthetivox.py), onsets moved through the lock-vox map so they +// sit where the locked stems sing them. Pitches are MIDI in the guitar's +// frame; the engine adds CHART_TUNE semitones (+13¢ over A440). +#pragma once + +#define CHART_TUNE 0.130 +#define CHORD_GSM 0 +#define CHORD_EMAJ7 1 +#define CHORD_B 2 + +typedef struct { int n; double t, dur; int chord, nb; double beats[7]; } ChartBar; +typedef struct { double t, dur; int midi; } ChartNote; + +#define CHART_NBARS 88 +static const ChartBar CHART_BARS[CHART_NBARS] = { + { 1, 5.0200, 1.9200, 0, 4, { 5.0200, 5.4940, 5.9680, 6.4540, 6.9400 } }, + { 2, 6.9400, 1.9460, 0, 4, { 6.9400, 7.4270, 7.9130, 8.4000, 8.8860 } }, + { 3, 8.8860, 1.9460, 0, 4, { 8.8860, 9.3730, 9.8590, 10.3460, 10.8320 } }, + { 4, 10.8320, 1.9160, 2, 4, { 10.8320, 11.3020, 11.7730, 12.3070, 12.7480 } }, + { 5, 12.7480, 1.9150, 2, 4, { 12.7480, 13.2300, 13.7110, 14.1870, 14.6630 } }, + { 6, 14.6630, 1.9620, 0, 4, { 14.6630, 15.1450, 15.6270, 16.1730, 16.6250 } }, + { 7, 16.6250, 1.8810, 0, 4, { 16.6250, 17.1070, 17.5890, 18.0480, 18.5060 } }, + { 8, 18.5060, 1.9280, 2, 4, { 18.5060, 19.0060, 19.5050, 19.9690, 20.4340 } }, + { 9, 20.4340, 1.8920, 2, 4, { 20.4340, 20.9100, 21.3860, 21.8560, 22.3260 } }, + { 10, 22.3260, 1.9850, 0, 4, { 22.3260, 22.8830, 23.3360, 23.8590, 24.3110 } }, + { 11, 24.3110, 2.0780, 2, 4, { 24.3110, 24.8920, 25.3680, 25.9020, 26.3890 } }, + { 12, 26.3890, 2.4250, 2, 5, { 26.3890, 26.8770, 27.3650, 27.8480, 28.3310, 28.8140 } }, + { 13, 28.8140, 2.0220, 2, 4, { 28.8140, 29.2970, 29.7800, 30.3080, 30.8360 } }, + { 14, 30.8360, 2.1480, 0, 4, { 30.8360, 31.3880, 31.9390, 32.4610, 32.9840 } }, + { 15, 32.9840, 2.1130, 0, 4, { 32.9840, 33.5010, 34.0170, 34.5570, 35.0970 } }, + { 16, 35.0970, 2.1710, 2, 4, { 35.0970, 35.6430, 36.1880, 36.7280, 37.2680 } }, + { 17, 37.2680, 2.1590, 2, 4, { 37.2680, 37.8080, 38.3480, 38.8820, 39.4270 } }, + { 18, 39.4270, 2.1480, 1, 4, { 39.4270, 39.9620, 40.4960, 41.0350, 41.5750 } }, + { 19, 41.5750, 2.1020, 2, 4, { 41.5750, 42.1040, 42.6320, 43.1430, 43.6770 } }, + { 20, 43.6770, 2.1590, 0, 4, { 43.6770, 44.2170, 44.7560, 45.2960, 45.8360 } }, + { 21, 45.8360, 2.1110, 0, 4, { 45.8360, 46.3630, 46.8910, 47.4180, 47.9470 } }, + { 22, 47.9470, 2.0780, 2, 4, { 47.9470, 48.4680, 48.9880, 49.5070, 50.0250 } }, + { 23, 50.0250, 2.0470, 2, 4, { 50.0250, 50.5370, 51.0480, 51.5600, 52.0720 } }, + { 24, 52.0720, 2.0350, 2, 4, { 52.0720, 52.5810, 53.0900, 53.5990, 54.1080 } }, + { 25, 54.1080, 2.0350, 2, 4, { 54.1080, 54.6170, 55.1260, 55.6350, 56.1430 } }, + { 26, 56.1430, 2.0290, 2, 4, { 56.1430, 56.6500, 57.1570, 57.6640, 58.1720 } }, + { 27, 58.1720, 1.0100, 2, 2, { 58.1720, 58.6790, 59.1820 } }, + { 26, 59.1820, 0.9590, 2, 2, { 59.1820, 59.6620, 60.1410 } }, + { 43, 60.1410, 0.4790, 0, 1, { 60.1410, 60.6200 } }, + { 44, 60.6200, 1.9180, 0, 4, { 60.6200, 61.0990, 61.5780, 62.0580, 62.5380 } }, + { 45, 62.5380, 1.9270, 0, 4, { 62.5380, 63.0190, 63.5010, 63.9830, 64.4650 } }, + { 46, 64.4650, 1.9280, 2, 4, { 64.4650, 64.9480, 65.4300, 65.9120, 66.3940 } }, + { 47, 66.3940, 1.9290, 2, 4, { 66.3940, 66.8760, 67.3580, 67.8400, 68.3230 } }, + { 48, 68.3230, 1.9280, 0, 4, { 68.3230, 68.8050, 69.2870, 69.7690, 70.2510 } }, + { 49, 70.2510, 1.9240, 1, 5, { 70.2510, 70.7320, 71.2130, 71.6940, 72.1740, 72.1750 } }, + { 50, 72.1740, 1.9210, 0, 4, { 72.1740, 72.6550, 73.1360, 73.6160, 74.0950 } }, + { 51, 74.0950, 0.8410, 0, 2, { 74.0950, 74.5740, 74.9360 } }, + { 27, 74.9360, 2.5280, 2, 5, { 74.9360, 75.4430, 75.9490, 76.4550, 76.9600, 77.4640 } }, + { 28, 77.4640, 1.9150, 1, 4, { 77.4640, 77.9430, 78.4220, 78.9010, 79.3790 } }, + { 29, 79.3790, 1.9100, 1, 4, { 79.3790, 79.8570, 80.3350, 80.8120, 81.2900 } }, + { 30, 81.2900, 1.9070, 0, 4, { 81.2900, 81.7670, 82.2440, 82.7210, 83.1970 } }, + { 31, 83.1970, 1.9030, 0, 4, { 83.1970, 83.6730, 84.1490, 84.6240, 85.1000 } }, + { 32, 85.1000, 1.9050, 2, 4, { 85.1000, 85.5770, 86.0530, 86.5290, 87.0050 } }, + { 33, 87.0050, 1.9040, 2, 4, { 87.0050, 87.4810, 87.9570, 88.4330, 88.9090 } }, + { 34, 88.9090, 1.9020, 2, 4, { 88.9090, 89.3840, 89.8600, 90.3350, 90.8110 } }, + { 35, 90.8110, 1.9030, 0, 4, { 90.8110, 91.2870, 91.7630, 92.2390, 92.7140 } }, + { 36, 92.7140, 1.9030, 0, 4, { 92.7140, 93.1900, 93.6660, 94.1420, 94.6170 } }, + { 37, 94.6170, 1.9030, 0, 4, { 94.6170, 95.0930, 95.5690, 96.0450, 96.5210 } }, + { 38, 96.5210, 1.9040, 2, 5, { 96.5210, 96.9960, 97.4720, 97.9480, 98.4240, 98.4250 } }, + { 39, 98.4240, 1.9040, 2, 4, { 98.4240, 98.9000, 99.3760, 99.8520, 100.3280 } }, + { 40, 100.3280, 1.9040, 2, 4, { 100.3280, 100.8040, 101.2800, 101.7560, 102.2320 } }, + { 41, 102.2320, 1.9020, 1, 4, { 102.2320, 102.7080, 103.1840, 103.6590, 104.1340 } }, + { 42, 104.1340, 1.9070, 2, 4, { 104.1340, 104.6100, 105.0860, 105.5640, 106.0410 } }, + { 43, 106.0410, 1.5970, 0, 4, { 106.0410, 106.5190, 106.9970, 107.4750, 107.6380 } }, + { 51, 107.6380, 1.0710, 0, 3, { 107.6380, 107.7550, 108.2320, 108.7100 } }, + { 52, 108.7100, 1.9020, 2, 4, { 108.7100, 109.1860, 109.6610, 110.1370, 110.6120 } }, + { 53, 110.6120, 1.8980, 2, 4, { 110.6120, 111.0860, 111.5610, 112.0360, 112.5100 } }, + { 54, 112.5100, 1.8910, 2, 4, { 112.5100, 112.9840, 113.4560, 113.9280, 114.4010 } }, + { 55, 114.4010, 1.8920, 0, 4, { 114.4010, 114.8740, 115.3480, 115.8200, 116.2930 } }, + { 56, 116.2930, 1.8900, 0, 4, { 116.2930, 116.7660, 117.2380, 117.7110, 118.1830 } }, + { 57, 118.1830, 1.8880, 0, 4, { 118.1830, 118.6550, 119.1270, 119.5990, 120.0710 } }, + { 58, 120.0710, 1.8860, 2, 4, { 120.0710, 120.5420, 121.0130, 121.4850, 121.9570 } }, + { 59, 121.9570, 1.8820, 2, 4, { 121.9570, 122.4270, 122.8980, 123.3680, 123.8390 } }, + { 60, 123.8390, 1.8840, 2, 4, { 123.8390, 124.3100, 124.7810, 125.2520, 125.7230 } }, + { 61, 125.7230, 1.8870, 2, 4, { 125.7230, 126.1940, 126.6660, 127.1380, 127.6100 } }, + { 62, 127.6100, 1.8910, 0, 4, { 127.6100, 128.0820, 128.5550, 129.0280, 129.5010 } }, + { 63, 129.5010, 1.8930, 0, 4, { 129.5010, 129.9730, 130.4460, 130.9200, 131.3940 } }, + { 64, 131.3940, 1.8970, 2, 4, { 131.3940, 131.8680, 132.3420, 132.8160, 133.2910 } }, + { 65, 133.2910, 1.8990, 2, 4, { 133.2910, 133.7660, 134.2410, 134.7160, 135.1900 } }, + { 66, 135.1900, 1.8950, 2, 4, { 135.1900, 135.6640, 136.1370, 136.6110, 137.0850 } }, + { 67, 137.0850, 1.8970, 0, 4, { 137.0850, 137.5590, 138.0330, 138.5080, 138.9820 } }, + { 68, 138.9820, 1.8940, 0, 4, { 138.9820, 139.4560, 139.9300, 140.4030, 140.8760 } }, + { 69, 140.8760, 1.8900, 0, 4, { 140.8760, 141.3480, 141.8210, 142.2930, 142.7660 } }, + { 70, 142.7660, 1.8900, 0, 5, { 142.7660, 143.2380, 143.7100, 144.1830, 144.6550, 144.6560 } }, + { 71, 144.6550, 1.8890, 2, 4, { 144.6550, 145.1280, 145.6000, 146.0730, 146.5440 } }, + { 72, 146.5440, 1.8880, 2, 4, { 146.5440, 147.0160, 147.4880, 147.9600, 148.4320 } }, + { 73, 148.4320, 1.8900, 1, 4, { 148.4320, 148.9050, 149.3770, 149.8500, 150.3220 } }, + { 74, 150.3220, 1.8900, 2, 4, { 150.3220, 150.7950, 151.2670, 151.7400, 152.2130 } }, + { 75, 152.2130, 1.8890, 0, 5, { 152.2130, 152.6850, 153.1570, 153.6290, 154.1010, 154.1020 } }, + { 76, 154.1010, 1.8910, 0, 4, { 154.1010, 154.5740, 155.0470, 155.5200, 155.9920 } }, + { 77, 155.9920, 1.8890, 2, 4, { 155.9920, 156.4650, 156.9370, 157.4090, 157.8820 } }, + { 78, 157.8820, 1.8890, 2, 4, { 157.8820, 158.3540, 158.8270, 159.2990, 159.7710 } }, + { 79, 159.7710, 1.8850, 2, 4, { 159.7710, 160.2430, 160.7150, 161.1860, 161.6560 } }, + { 80, 161.6560, 1.8750, 1, 4, { 161.6560, 162.1260, 162.5950, 163.0640, 163.5320 } }, + { 81, 163.5320, 1.8700, 0, 5, { 163.5320, 164.0000, 164.4670, 164.9340, 165.4010, 165.4020 } }, + { 82, 165.4010, 1.8650, 0, 4, { 165.4010, 165.8680, 166.3340, 166.8000, 167.2660 } }, + { 83, 167.2660, 1.8640, 0, 4, { 167.2660, 167.7330, 168.1990, 168.6650, 169.1310 } }, + { 84, 169.1310, 1.8700, 2, 4, { 169.1310, 169.5980, 170.0650, 170.5330, 171.0010 } }, +}; + +#define CHART_NNOTES 279 +static const ChartNote CHART_NOTES[CHART_NNOTES] = { + { 15.6850, 0.2050, 54 }, + { 23.1900, 0.2350, 68 }, + { 24.6400, 0.3850, 56 }, + { 26.2500, 0.1500, 64 }, + { 26.6200, 0.8400, 56 }, + { 27.4600, 0.2200, 58 }, + { 27.7750, 0.7100, 59 }, + { 28.4850, 0.2700, 61 }, + { 28.7550, 0.7850, 59 }, + { 29.5400, 0.4500, 61 }, + { 29.9900, 0.8850, 63 }, + { 30.8750, 0.8450, 59 }, + { 31.7200, 0.3350, 61 }, + { 32.0550, 0.7650, 63 }, + { 32.8200, 0.1950, 61 }, + { 33.0150, 0.8100, 59 }, + { 33.8250, 0.2250, 68 }, + { 34.0500, 0.9650, 66 }, + { 35.1700, 0.1750, 58 }, + { 35.3450, 0.6150, 59 }, + { 35.9600, 0.3000, 61 }, + { 36.2600, 0.7300, 63 }, + { 36.9900, 0.3050, 61 }, + { 37.2950, 0.2600, 59 }, + { 37.5650, 0.4850, 59 }, + { 38.0500, 0.3100, 61 }, + { 38.3600, 0.7850, 63 }, + { 39.1450, 0.3950, 68 }, + { 39.5400, 0.2800, 66 }, + { 39.8200, 0.2100, 68 }, + { 40.0300, 1.0700, 63 }, + { 41.1200, 0.2000, 63 }, + { 41.3600, 0.2200, 68 }, + { 41.6600, 0.2900, 66 }, + { 41.9500, 0.2100, 68 }, + { 42.1600, 1.1700, 63 }, + { 43.3300, 0.3900, 64 }, + { 43.7400, 0.7050, 56 }, + { 44.4450, 0.3300, 58 }, + { 44.7750, 0.7800, 59 }, + { 45.5550, 0.2800, 61 }, + { 45.8350, 0.8432, 59 }, + { 46.6782, 0.2843, 61 }, + { 46.9625, 0.7452, 63 }, + { 47.9579, 0.7892, 59 }, + { 48.7471, 0.1753, 61 }, + { 48.9224, 0.9471, 63 }, + { 49.8695, 0.1923, 61 }, + { 50.0618, 0.7756, 59 }, + { 50.8374, 0.3066, 68 }, + { 51.3693, 0.7716, 66 }, + { 52.1409, 0.9620, 56 }, + { 53.1029, 0.7982, 59 }, + { 53.9011, 0.2437, 61 }, + { 54.1448, 0.5909, 59 }, + { 54.8740, 0.7718, 63 }, + { 55.6457, 0.3117, 68 }, + { 55.9575, 0.5559, 66 }, + { 56.5134, 0.1824, 68 }, + { 56.6958, 1.0492, 63 }, + { 57.9160, 0.2759, 68 }, + { 58.1919, 0.3547, 66 }, + { 58.5466, 0.2087, 68 }, + { 58.7553, 17.4638, 63 }, + { 60.6785, 0.6697, 56 }, + { 61.3482, 0.2537, 58 }, + { 61.6019, 0.7030, 59 }, + { 62.3049, 0.2987, 61 }, + { 62.6035, 0.1244, 58 }, + { 62.7279, 0.5436, 59 }, + { 63.2715, 0.2349, 61 }, + { 63.5064, 0.1918, 64 }, + { 63.6981, 0.5070, 63 }, + { 64.2051, 0.2977, 61 }, + { 64.5028, 1.0514, 59 }, + { 65.5543, 0.6754, 63 }, + { 66.2296, 0.1552, 61 }, + { 66.3848, 0.7055, 59 }, + { 67.1171, 0.2143, 68 }, + { 67.3629, 0.3586, 66 }, + { 67.7215, 0.3638, 64 }, + { 68.3871, 0.6715, 56 }, + { 69.0585, 0.2331, 58 }, + { 69.3147, 0.5170, 59 }, + { 69.8317, 0.2119, 58 }, + { 70.0435, 0.2298, 61 }, + { 70.2733, 0.7211, 59 }, + { 70.9944, 0.2253, 61 }, + { 71.2198, 0.7036, 63 }, + { 71.9234, 0.1845, 68 }, + { 72.1079, 0.4209, 66 }, + { 72.5288, 0.1814, 68 }, + { 72.7101, 0.9548, 63 }, + { 73.8478, 0.2438, 68 }, + { 74.0916, 0.3419, 66 }, + { 74.4335, 0.1779, 68 }, + { 74.6114, 0.1123, 64 }, + { 74.7236, 33.2825, 63 }, + { 76.2191, 0.1903, 61 }, + { 76.4093, 0.3084, 59 }, + { 76.7177, 0.2461, 61 }, + { 77.4785, 0.4795, 66 }, + { 77.9580, 0.5869, 64 }, + { 78.5449, 0.3678, 63 }, + { 78.9127, 0.1253, 59 }, + { 79.0798, 0.1207, 63 }, + { 79.2005, 0.6265, 66 }, + { 79.8270, 0.5601, 64 }, + { 80.3871, 0.2054, 61 }, + { 80.5925, 0.2985, 59 }, + { 81.0381, 0.2785, 59 }, + { 81.3166, 0.1230, 64 }, + { 81.4396, 0.3264, 66 }, + { 81.7660, 0.2645, 64 }, + { 82.0777, 0.1653, 56 }, + { 82.2431, 0.5471, 63 }, + { 82.7902, 0.3112, 59 }, + { 83.1013, 0.5702, 66 }, + { 83.6715, 0.4615, 64 }, + { 84.1331, 0.2825, 63 }, + { 84.4156, 0.2904, 59 }, + { 84.8777, 0.2099, 59 }, + { 85.1308, 1.1696, 63 }, + { 86.3004, 0.1240, 58 }, + { 86.4245, 0.3769, 59 }, + { 86.8014, 0.2290, 61 }, + { 87.0304, 0.2476, 64 }, + { 87.2780, 0.2095, 66 }, + { 87.4875, 0.4437, 64 }, + { 88.0069, 0.1979, 63 }, + { 88.2048, 0.2356, 59 }, + { 88.4404, 0.2166, 52 }, + { 88.6570, 0.7070, 59 }, + { 89.4021, 0.4667, 64 }, + { 89.9253, 0.7718, 63 }, + { 90.6971, 0.5368, 66 }, + { 91.2339, 0.1130, 63 }, + { 91.3469, 0.2261, 64 }, + { 91.6342, 0.3812, 63 }, + { 92.0154, 0.2852, 59 }, + { 92.3378, 0.3968, 59 }, + { 92.7346, 0.1219, 64 }, + { 92.8565, 0.3169, 66 }, + { 93.1734, 0.7121, 63 }, + { 93.8855, 0.5397, 59 }, + { 94.4252, 0.3257, 61 }, + { 94.7509, 0.2074, 66 }, + { 94.9583, 1.3615, 63 }, + { 96.3199, 0.2527, 59 }, + { 96.5726, 0.2193, 64 }, + { 96.7918, 0.2193, 66 }, + { 97.0111, 0.2145, 63 }, + { 97.2257, 0.2772, 64 }, + { 97.5029, 0.4342, 63 }, + { 97.9370, 0.4594, 59 }, + { 98.3964, 0.5205, 66 }, + { 98.9169, 0.1140, 63 }, + { 99.0446, 0.3758, 64 }, + { 99.4204, 0.2672, 63 }, + { 99.6876, 0.3003, 59 }, + { 100.1070, 0.4170, 61 }, + { 100.5240, 0.2848, 66 }, + { 100.8088, 0.3764, 64 }, + { 101.1852, 0.1436, 68 }, + { 101.3288, 0.7068, 63 }, + { 102.0356, 0.1555, 64 }, + { 102.1911, 0.5042, 66 }, + { 102.6953, 0.1429, 64 }, + { 102.8382, 0.1572, 63 }, + { 102.9955, 0.2323, 64 }, + { 108.0061, 0.2616, 61 }, + { 108.2677, 0.1821, 59 }, + { 108.4498, 0.2381, 61 }, + { 108.7065, 0.1095, 68 }, + { 108.8161, 0.4014, 66 }, + { 109.2174, 0.5678, 64 }, + { 109.7853, 0.2674, 63 }, + { 110.0527, 0.2677, 59 }, + { 110.3557, 0.1263, 63 }, + { 110.4820, 0.6465, 66 }, + { 111.1286, 0.2162, 63 }, + { 111.3448, 0.2018, 64 }, + { 111.5467, 0.2617, 61 }, + { 111.8084, 0.2805, 59 }, + { 112.1934, 0.1425, 56 }, + { 112.3360, 0.1568, 59 }, + { 112.4928, 0.1566, 64 }, + { 112.6494, 0.4517, 66 }, + { 113.1010, 0.2255, 64 }, + { 113.3266, 0.1467, 61 }, + { 113.4732, 0.5124, 63 }, + { 113.9857, 0.3158, 59 }, + { 114.3876, 0.1408, 66 }, + { 114.5472, 0.2951, 66 }, + { 114.8423, 0.5391, 64 }, + { 115.3814, 0.2037, 63 }, + { 115.5852, 0.2660, 59 }, + { 116.1080, 0.3499, 58 }, + { 116.4579, 0.1608, 66 }, + { 116.6188, 0.1307, 64 }, + { 116.7494, 0.8520, 63 }, + { 117.6015, 0.3739, 59 }, + { 117.9754, 0.3325, 61 }, + { 118.3079, 0.4351, 66 }, + { 118.7430, 0.4225, 64 }, + { 119.1656, 0.3916, 63 }, + { 119.5571, 0.8084, 59 }, + { 120.3655, 0.1832, 66 }, + { 120.5487, 0.4896, 64 }, + { 121.0383, 0.4987, 63 }, + { 121.5370, 0.3043, 61 }, + { 121.8413, 0.4061, 66 }, + { 122.2474, 0.2464, 64 }, + { 122.4938, 0.1687, 66 }, + { 122.6626, 0.2121, 64 }, + { 122.8747, 0.2266, 63 }, + { 123.1013, 0.1931, 59 }, + { 123.2944, 0.1236, 61 }, + { 123.5290, 0.1495, 52 }, + { 123.6785, 0.1399, 58 }, + { 123.9829, 0.3256, 66 }, + { 124.3085, 0.7657, 63 }, + { 125.0742, 0.4633, 59 }, + { 125.5376, 0.2084, 61 }, + { 125.8562, 0.1905, 66 }, + { 126.0467, 0.3410, 64 }, + { 126.3878, 0.9521, 63 }, + { 127.3399, 0.2212, 59 }, + { 127.6297, 0.1751, 64 }, + { 127.8521, 0.2129, 66 }, + { 128.0650, 0.5672, 64 }, + { 128.6322, 0.3477, 63 }, + { 128.9799, 0.1887, 59 }, + { 129.1687, 0.1490, 68 }, + { 129.3375, 0.2106, 63 }, + { 129.5481, 0.3698, 66 }, + { 129.9179, 0.2435, 64 }, + { 130.1896, 0.2808, 64 }, + { 130.4703, 0.2918, 63 }, + { 130.7622, 0.1807, 61 }, + { 131.1329, 0.2272, 59 }, + { 131.6425, 0.2115, 66 }, + { 131.8539, 0.4947, 64 }, + { 132.3487, 0.3788, 63 }, + { 132.7274, 0.4982, 59 }, + { 133.2256, 0.2394, 66 }, + { 133.5332, 0.2288, 66 }, + { 133.7619, 0.4866, 64 }, + { 134.2485, 0.1413, 61 }, + { 134.3898, 4.0191, 63 }, + { 148.1978, 0.5050, 58 }, + { 148.7028, 0.1617, 66 }, + { 148.8645, 0.6635, 64 }, + { 149.5280, 0.3176, 63 }, + { 149.8456, 0.4366, 59 }, + { 150.2823, 0.5370, 66 }, + { 150.8192, 0.5074, 64 }, + { 151.3266, 0.3740, 63 }, + { 151.7006, 0.2748, 64 }, + { 152.2655, 0.2951, 63 }, + { 152.5606, 0.4933, 64 }, + { 153.0539, 0.3827, 63 }, + { 153.5481, 0.3486, 59 }, + { 153.8966, 0.2704, 61 }, + { 154.1671, 0.4030, 64 }, + { 154.5701, 0.2489, 63 }, + { 154.8190, 0.2881, 64 }, + { 155.1071, 0.2815, 63 }, + { 155.3886, 0.2161, 59 }, + { 156.0269, 0.2332, 64 }, + { 156.2600, 0.1885, 66 }, + { 156.4486, 0.5444, 64 }, + { 156.9930, 0.3443, 63 }, + { 157.4148, 0.2760, 58 }, + { 157.8119, 0.4753, 66 }, + { 158.3506, 0.4547, 64 }, + { 158.8053, 2.6401, 63 }, + { 161.4454, 0.2265, 61 }, + { 161.6719, 2.1692, 59 }, +}; diff --git a/pop/sailor-song/c/sailorremix-v5.c b/pop/sailor-song/c/sailorremix-v5.c new file mode 100644 index 0000000000..e8e7d3e459 --- /dev/null +++ b/pop/sailor-song/c/sailorremix-v5.c @@ -0,0 +1,692 @@ +// sailorremix.c — "sailor song (outside remix)", the C engine. +// +// A cover of Gigi Perez's "Sailor Song", played by @jeffrey's niece: her +// voice and a capo-4 nylon guitar on one phone mic. This file is the score. +// It follows the /pop single-file renderer shape (pop/loner/c, +// pop/cult/c): the measuring side bakes, this engine arranges and mixes. +// +// THE DEAL bin/aesthetivox.py owns WORLD (her lead regulated onto G# +// minor in her guitar's +13¢ frame, a dark octave halo, five +// self-harmony stems); bin/lock-vox.mjs pulls every onset onto +// her own strum 8ths; bin/replay-guitar.mjs re-plays her part +// bar for bar on pop/guitar/c/strum; bin/bells.sh bakes a FEM +// bell bank (pop/bell/c); bin/chart.mjs emits sailor-chart.h — +// her bars, her snapped beats, her chords, her tuned notes. +// FOLLOW Nothing is warped to a grid. Every hit lands on HER strum +// points (her motif X..XX..X: 1, &2, 3, &4). The clock breathes +// from 110 to 125 because she does. +// POCKET kick 1 + &2, brush snare 3, rim &4 — her own strum points. +// TRAP a gliding 808 on the kick points tuned to her roots; bright +// 16th hats with 32nd / triplet rolls into every other bar +// line; a layered clap on 3. +// SINES voice-led sine choir — sub on the roots, a tenor trio under +// her, a high pair over her, her register (G#3–G#4) left clear +// — plus a descant, her melody shadowed a 3rd and an octave up, +// and her chorus hook re-laid over the guitar-only break. +// Saturated and pumped by the kick so they hold in a pop mix. +// AC FEM bells answer her in the breath gaps (her last three +// notes ring back); a vibraphone (pop/marimba's modal preset, +// ported) arpeggiates her chords on her 8ths. +// SPACE pop/nullabye/c/ac_hrtf.h: bells orbit overhead, the vibes +// sweep ear to ear, the highs float up and wide, and her +// harmonies stand at fixed places — in the bridge they rove. +// ROOM near early reflections always on, a 2.2 s dark FDN behind. +// Verses close and bright (air above 7 kHz), choruses roomier. +// +// Form (bars from sailor-chart.h, 2:59): +// intro 1–10 · verse1 11–27 · chorus1 28–43 · verse2 44–51 · +// chorus2 52–67 · break 68–71 · bridge 72–80 · outro 81–84 +// +// Build: bash pop/sailor-song/c/build.sh +// Run: pop/sailor-song/c/sailorremix (from the repo root) +// → pop/sailor-song/out/sailor-song-v5-full.wav + .events.json +// bash pop/sailor-song/c/cut.sh → master (−10 LUFS) + mp3 w/ cover + +#include +#include +#include +#include +#include + +#include "../../nullabye/c/ac_hrtf.h" +#include "sailor-chart.h" + +#define SR 48000 +#define LANE "pop/sailor-song" +#define PI 3.14159265358979323846 + +// ── rng (seeded, so the same score renders the same bytes) ────────────── +static uint32_t rng_state = 0x5a11025u; +static double rnd(void) { // −1..1 + rng_state ^= rng_state << 13; rng_state ^= rng_state >> 17; rng_state ^= rng_state << 5; + return (double)rng_state / 2147483648.0 - 1.0; +} + +// ── WAV IO (the fleet loader, loner/cult's, stereo) ────────────────────── +typedef struct { float *L, *R; long n; } Stereo; +static Stereo load_wav(const char *path) { + Stereo s = { 0 }; + FILE *f = fopen(path, "rb"); + if (!f) { fprintf(stderr, " ! missing %s\n", path); return s; } + fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET); + uint8_t *buf = (uint8_t *)malloc(sz); + if (!buf || (long)fread(buf, 1, sz, f) != sz) { fclose(f); free(buf); return s; } + fclose(f); + long p = 12; int fmt = 1, ch = 1, bits = 16; long dOff = 0, dLen = 0; + while (p + 8 <= sz) { + uint32_t c = (uint32_t)buf[p+4] | (uint32_t)buf[p+5] << 8 | (uint32_t)buf[p+6] << 16 | (uint32_t)buf[p+7] << 24; + if (!memcmp(buf + p, "fmt ", 4)) { fmt = buf[p+8] | (buf[p+9] << 8); ch = buf[p+10] | (buf[p+11] << 8); bits = buf[p+22] | (buf[p+23] << 8); } + else if (!memcmp(buf + p, "data", 4)) { dOff = p + 8; dLen = c; if (dOff + dLen > sz) dLen = sz - dOff; break; } + p += 8 + c + (c & 1); + } + const int bps = bits / 8; const long frames = dLen / (bps * ch); + s.L = (float *)calloc(frames, sizeof(float)); s.R = (float *)calloc(frames, sizeof(float)); s.n = frames; + for (long i = 0; i < frames; i++) for (int c = 0; c < 2; c++) { + const long o = dOff + (i * ch + (c < ch ? c : 0)) * bps; double v; + if (fmt == 3 && bits == 32) { float x; memcpy(&x, buf + o, 4); v = x; } + else if (bits == 32) { int32_t x; memcpy(&x, buf + o, 4); v = x / 2147483648.0; } + else if (bits == 24) { int32_t x = buf[o] | (buf[o+1] << 8) | ((int32_t)(int8_t)buf[o+2] << 16); v = x / 8388608.0; } + else { int16_t x; memcpy(&x, buf + o, 2); v = x / 32768.0; } + (c ? s.R : s.L)[i] = (float)v; + } + free(buf); + return s; +} +static void write_wav_f32(const char *path, const float *L, const float *R, long n) { + FILE *f = fopen(path, "wb"); + if (!f) { fprintf(stderr, "! cannot write %s\n", path); exit(1); } + uint32_t dsz = (uint32_t)(n * 8), riff = 36 + dsz, sr = SR, br = SR * 8, fsz = 16; + uint16_t fmt = 3, ch = 2, ba = 8, bits = 32; + fwrite("RIFF", 1, 4, f); fwrite(&riff, 4, 1, f); fwrite("WAVE", 1, 4, f); + fwrite("fmt ", 1, 4, f); fwrite(&fsz, 4, 1, f); + fwrite(&fmt, 2, 1, f); fwrite(&ch, 2, 1, f); fwrite(&sr, 4, 1, f); + fwrite(&br, 4, 1, f); fwrite(&ba, 2, 1, f); fwrite(&bits, 2, 1, f); + fwrite("data", 1, 4, f); fwrite(&dsz, 4, 1, f); + for (long i = 0; i < n; i++) { fwrite(&L[i], 4, 1, f); fwrite(&R[i], 4, 1, f); } + fclose(f); +} +static float sample(const float *b, long len, long i) { return (b && i >= 0 && i < len) ? b[i] : 0.0f; } + +// ── the form + the arrangement ────────────────────────────────────────── +enum { INTRO, VERSE1, CHORUS1, VERSE2, CHORUS2, BREAK, BRIDGE, OUTRO, NSEC }; +static const char *SEC_NAME[NSEC] = { "intro", "verse1", "chorus1", "verse2", "chorus2", "break", "bridge", "outro" }; +static const int SEC_FROM[NSEC] = { 1, 11, 28, 44, 52, 68, 72, 81 }; +static int section_of(int bar) { int s = 0; for (int k = 0; k < NSEC; k++) if (bar >= SEC_FROM[k]) s = k; return s; } + +// her own harmonies: which stem sings, how loud, where (azimuth °) +enum { H_UP3, H_DOWN3, H_UP5, H_DOWN6, H_DOWN8, NHARM }; +static const char *HARM_FILE[NHARM] = { "harm-up3", "harm-down3", "harm-up5", "harm-down6", "harm-down8" }; +static const double HARM_DELAY[NHARM] = { 0.018, 0.026, 0.022, 0.030, 0.012 }; // a breath apart +typedef struct { double g, az; } HarmSet; + +typedef struct { + double her, acg, elg; // her guitar / acoustic replay / electric replay + double kick, snare, clap, rim, hat, trap, b808; + double sub, tenor, high, third, octave, descant, hook, vib, bell; + HarmSet harm[NHARM]; + double send, air; // vocal room send, air lift +} Arr; +#define H(i, g, az) [i] = { g, az } +static const Arr ARR[NSEC] = { + [INTRO] = { .8, 0, 0, 0, 0, 0, 0, 0, 0, 0, .5, .7, .3, 0, 0, 0, 0, .5, .6, + { { 0 } }, .12, .2 }, + [VERSE1] = { .8, .15, 0, .7, 0, 0, .5, 0, 0, 0, .8, .8, 0, 0, 0, 0, 0, .35, .9, + { { 0 } }, .07, .45 }, + [CHORUS1] = { .45, .55, .2, .9, .7, .6, .5, 0, .7, .8, 1, 1, .7, .35, 0, 0, 0, .6, .6, + { H(H_UP3, .34, -50), H(H_DOWN6, .28, 50) }, .14, .3 }, + [VERSE2] = { .7, .3, 0, .8, .5, 0, .5, .4, 0, .4, .9, .9, .3, 0, 0, 0, 0, .5, .9, + { H(H_DOWN3, .24, 45) }, .08, .45 }, + [CHORUS2] = { .4, .6, .35, 1, .8, .8, .5, 0, 1, 1, 1, 1, .9, .3, .3, 1, 0, .7, .5, + { H(H_UP3, .34, -60), H(H_DOWN3, .30, 60), H(H_DOWN8, .24, 0) }, .15, .35 }, + [BREAK] = { .9, 0, 0, 0, 0, 0, 0, 0, 0, 0, .8, 1, 1, 0, 0, .6, 1, .6, .8, + { { 0 } }, .20, .2 }, + [BRIDGE] = { .5, .5, .3, .8, .6, .5, .5, 0, .8, .8, 1, 1, .9, 0, .3, .5, 0, .8, .6, + { { 0 } }, .09, .5 }, // harmonies rove, see harm_at() + [OUTRO] = { .9, 0, 0, .5, 0, 0, .4, 0, 0, .3, .8, .8, .6, 0, 0, 0, 0, .4, 1, + { H(H_DOWN8, .30, 0), H(H_UP3, .20, -40) }, .20, .15 }, +}; +// "pitching around": in the bridge her harmony changes interval and place each bar +static HarmSet harm_at(int bar, int h) { + int s = section_of(bar); + if (s != BRIDGE) return ARR[s].harm[h]; + static const HarmSet ROVE[4][NHARM] = { + { H(H_UP3, .36, -60) }, { H(H_UP5, .30, 60) }, { H(H_DOWN3, .34, -35) }, { H(H_UP3, .30, 45), H(H_DOWN6, .24, -50) } }; + return ROVE[(bar - 72) % 4][h]; +} + +// ── buses ──────────────────────────────────────────────────────────────── +static long N; +static float *dL, *dR, *tL, *tR, *b808, *sL, *sR, *hiM, *bellM, *vibM, *duck; +static void add(float *b, long i, double v) { if (i >= 0 && i < N) b[i] += (float)v; } +static long at(double t) { return lround(t * SR); } +static double mtof(double m) { return 440.0 * pow(2.0, (m + CHART_TUNE - 69.0) / 12.0); } + +// score events → out/…events.json for the score video +static FILE *EV; +static int evFirst = 1; +static void ev(double t, const char *voice, double dur, double gain, int midi) { + if (!EV) return; + fprintf(EV, "%s{\"t\":%.3f,\"voice\":\"%s\",\"dur\":%.3f,\"gain\":%.2f%s", evFirst ? "" : ",\n", t, voice, dur, gain, ""); + if (midi >= 0) fprintf(EV, ",\"midi\":%d", midi); + fputc('}', EV); evFirst = 0; +} + +// ── the kit ────────────────────────────────────────────────────────────── +static void kick(double t, double g) { + long a = at(t); double ph = 0; + for (long i = 0; i < 0.36 * SR; i++) { double u = (double)i / SR; + ph += 2 * PI * (48 + 110 * exp(-u * 45)) / SR; // v5.1: a real pop kick — + double click = i < 0.004 * SR ? rnd() * 0.5 * (1 - u / 0.004) : 0; // beater click + double v = (tanh(sin(ph) * 1.6) * exp(-u * 6.5) + click) * g; add(dL, a + i, v); add(dR, a + i, v); } + for (long i = 0; i < 0.25 * SR; i++) add(duck, a + i, g * exp(-(double)i / SR * 9)); +} +static void snare(double t, double g) { // brushy: slow swell-in, dark noise, soft body + long a = at(t); double prev = 0, lp = 0, ph = 0; + for (long i = 0; i < 0.24 * SR; i++) { double u = (double)i / SR, nz = rnd(), hp = nz - prev; prev = nz; + lp += (hp - lp) * 0.45; ph += 2 * PI * (200 + 60 * exp(-u * 60)) / SR; // v5.1: crisp, not brushed + double v = (lp * exp(-u * 17) * 1.4 + sin(ph) * 0.55 * exp(-u * 26)) * g; + add(dL, a + i, v * 0.92); add(dR, a + i, v); } +} +static void rim(double t, double g) { + long a = at(t); + for (long i = 0; i < 0.07 * SR; i++) { double u = (double)i / SR; + double v = (sin(2 * PI * 1650 * u) * 0.5 + sin(2 * PI * 520 * u)) * exp(-u * 70) * g; + add(dL, a + i, v * 1.1); add(dR, a + i, v * 0.8); } +} +static void soft_hat(double t, double g, double pan) { + long a = at(t); double prev = 0; + for (long i = 0; i < 0.03 * SR; i++) { double nz = rnd(), hp = nz - prev; prev = nz; + double v = hp * exp(-(double)i / SR * 300) * g * 0.6; add(dL, a + i, v * (1 - pan)); add(dR, a + i, v * (1 + pan)); } +} +static void trap_hat(double t, double g, double pan) { // two-stage highpass: brighter, tighter + long a = at(t); double p1 = 0, p2 = 0; + for (long i = 0; i < 0.022 * SR; i++) { double nz = rnd(), h1 = nz - p1; p1 = nz; double h2 = h1 - p2; p2 = h1; + double v = h2 * exp(-(double)i / SR * 220) * g * 0.35; add(tL, a + i, v * (1 - pan)); add(tR, a + i, v * (1 + pan)); } +} +static void clap(double t, double g) { + static const double OFF[3] = { 0, 0.011, 0.022 }, AMP[3] = { 0.7, 0.8, 1.0 }; + for (int k = 0; k < 3; k++) { + long a = at(t + OFF[k]); double lp = 0, prev = 0; + for (long i = 0; i < 0.16 * SR; i++) { double u = (double)i / SR, nz = rnd(), hp = nz - prev; prev = nz; + lp += (hp - lp) * 0.45; + double v = lp * exp(-u * (k < 2 ? 90 : 16)) * g * AMP[k] * 0.9; add(tL, a + i, v); add(tR, a + i, v * 0.95); } + } +} +// 808: drops ~a 4th into the root, long tail, tanh so its harmonics speak +// above 180 Hz on a phone +static void eight08(double t, int midi, double dur, double g) { + long a = at(t); double f1 = mtof(midi), f0 = f1 * 1.33, ph = 0; + for (long i = 0; i < (dur + 0.15) * SR; i++) { double u = (double)i / SR; + ph += 2 * PI * (f1 + (f0 - f1) * exp(-u * 26)) / SR; + double env = fmin(1, u / 0.002) * (u > dur ? exp(-(u - dur) * 30) : exp(-u * 1.1)); + add(b808, a + i, tanh(sin(ph) * 3.4) * 0.7 * env * g); /* v5.1: drive 3.4 so harmonics carry it on phones */ } + for (long i = 0; i < 0.3 * SR; i++) add(duck, a + i, g * exp(-(double)i / SR * 7)); +} + +// ── sines ──────────────────────────────────────────────────────────────── +// slow swell, 4.5 Hz tremolo, a ±det¢ twin. R == NULL → mono (to a spatial bus) +static void sine(float *L, float *R, double t0, double dur, double midi, double g, double pan, + double atk, double rel, double det) { + double f = mtof(midi), fa = f * pow(2, det / 1200), fb = f * pow(2, -det / 1200); + double pa = (rnd() + 1) * 3, pb = (rnd() + 1) * 3, pc_ = 0, pa0 = pa, pb0 = pb; + double gl = g * sqrt((1 - pan) / 2), gr = g * sqrt((1 + pan) / 2); + long a = at(t0), len = (long)((dur + rel) * SR); + for (long i = 0; i < len; i++) { + double u = (double)i / SR; + double env = fmin(1, u / atk) * (u > dur ? exp(-(u - dur) / (rel / 3)) : 1); + // analog: each voice drifts on its own slow wander; a centre third voice; soft saturation + double drift = det > 0 ? 1 + 0.0012 * sin(2 * PI * 0.13 * u + pa0) + 0.0007 * sin(2 * PI * 0.31 * u + pb0) : 1; + pa += 2 * PI * fa * drift / SR; pb += 2 * PI * fb / drift / SR; pc_ += 2 * PI * f * drift / SR; + double trem = 1 - 0.08 * (0.5 + 0.5 * sin(2 * PI * 4.5 * u)); + double c3 = det > 0 ? sin(pc_) * 0.5 : 0, xa = tanh((sin(pa) + c3) * 1.3) / 1.1, xb = tanh((sin(pb) + c3) * 1.3) / 1.1; + if (!R) add(L, a + i, (xa + xb) * 0.5 * env * trem * g); + else { add(L, a + i, xa * env * trem * gl); add(R, a + i, xb * env * trem * gr); } + } +} +#define PAD(t, d, m, g, pan) sine(sL, sR, t, d, m, g, pan, 0.35, 0.9, 3) + +// ── AC voices ──────────────────────────────────────────────────────────── +// FEM bells from the baked bank (bin/bells.sh). Loner rule: ≤ E5, fade tails. +static Stereo BELLS[128]; +static void fem_bell(double t, int midi, double g) { + while (midi > 76) midi -= 12; + while (midi < 63) midi += 12; + if (!BELLS[midi].L) { + char p[256]; snprintf(p, sizeof p, LANE "/src/bells/bell-%d.wav", midi); + BELLS[midi] = load_wav(p); + if (!BELLS[midi].L) return; + long f = (long)(0.6 * SR); + for (long i = 0; i < f && i < BELLS[midi].n; i++) { double k = (double)i / f; + BELLS[midi].L[BELLS[midi].n - 1 - i] *= (float)k; BELLS[midi].R[BELLS[midi].n - 1 - i] *= (float)k; } + } + const Stereo *b = &BELLS[midi]; long a = at(t); + for (long i = 0; i < b->n; i++) add(bellM, a + i, (b->L[i] + b->R[i]) * 0.5 * g); + ev(t, "bell", 1.2, g, midi); +} +// vibraphone — pop/marimba's modal preset, ported: partials 1/4/10, decays +// 4.5/0.9/0.25 s, 5.5 Hz motor tremolo at 0.55; damper after `hold` +static void vib(double t, int midi, double g, double hold) { + static const double P[3] = { 1, 4, 10 }, AMP[3] = { 1, 0.22, 0.08 }, DEC[3] = { 4.5, 0.9, 0.25 }; + double f = mtof(midi); long a = at(t), len = (long)((hold + 0.5) * SR); + for (long i = 0; i < len; i++) { double u = (double)i / SR, v = 0; + for (int k = 0; k < 3; k++) if (f * P[k] < SR / 2) v += AMP[k] * sin(2 * PI * f * P[k] * u) * exp(-u / DEC[k]); + double damp = u > hold ? exp(-(u - hold) * 10) : 1; + double trem = 1 - 0.55 * (0.5 + 0.5 * sin(2 * PI * 5.5 * u)); + add(vibM, a + i, v * fmin(1, u / 0.001) * damp * trem * g * 0.6); } + ev(t, "vib", 0.25, g, midi); +} + +// ── harmony ────────────────────────────────────────────────────────────── +static const int CHORD_PCS[3][3] = { { 8, 11, 3 }, { 8, 11, 3 }, { 11, 3, 6 } }; // Emaj7 bars sound G#m (bass G#) +static const int ROOT[3] = { 44, 44, 47 }; // G#2, B2 +static const int SCALE[7] = { 8, 10, 11, 1, 3, 4, 6 }; // G# natural minor +static int is_pc(const int *pcs, int np, int m) { int pc = ((m % 12) + 12) % 12; for (int k = 0; k < np; k++) if (pcs[k] == pc) return 1; return 0; } +static int nearest(const int *pcs, int np, double target, int lo, int hi) { + int best = -1; for (int m = lo; m <= hi; m++) if (is_pc(pcs, np, m) && (best < 0 || fabs(m - target) < fabs(best - target))) best = m; + return best; +} +static void lead(int *voices, int nv, const int *pcs, int lo, int hi) { // nearest chord tone per voice, no doubling + int used[3] = { -1, -1, -1 }, nu = 0; + for (int v = 0; v < nv; v++) { + int freeP[3], nf = 0; + for (int k = 0; k < 3; k++) { int u = 0; for (int j = 0; j < nu; j++) if (used[j] == pcs[k]) u = 1; if (!u) freeP[nf++] = pcs[k]; } + int m = nf ? nearest(freeP, nf, voices[v], lo, hi) : nearest(pcs, 3, voices[v], lo, hi); + voices[v] = m; if (nu < 3) used[nu++] = ((m % 12) + 12) % 12; + } +} +static int third_above(int m) { + int pc = ((m % 12) + 12) % 12, oct = m / 12 - (m < 0), i = -1; + for (int k = 0; k < 7; k++) if (SCALE[k] == pc) i = k; + if (i < 0) return m + 3; + int t = SCALE[(i + 2) % 7]; + return oct * 12 + t + (t < pc ? 12 : 0); +} +static const ChartBar *bar_at(double t) { + for (int k = 0; k < CHART_NBARS; k++) if (t >= CHART_BARS[k].t && t < CHART_BARS[k].t + CHART_BARS[k].dur) return &CHART_BARS[k]; + return NULL; +} + +// ── automation: a per-bar target, glided over 150 ms ───────────────────── +typedef double (*BarValue)(const ChartBar *, int); +static float *automate(BarValue fn, int arg) { + float *a = (float *)malloc(N * sizeof(float)); double cur = -1, glide = 1 - exp(-1.0 / (0.15 * SR)); + const ChartBar *b = &CHART_BARS[0]; + for (long i = 0; i < N; i++) { + double t = (double)i / SR; + while (b < &CHART_BARS[CHART_NBARS - 1] && t >= b->t + b->dur) b++; + double tg = fn(b, arg); + cur = cur < 0 ? tg : cur + (tg - cur) * glide; a[i] = (float)cur; + } + return a; +} +#define ARRV(field) static double v_##field(const ChartBar *b, int x) { (void)x; return ARR[section_of(b->n)].field; } +ARRV(send) ARRV(air) ARRV(her) ARRV(acg) ARRV(elg) +static double v_harm_g(const ChartBar *b, int h) { return harm_at(b->n, h).g; } +static double v_harm_az(const ChartBar *b, int h) { return harm_at(b->n, h).az; } + +// ── space: ac_hrtf over a whole bus ───────────────────────────────────── +typedef double (*Path)(double t); +static void spatialize(const float *mono, const float *azA, Path az, Path el, double dist, float *L, float *R, double g) { + ACHrtf h; memset(&h, 0, sizeof h); + for (long i = 0; i < N; i++) { + double t = (double)i / SR, a = azA ? azA[i] : az(t); + float l, r; + ac_hrtf_process(&h, mono[i], a * PI / 180, el(t) * PI / 180, dist, &l, &r); + L[i] += l * (float)g; R[i] += r * (float)g; + } +} +static double orbit_az(double t) { return 70 * sin(2 * PI * t / 24); } +static double orbit_el(double t) { return 25 + 10 * sin(2 * PI * t / 17); } +static double sweep_az(double t) { return 65 * sin(2 * PI * t / 8); } +static double wide_az(double t) { return 80 * sin(2 * PI * t / 31 + 1); } +static double el_zero(double t) { (void)t; return 0; } +static double el_up(double t) { (void)t; return 35; } +static double el_near(double t) { (void)t; return 5; } + +// ── reverb: 8-line FDN (Hadamard feedback, damped) ────────────────────── +static void fdn(const float *inL, const float *inR, float *oL, float *oR, double t60, double damp, double pre) { + static const int LENS[8] = { 1433, 1601, 1867, 2053, 2251, 2399, 2617, 2797 }; + float *buf[8]; int len[8], idx[8] = { 0 }; double lp[8] = { 0 }, gain[8], o[8], hh[8]; + for (int k = 0; k < 8; k++) { len[k] = (int)lround(LENS[k] * (double)SR / 44100); buf[k] = (float *)calloc(len[k], sizeof(float)); + gain[k] = pow(10, -3.0 * len[k] / (SR * t60)); } + long P = lround(pre * SR); + for (long i = 0; i < N; i++) { + double xl = i >= P ? inL[i - P] : 0, xr = i >= P ? inR[i - P] : 0; + for (int k = 0; k < 8; k++) { lp[k] += (buf[k][idx[k]] - lp[k]) * (1 - damp); o[k] = lp[k]; } + double a0 = o[0] + o[1], a1 = o[0] - o[1], a2 = o[2] + o[3], a3 = o[2] - o[3], a4 = o[4] + o[5], a5 = o[4] - o[5], a6 = o[6] + o[7], a7 = o[6] - o[7]; + double b0 = a0 + a2, b1 = a1 + a3, b2 = a0 - a2, b3 = a1 - a3, b4 = a4 + a6, b5 = a5 + a7, b6 = a4 - a6, b7 = a5 - a7; + hh[0] = b0 + b4; hh[1] = b1 + b5; hh[2] = b2 + b6; hh[3] = b3 + b7; hh[4] = b0 - b4; hh[5] = b1 - b5; hh[6] = b2 - b6; hh[7] = b3 - b7; + for (int k = 0; k < 8; k++) { buf[k][idx[k]] = (float)(hh[k] * 0.35355 * gain[k] + (k % 2 ? xr : xl) * 0.5); idx[k] = (idx[k] + 1) % len[k]; } + oL[i] = (float)(o[0] + o[2] + o[4] + o[6]); oR[i] = (float)(o[1] + o[3] + o[5] + o[7]); + } + for (int k = 0; k < 8; k++) free(buf[k]); +} + +static float *zeros(void) { return (float *)calloc(N, sizeof(float)); } + +// humanize: ±4 ms, ±12 % — a hand, not a sequencer ("a bit midi like") +static double hum_t(double t) { return t + rnd() * 0.004; } +static double hum_g(double g) { return g * (1 + rnd() * 0.12); } + +// ── the dance layer (v5.2: "turn the song into a dance mix") ───────────── +// Her ~120 is house tempo, so four on HER floor: a kick on every one of her +// beats, claps 2 + 4, open hats on every offbeat, an offbeat house bass on +// her roots, risers into the drops. +static void dance_kick(double t, double g) { + long a = at(t); double ph = 0; + for (long i = 0; i < 0.42 * SR; i++) { double u = (double)i / SR; + ph += 2 * PI * (50 + 160 * exp(-u * 38) + 40 * exp(-u * 300)) / SR; + double click = i < 0.003 * SR ? rnd() * 0.6 * (1 - u / 0.003) : 0; + double v = (tanh(sin(ph) * 2.4) * 0.85 * exp(-u * 5.5) + click) * g; + add(dL, a + i, v); add(dR, a + i, v); } + for (long i = 0; i < 0.3 * SR; i++) add(duck, a + i, g * 1.3 * exp(-(double)i / SR * 8)); +} +static void open_hat(double t, double g) { + long a = at(t); double p1 = 0, p2 = 0; + for (long i = 0; i < 0.16 * SR; i++) { double u = (double)i / SR, nz = rnd(), h1 = nz - p1; p1 = nz; double h2 = h1 - p2; p2 = h1; + double v = h2 * fmin(1, u / 0.002) * exp(-u * 16) * g * 0.3; add(tL, a + i, v * 0.8); add(tR, a + i, v * 1.1); } +} +// house bass: a saturated sine pluck with a closing lowpass, on the offbeat +static void house_bass(double t, int midi, double dur, double g) { + long a = at(t); double f = mtof(midi), ph = 0, lp = 0; + for (long i = 0; i < (dur + 0.05) * SR; i++) { double u = (double)i / SR; + ph += 2 * PI * f / SR; + double raw = tanh(sin(ph) * 4.0 + 0.6 * sin(2 * ph)); + double fc = 180 + 2400 * exp(-u * 14), k = 1 - exp(-2 * PI * fc / SR); + lp += (raw - lp) * k; + double env = fmin(1, u / 0.003) * (u > dur ? exp(-(u - dur) * 60) : 1); + add(b808, a + i, lp * env * g); } +} +// riser: white noise through a rising bandpass, swelling into the downbeat +static void riser(double t0, double t1, double g) { + long a = at(t0), len = at(t1) - a; double b1 = 0, b2 = 0; + for (long i = 0; i < len; i++) { double x = (double)i / len, fc = 300 * pow(40, x), k = 1 - exp(-2 * PI * fc / SR); + double nz = rnd(); b1 += (nz - b1) * k; b2 += (b1 - b2) * k; + double v = (b1 - b2) * x * x * g; add(tL, a + i, v * (1 - 0.3 * sin(PI * 8 * x))); add(tR, a + i, v * (1 + 0.3 * sin(PI * 8 * x))); } +} +// 4otf clap ohat hbass (v5.2) +static const double DANCE[NSEC][4] = { + [INTRO] = { .6, 0, 0, 0 }, [VERSE1] = { .75, .5, .4, .6 }, [CHORUS1] = { 1, 1, .8, 1 }, [VERSE2] = { .85, .7, .6, .8 }, + [CHORUS2] = { 1, 1, 1, 1 }, [BREAK] = { 0, 0, 0, 0 }, [BRIDGE] = { 1, .8, .8, .9 }, [OUTRO] = { .7, .4, .3, .5 } }; + +// ── hand percussion (v5.1: "we have no actual percussion yet") ─────────── +static float *pL, *pR; +// shaker: band-passed noise with a swell-in (the bead mass lags the hand) +static void shaker(double t, double g, double pan) { + long a = at(t); double b1 = 0, b2 = 0, prev = 0; + for (long i = 0; i < 0.07 * SR; i++) { double u = (double)i / SR, nz = rnd(), hp = nz - prev; prev = nz; + b1 += (hp - b1) * 0.55; b2 += (b1 - b2) * 0.55; + double env = fmin(1, u / 0.012) * exp(-u * 45); + double v = (b1 - b2 * 0.6) * env * g; add(pL, a + i, v * (1 - pan)); add(pR, a + i, v * (1 + pan)); } +} +// tambourine: jingles = noise through three metallic partials, a short hit + ring +static void tamb(double t, double g, double pan) { + static const double F[3] = { 7040, 9360, 11870 }; + long a = at(t); double prev = 0; + for (long i = 0; i < 0.22 * SR; i++) { double u = (double)i / SR, nz = rnd(), hp = nz - prev, v = 0; prev = nz; + for (int k = 0; k < 3; k++) v += sin(2 * PI * F[k] * u + k) * 0.25; + v = (v * (0.6 + 0.4 * nz) + hp * 0.5) * exp(-u * 18) * g; + add(pL, a + i, v * (1 - pan)); add(pR, a + i, v * (1 + pan)); } +} +// conga: a sine membrane with a pitch drop, tuned to the chord; slap = + noise +static void conga(double t, int midi, double g, double pan, int slap) { + long a = at(t); double f = mtof(midi), ph = 0, prev = 0; + for (long i = 0; i < 0.3 * SR; i++) { double u = (double)i / SR, nz = rnd(), hp = nz - prev; prev = nz; + ph += 2 * PI * f * (1 + 0.35 * exp(-u * 60)) / SR; + double v = sin(ph) * exp(-u * (slap ? 22 : 11)) + (slap ? hp * exp(-u * 80) * 0.8 : 0); + v *= g; add(pL, a + i, v * (1 - pan)); add(pR, a + i, v * (1 + pan)); } +} +// shaker tamb conga +static const double PERC[NSEC][3] = { + [INTRO] = { 0, 0, 0 }, [VERSE1] = { .5, 0, .6 }, [CHORUS1] = { .8, .8, .7 }, [VERSE2] = { .6, .4, .7 }, + [CHORUS2] = { 1, 1, .8 }, [BREAK] = { .4, 0, .5 }, [BRIDGE] = { .8, .7, .8 }, [OUTRO] = { .3, 0, .5 } }; + +// ── pop acoustic chain for her guitar (v5.1: "doesn't feel poppy") ─────── +// RBJ biquads: HP 110 Hz · −4 dB bell at 400 Hz (the box) · +4 dB shelf at +// 10 kHz (sparkle) → fast 4:1 compressor (snap + sustain) → M/S width 1.5. +typedef struct { double b0, b1, b2, a1, a2, x1, x2, y1, y2; } Biquad; +static Biquad bq(int type, double f, double q, double db) { + double A = pow(10, db / 40), w = 2 * PI * f / SR, c = cos(w), sn = sin(w), al = sn / (2 * q), b0, b1, b2, a0, a1, a2; + if (type == 0) { b0 = (1 + c) / 2; b1 = -(1 + c); b2 = b0; a0 = 1 + al; a1 = -2 * c; a2 = 1 - al; } // highpass + else if (type == 1) { b0 = 1 + al * A; b1 = -2 * c; b2 = 1 - al * A; a0 = 1 + al / A; a1 = -2 * c; a2 = 1 - al / A; } // bell + else { double sq = 2 * sqrt(A) * al; // high shelf + b0 = A * ((A + 1) + (A - 1) * c + sq); b1 = -2 * A * ((A - 1) + (A + 1) * c); b2 = A * ((A + 1) + (A - 1) * c - sq); + a0 = (A + 1) - (A - 1) * c + sq; a1 = 2 * ((A - 1) - (A + 1) * c); a2 = (A + 1) - (A - 1) * c - sq; } + Biquad b = { b0 / a0, b1 / a0, b2 / a0, a1 / a0, a2 / a0, 0, 0, 0, 0 }; + return b; +} +static double bq_run(Biquad *b, double x) { + double y = b->b0 * x + b->b1 * b->x1 + b->b2 * b->x2 - b->a1 * b->y1 - b->a2 * b->y2; + b->x2 = b->x1; b->x1 = x; b->y2 = b->y1; b->y1 = y; return y; +} +static void pop_guitar(Stereo *g, double width) { + if (!g->L) return; + for (int ch = 0; ch < 2; ch++) { + float *x = ch ? g->R : g->L; + Biquad hp = bq(0, 110, 0.707, 0), box = bq(1, 400, 1.0, -4), air = bq(2, 10000, 0.707, 4); + double env = 0, gc = 1, aA = exp(-1 / (0.003 * SR)), aR = exp(-1 / (0.08 * SR)); + double rms = 0; for (long i = 0; i < g->n; i++) rms += (double)x[i] * x[i]; + double th = sqrt(rms / g->n) * 1.2; + for (long i = 0; i < g->n; i++) { + double y = bq_run(&air, bq_run(&box, bq_run(&hp, x[i]))); + double lv = fabs(y); env = lv > env ? aA * env + (1 - aA) * lv : aR * env + (1 - aR) * lv; + double gt = env > th ? pow(env / th, 1.0 / 4 - 1) : 1; + gc += (gt - gc) * 0.02; + x[i] = (float)(y * gc * 1.8); + } + } + for (long i = 0; i < g->n; i++) { double m = (g->L[i] + g->R[i]) / 2, sd = (g->L[i] - g->R[i]) / 2 * width; + g->L[i] = (float)(m + sd); g->R[i] = (float)(m - sd); } +} + +int main(void) { + // ── her ── + Stereo vox = load_wav(LANE "/src/vox/locked/vocals-aesthetivox.wav"); + Stereo halo = load_wav(LANE "/src/vox/locked/vocals-halo.wav"); + Stereo gtr = load_wav(LANE "/src/vox/guitar-48k.wav"); + Stereo acg = load_wav(LANE "/src/vox/replay-acoustic.wav"); + Stereo elg = load_wav(LANE "/src/vox/replay-electric.wav"); + Stereo harm[NHARM]; + for (int h = 0; h < NHARM; h++) { char p[256]; snprintf(p, sizeof p, LANE "/src/vox/locked/%s.wav", HARM_FILE[h]); harm[h] = load_wav(p); } + if (!vox.L || !gtr.L) { fprintf(stderr, "! run bin/aesthetivox.py + bin/lock-vox.mjs first\n"); return 1; } + N = vox.n; + pL = zeros(); pR = zeros(); + pop_guitar(>r, 1.5); pop_guitar(&acg, 1.3); + dL = zeros(); dR = zeros(); tL = zeros(); tR = zeros(); b808 = zeros(); sL = zeros(); sR = zeros(); + hiM = zeros(); bellM = zeros(); vibM = zeros(); duck = zeros(); + EV = fopen(LANE "/out/sailor-song-v5.events.json", "w"); + if (EV) fprintf(EV, "{\"tempoBPM\":120,\"seconds\":%.2f,\"sections\":[", (double)N / SR); + if (EV) for (int s = 0; s < NSEC; s++) { + int a = SEC_FROM[s] - 1, z = (s + 1 < NSEC ? SEC_FROM[s + 1] : CHART_NBARS + 1) - 2; + fprintf(EV, "%s{\"name\":\"%s\",\"start\":%.3f,\"end\":%.3f}", s ? "," : "", SEC_NAME[s], CHART_BARS[a].t, CHART_BARS[z].t + CHART_BARS[z].dur); + } + if (EV) fprintf(EV, "],\"bars\":["); + if (EV) for (int k = 0; k < CHART_NBARS; k++) { static const char *CN[3] = { "G#m", "Emaj7", "B" }; + fprintf(EV, "%s{\"n\":%d,\"t\":%.3f,\"dur\":%.3f,\"chord\":\"%s\",\"section\":\"%s\"}", k ? "," : "", CHART_BARS[k].n, CHART_BARS[k].t, CHART_BARS[k].dur, CN[CHART_BARS[k].chord], SEC_NAME[section_of(CHART_BARS[k].n)]); } + if (EV) fprintf(EV, "],\"events\":[\n"); + + // ── score the bars ── + int tenor[3] = { 63, 66, 71 }, high[2] = { 80, 83 }, desc = 90; // v5.1: beds above her, not under + lead(tenor, 3, CHORD_PCS[0], 60, 72); + for (int k = 0; k < CHART_NBARS; k++) { + const ChartBar *b = &CHART_BARS[k]; + const int s = section_of(b->n); const Arr *o = &ARR[s]; + const double *bt = b->beats; const int nb = b->nb; const int *pcs = CHORD_PCS[b->chord]; + const double beatDur = b->dur / nb; + const double swell = s == BRIDGE ? 0.75 + 0.25 * (b->n - 72) / 8.0 : 1; + #define MID(j) (bt[j] + (bt[(j) + 1] - bt[j]) / 2) + // pocket on her strum points + const double *dz = DANCE[s]; + const int intro_in = s == INTRO && b->n < 7; // the kick builds in at bar 7 + if (dz[0] && !intro_in) for (int j = 0; j < nb; j++) { double g = 0.95 * dz[0] * (s == INTRO ? 0.6 + 0.1 * (b->n - 7) : 1); + dance_kick(bt[j], g); ev(bt[j], "kick", 0.1, g, -1); } + if (dz[1]) for (int j = 1; j < nb; j += 2) { clap(hum_t(bt[j]), hum_g(0.55 * dz[1] * swell)); ev(bt[j], "clap", 0.1, dz[1], -1); } + if (dz[2]) for (int j = 0; j < nb; j++) { open_hat(hum_t(MID(j)), hum_g(0.5 * dz[2] * swell)); ev(MID(j), "hat", 0.1, dz[2], -1); } + if (dz[3]) for (int j = 0; j < nb; j++) { int r = ROOT[b->chord] - 12 + (j == nb - 1 && b->n % 2 ? 7 : 0); + house_bass(MID(j), r, beatDur * 0.42, 0.55 * dz[3]); ev(MID(j), "bass", beatDur * 0.42, dz[3], r); } + // risers: into each chorus + out of the break; a snare roll into the bridge's last bars + if (b->n == 26 || b->n == 50 || b->n == 70) riser(b->t, b->t + b->dur + CHART_BARS[k + 1].dur, 0.5); + if (s == BRIDGE && b->n >= 79) for (int j = 0; j < nb; j++) for (int q = 0; q < (b->n == 80 ? 4 : 2); q++) { + int dv = b->n == 80 ? 4 : 2; double t = bt[j] + (bt[j + 1] - bt[j]) * q / dv; snare(t, 0.35 + 0.3 * (j * dv + q) / (nb * dv)); } + if (o->kick && !dz[0]) { kick(bt[0], 0.95 * o->kick); ev(bt[0], "kick", 0.1, o->kick, -1); + if (nb > 2) { kick(MID(1), 0.6 * o->kick); ev(MID(1), "kick", 0.1, o->kick * 0.6, -1); } } + if (o->snare && nb > 2 && !dz[1]) { snare(bt[2], 0.6 * o->snare * swell); ev(bt[2], "snare", 0.1, o->snare, -1); } + if (o->clap && nb > 2 && !dz[1]) { clap(bt[2], 0.5 * o->clap * swell); ev(bt[2], "clap", 0.1, o->clap, -1); } + if (o->rim && nb > 3) { rim(MID(3), 0.22 * o->rim); ev(MID(3), "rim", 0.05, o->rim, -1); } + if (o->hat) for (int j = 0; j < nb; j++) { soft_hat(bt[j], 0.15 * o->hat, -0.35); soft_hat(MID(j), 0.09 * o->hat, 0.35); } + { // hand percussion on her grid: shaker 16ths, tambourine backbeat, congas on her &2 / &4 + const double *pc = PERC[s]; + if (pc[0]) for (int j = 0; j < nb; j++) for (int q = 0; q < 4; q++) { + double t = bt[j] + (bt[j + 1] - bt[j]) * q / 4, acc = q == 2 ? 1 : (q ? 0.55 : 0.75); + shaker(hum_t(t), hum_g(0.35 * pc[0] * acc * swell), 0.55); ev(t, "shaker", 0.03, pc[0] * acc, -1); } + if (pc[1] && nb > 2) { tamb(bt[2], 0.30 * pc[1] * swell, -0.4); ev(bt[2], "tamb", 0.1, pc[1], -1); + if (nb > 3) tamb(MID(3), 0.14 * pc[1], -0.4); } + if (pc[2]) { int lo = ROOT[b->chord] + 12, hi = lo + 7; // tuned to her root / fifth + if (nb > 1) { conga(hum_t(MID(1)), hi, hum_g(0.30 * pc[2]), 0.35, 0); ev(MID(1), "conga", 0.1, pc[2], hi); } + if (nb > 3) { conga(hum_t(bt[3]), lo, hum_g(0.34 * pc[2]), 0.25, 0); conga(hum_t(MID(3)), hi, hum_g(0.26 * pc[2]), 0.35, 1); ev(bt[3], "conga", 0.1, pc[2], lo); } } + } + // 808 on the kick points, gliding into her root + if (o->b808 && !dz[3]) { int r = ROOT[b->chord] - 12; + eight08(bt[0], r, beatDur * 1.4, 0.8 * o->b808); ev(bt[0], "808", beatDur * 1.4, o->b808, r); + if (nb > 2) { eight08(MID(1), r, beatDur * 1.3, 0.6 * o->b808); ev(MID(1), "808", beatDur * 1.3, o->b808 * 0.6, r); } } + // trap hats: 16ths, accents on the beat, a roll into every other bar line + if (o->trap) { + int roll = b->n % 2 == 0, trip = b->n % 4 == 0; + for (int j = 0; j < nb; j++) { + int last = j == nb - 1 && roll, div = last ? (trip ? 6 : 8) : 4; + for (int q = 0; q < div; q++) { + double t = bt[j] + (bt[j + 1] - bt[j]) * q / div; + double acc = q == 0 ? 1 : (q % 2 ? 0.55 : 0.75); + double g = 0.5 * o->trap * acc * (last ? 0.6 + 0.4 * q / div : 1) * swell; + trap_hat(t, g, q % 2 ? 0.45 : -0.25); ev(t, "hat", 0.02, g, -1); + } + } + } + // sine choir + const double dur = b->dur * 0.97; + if (o->sub) { sine(sL, sR, b->t, dur, ROOT[b->chord] - 12, 0.18 * o->sub, 0, 0.35, 0.9, 0); + sine(sL, sR, b->t, dur, ROOT[b->chord], 0.12 * o->sub, 0, 0.35, 0.9, 1); ev(b->t, "sub", dur, o->sub, ROOT[b->chord] - 12); } + lead(tenor, 3, pcs, 60, 72); + if (o->tenor) for (int v = 0; v < 3; v++) { PAD(b->t, dur, tenor[v], 0.10 * o->tenor * swell, (v - 1) * 0.6); ev(b->t, "pad", dur, o->tenor, tenor[v]); } + lead(high, 2, pcs, 76, 88); + if (o->high) for (int v = 0; v < 2; v++) { sine(hiM, NULL, b->t, dur, high[v], 0.05 * o->high * swell, 0, 0.8, 1.4, 5); ev(b->t, "high", dur, o->high, high[v]); } + if (o->descant) { // one long high chord tone per bar, leaning down by step + int c = -1; for (int d = -1; d >= -2 && c < 0; d--) { int m = nearest(pcs, 3, desc + d, 83, 93); if (m <= desc) c = m; } + if (c < 0) c = nearest(pcs, 3, desc, 83, 93); + desc = c <= 84 ? 92 : c; + sine(hiM, NULL, b->t, dur, c, 0.04 * o->descant, 0, 1.0, 1.8, 4); ev(b->t, "descant", dur, o->descant, c); + } + // vibraphone arpeggio on her 8ths (16ths in the last bridge bars) + if (o->vib) { + int tones[6]; for (int j = 0; j < 6; j++) tones[j] = nearest(pcs, 3, 64 + j * 3, 64, 81); + int dens = (s == BRIDGE && b->n >= 77) ? 4 : 2, c = 0; + for (int j = 0; j < nb; j++) for (int q = 0; q < dens; q++) + vib(hum_t(bt[j] + (bt[j + 1] - bt[j]) * q / dens), tones[c++ % 6], hum_g(0.10 * o->vib * swell), beatDur / dens * 1.5); + } + // a fem bell on every other downbeat where the trap plays + if (o->bell && o->trap && b->n % 2 == 0) fem_bell(bt[0], nearest(pcs, 3, 72, 68, 76), 0.18 * o->bell); + ev(b->t, "gtr", dur, o->her, -1); + } + // her melody, shadowed a 3rd up and an octave up + for (int k = 0; k < CHART_NNOTES; k++) { + const ChartNote *v = &CHART_NOTES[k]; const ChartBar *b = bar_at(v->t); + if (!b || v->dur < 0.18) continue; + const Arr *o = &ARR[section_of(b->n)]; + ev(v->t, "vox", v->dur, 1, v->midi); + if (o->third) sine(sL, sR, v->t, v->dur, third_above(v->midi), 0.06 * o->third, 0.3, 0.06, 0.35, 2); + if (o->octave) sine(hiM, NULL, v->t, v->dur, v->midi + 12, 0.03 * o->octave, 0, 0.08, 0.5, 4); + } + // fem bell answers: in every breath > 1 s her last three notes ring back + for (int k = 1; k < CHART_NNOTES; k++) { + const ChartNote *p = &CHART_NOTES[k - 1]; double end = p->t + p->dur, gap = CHART_NOTES[k].t - end; + const ChartBar *b = bar_at(end); + if (gap < 1.0 || !b) continue; + const Arr *o = &ARR[section_of(b->n)]; + if (!o->bell) continue; + double e8 = (b->beats[1] - b->beats[0]) / 2; + for (int j = 0, from = k >= 3 ? k - 3 : 0; from + j < k; j++) { + double t = end + 0.12 + j * e8; + if (t < CHART_NOTES[k].t - 0.1) fem_bell(hum_t(t), CHART_NOTES[from + j].midi + 12, hum_g(0.16 * o->bell)); + } + } + // the hook on sines over the guitar break: chorus 1 bars 28–31 re-laid on 68–71 + for (int k = 0; k < CHART_NNOTES; k++) { + const ChartNote *v = &CHART_NOTES[k]; const ChartBar *sb = bar_at(v->t); + if (!sb || sb->n < 28 || sb->n > 31) continue; + const ChartBar *db = &CHART_BARS[sb->n + 40 - 1]; + double t = db->t + (v->t - sb->t) / sb->dur * db->dur, d = v->dur * db->dur / sb->dur; + sine(sL, sR, t, d, v->midi, 0.09, 0, 0.05, 0.6, 3); + sine(hiM, NULL, t, d, v->midi + 12, 0.035, 0, 0.08, 0.8, 5); + ev(t, "hook", d, 1, v->midi); + } + + // ── space ── + float *spL = zeros(), *spR = zeros(); + spatialize(bellM, NULL, orbit_az, orbit_el, 1.2, spL, spR, 0.9); // bells orbit overhead + spatialize(vibM, NULL, sweep_az, el_zero, 1.2, spL, spR, 0.8); // vibes sweep ear to ear + float *hiL = zeros(), *hiR = zeros(); + spatialize(hiM, NULL, wide_az, el_up, 1.2, hiL, hiR, 1.3); // highs float up + wide + float *hL = zeros(), *hR = zeros(); + for (int h = 0; h < NHARM; h++) { + if (!harm[h].L) continue; + float *g = automate(v_harm_g, h), *az = automate(v_harm_az, h); + int any = 0; for (long i = 0; i < N; i += 480) if (g[i] > 0.002) { any = 1; break; } + if (any) { + float *m = zeros(); long d = lround(HARM_DELAY[h] * SR); + for (long i = 0; i < N; i++) m[i] = sample(harm[h].L, harm[h].n, i - d) * g[i] * 0.6f; + spatialize(m, az, NULL, el_near, 1.0, hL, hR, 1.0); + free(m); + } + free(g); free(az); + } + + // ── mix ── + float *sendA = automate(v_send, 0), *airA = automate(v_air, 0); + float *herA = automate(v_her, 0), *acgA = automate(v_acg, 0), *elgA = automate(v_elg, 0); + float *L = zeros(), *R = zeros(), *xL = zeros(), *xR = zeros(); + double env = 0, gcur = 1, e = 0, vlp = 0, dlp = 0, drp = 0; + const double aA = exp(-1 / (0.01 * SR)), aR = exp(-1 / (0.2 * SR)); + const double k7 = 1 - exp(-2 * PI * 7000 / SR); + for (long i = 0; i < N; i++) { + // sidechain: kick + 808 pump the sines and the replay guitars + e = fmax(duck[i], e * 0.99985); double pump = 1 - 0.6 * fmin(1, e); // v5.2: dance pump + // vocal leveler: RMS 3:1 over threshold, 10 ms / 200 ms + double x = vox.L[i], pw = x * x; + env = pw > env ? aA * env + (1 - aA) * pw : aR * env + (1 - aR) * pw; + double lv = sqrt(env), gt = lv > 0.08 ? pow(lv / 0.08, 1.0 / 3 - 1) : 1; + gcur = gt < gcur ? gt : gcur + (gt - gcur) * 0.0005; + double v0 = x * gcur * 1.5; vlp += (v0 - vlp) * k7; + double v = v0 + (v0 - vlp) * airA[i] * 1.6; // air above 7 kHz + double hv = sample(halo.L, halo.n, i) * 0.13; + dlp = dL[i]; drp = dR[i]; // v5.1: kit full-band + forward (was 4.5 kHz lowpass) + double sl = tanh(sL[i] * 1.6 * 1.4) / tanh(1.4) * pump, sr = tanh(sR[i] * 1.6 * 1.4) / tanh(1.4) * pump; + double gl = sample(gtr.L, gtr.n, i) * herA[i] * 0.8 + (sample(acg.L, acg.n, i) * acgA[i] + sample(elg.L, elg.n, i) * elgA[i]) * 0.55 * pump; + double gr = sample(gtr.R, gtr.n, i) * herA[i] * 0.8 + (sample(acg.R, acg.n, i) * acgA[i] + sample(elg.R, elg.n, i) * elgA[i]) * 0.55 * pump; + double pl = spL[i] + hiL[i] * pump, pr = spR[i] + hiR[i] * pump; + L[i] = (float)(v + hL[i] + hv * 0.6 + gl + dlp * 1.1 + tL[i] * 1.1 + pL[i] * 0.9 + b808[i] * 0.42 + sl * 0.8 + pl); + R[i] = (float)(v + hR[i] + hv * 0.6 + gr + drp * 1.1 + tR[i] * 1.1 + pR[i] * 0.9 + b808[i] * 0.42 + sr * 0.8 + pr); + xL[i] = (float)(v * sendA[i] + hL[i] * 0.3 + hv + sl * 0.35 + pl * 0.5 + dlp * 0.05 + gl * 0.06); + xR[i] = (float)(v * sendA[i] + hR[i] * 0.3 + hv + sr * 0.35 + pr * 0.5 + drp * 0.05 + gr * 0.06); + } + // the room: near early reflections always on, a 2.2 s dark tail behind + float *wL = zeros(), *wR = zeros(); + fdn(xL, xR, wL, wR, 2.2, 0.4, 0.025); + static const double ROOM[6][3] = { { 0.011, 0.16, 0 }, { 0.017, 0.14, 1 }, { 0.023, 0.11, 0 }, { 0.031, 0.10, 1 }, { 0.043, 0.07, 0 }, { 0.053, 0.06, 1 } }; + for (long i = N - 1; i >= 0; i--) { + double el = 0, er = 0; + for (int r = 0; r < 6; r++) { long j = i - lround(ROOM[r][0] * SR); if (j >= 0) { double y = (xL[j] + xR[j]) * ROOM[r][1]; if (ROOM[r][2] > 0) er += y; else el += y; } } + L[i] += (float)(wL[i] * 0.4 + el); R[i] += (float)(wR[i] * 0.4 + er); + } + // bass mono below 120 Hz (house rule) + { double ml = 0, mr = 0, k = 1 - exp(-2 * PI * 120 / SR); + for (long i = 0; i < N; i++) { ml += (L[i] - ml) * k; mr += (R[i] - mr) * k; double m = (ml + mr) / 2; L[i] += (float)(m - ml); R[i] += (float)(m - mr); } } + // raised-cosine top and tail, one linear trim (cut.sh masters) + long fade = SR; + for (long i = 0; i < fade; i++) { double g = 0.5 - 0.5 * cos(PI * i / fade); L[i] *= (float)g; R[i] *= (float)g; L[N - 1 - i] *= (float)g; R[N - 1 - i] *= (float)g; } + double peak = 0; for (long i = 0; i < N; i++) { peak = fmax(peak, fabs(L[i])); peak = fmax(peak, fabs(R[i])); } + for (long i = 0; i < N; i++) { L[i] *= (float)(0.7 / peak); R[i] *= (float)(0.7 / peak); } + write_wav_f32(LANE "/out/sailor-song-v5-full.wav", L, R, N); + if (EV) { fprintf(EV, "\n]}\n"); fclose(EV); } + printf("✓ %s/out/sailor-song-v5-full.wav %.1f s, %d bars, %d notes\n", LANE, (double)N / SR, CHART_NBARS, CHART_NNOTES); + return 0; +} diff --git a/pop/sailor-song/c/sailorremix-v7.c b/pop/sailor-song/c/sailorremix-v7.c new file mode 100644 index 0000000000..ad5c1dccc6 --- /dev/null +++ b/pop/sailor-song/c/sailorremix-v7.c @@ -0,0 +1,946 @@ +// sailorremix.c — "sailor song (outside remix)", the C engine. +// +// A cover of Gigi Perez's "Sailor Song", played by @jeffrey's niece: her +// voice and a capo-4 nylon guitar on one phone mic. This file is the score. +// It follows the /pop single-file renderer shape (pop/loner/c, +// pop/cult/c): the measuring side bakes, this engine arranges and mixes. +// +// THE DEAL bin/aesthetivox.py owns WORLD (her lead regulated onto G# +// minor in her guitar's +13¢ frame, a dark octave halo, five +// self-harmony stems); bin/lock-vox.mjs pulls every onset onto +// her own strum 8ths; bin/replay-guitar.mjs re-plays her part +// bar for bar on pop/guitar/c/strum; bin/bells.sh bakes a FEM +// bell bank (pop/bell/c); bin/chart.mjs emits sailor-chart.h — +// her bars, her snapped beats, her chords, her tuned notes. +// FOLLOW Nothing is warped to a grid. Every hit lands on HER strum +// points (her motif X..XX..X: 1, &2, 3, &4). The clock breathes +// from 110 to 125 because she does. +// POCKET kick 1 + &2, brush snare 3, rim &4 — her own strum points. +// TRAP a gliding 808 on the kick points tuned to her roots; bright +// 16th hats with 32nd / triplet rolls into every other bar +// line; a layered clap on 3. +// SINES voice-led sine choir — sub on the roots, a tenor trio under +// her, a high pair over her, her register (G#3–G#4) left clear +// — plus a descant, her melody shadowed a 3rd and an octave up, +// and her chorus hook re-laid over the guitar-only break. +// Saturated and pumped by the kick so they hold in a pop mix. +// AC FEM bells answer her in the breath gaps (her last three +// notes ring back); a vibraphone (pop/marimba's modal preset, +// ported) arpeggiates her chords on her 8ths. +// SPACE pop/nullabye/c/ac_hrtf.h: bells orbit overhead, the vibes +// sweep ear to ear, the highs float up and wide, and her +// harmonies stand at fixed places — in the bridge they rove. +// ROOM near early reflections always on, a 2.2 s dark FDN behind. +// Verses close and bright (air above 7 kHz), choruses roomier. +// +// Form (bars from sailor-chart.h, 2:59): +// intro 1–10 · verse1 11–27 · chorus1 28–43 · verse2 44–51 · +// chorus2 52–67 · break 68–71 · bridge 72–80 · outro 81–84 +// +// v6 (ANALYSIS.md, the platter reading of her take): +// START bedroom first. The record opens on her first strum (1.65 s), +// her guitar and then her voice alone, dry and close — nothing +// says electronic. From bar 16 the mix grows in one lane at a +// time (chamber-03 rule 1: one change per phrase): sub 16, pads +// + shaker 20, congas 22, a kick on her 1 and 3 only at 24, the +// pickup bass 24, hats 25, riser 26, and the floor lands whole +// with an explosion on chorus 1 (bar 28). +// CLOCK bin/regularize.mjs: her rubato is kept through the bedroom +// bars; from bar 24 the clock keeps 30% of her sway around +// 120 (verse 2 119, choruses 120–121, outro 122 instead of 130). +// Every stem and the chart go through the same time map. +// HOCKET her hand is the periodic necklace X..XX..X (1 &2 3 &4, LHL 0). +// The kit now plays her COMPLEMENT .XX..XX. (&1 2 &3 4): open hats +// on &1/&3, claps on 2/4, so every 8th is struck exactly once +// between her and the kit (rhythm-platter 06); the floor kick +// doubles her 1 and 3. Her pickups &2/&4 get only a soft accent +// and the house bass — the rim that fought her anacrusis is gone +// where she sings. +// BINS off-beat hits sit at her measured bin, +5 ms (rhythm 07): the +// necklace says which pulse, BIN_OFF says where in it. +// IMPACT chamber-04 rule 10: at every drop the whole ring is displaced +// and springs back (1.1 Hz, damping 1.2); kick and sub never +// move; an explosion (a noise chiff hurled once around the head) +// opens it. +// VOX ARPS her own harmony stems gated into arpeggios on her 8ths/16ths/ +// triplets, each step at its own seat — a different figure per +// section (down3 is the resident interval, up5 the rare one: +// chord-tone shares .60 / .35 against her chords). +// +// Build: bash pop/sailor-song/c/build.sh +// Run: pop/sailor-song/c/sailorremix (from the repo root) +// → pop/sailor-song/out/sailor-song-v5-full.wav + .events.json +// bash pop/sailor-song/c/cut.sh → master (−10 LUFS) + mp3 w/ cover + +#include +#include +#include +#include +#include + +#include "../../nullabye/c/ac_hrtf.h" +#include "sailor-chart.h" + +#define SR 48000 +#define VERSION "v7" +#define START_BAR 11 // v6.6: (the pre-roll is now her first WORD; see main) +#define FLOOR_BAR 28 // v7: the floor lands on "kiss" +#define BIN_OFF 0.005 // v6: her off-beat bin, measured (ANALYSIS.md §2) +#define LANE "pop/sailor-song" +#define PI 3.14159265358979323846 + +// ── rng (seeded, so the same score renders the same bytes) ────────────── +static uint32_t rng_state = 0x5a11025u; +static double rnd(void) { // −1..1 + rng_state ^= rng_state << 13; rng_state ^= rng_state >> 17; rng_state ^= rng_state << 5; + return (double)rng_state / 2147483648.0 - 1.0; +} + +// ── WAV IO (the fleet loader, loner/cult's, stereo) ────────────────────── +typedef struct { float *L, *R; long n; } Stereo; +static Stereo load_wav(const char *path) { + Stereo s = { 0 }; + FILE *f = fopen(path, "rb"); + if (!f) { fprintf(stderr, " ! missing %s\n", path); return s; } + fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET); + uint8_t *buf = (uint8_t *)malloc(sz); + if (!buf || (long)fread(buf, 1, sz, f) != sz) { fclose(f); free(buf); return s; } + fclose(f); + long p = 12; int fmt = 1, ch = 1, bits = 16; long dOff = 0, dLen = 0; + while (p + 8 <= sz) { + uint32_t c = (uint32_t)buf[p+4] | (uint32_t)buf[p+5] << 8 | (uint32_t)buf[p+6] << 16 | (uint32_t)buf[p+7] << 24; + if (!memcmp(buf + p, "fmt ", 4)) { fmt = buf[p+8] | (buf[p+9] << 8); ch = buf[p+10] | (buf[p+11] << 8); bits = buf[p+22] | (buf[p+23] << 8); } + else if (!memcmp(buf + p, "data", 4)) { dOff = p + 8; dLen = c; if (dOff + dLen > sz) dLen = sz - dOff; break; } + p += 8 + c + (c & 1); + } + const int bps = bits / 8; const long frames = dLen / (bps * ch); + s.L = (float *)calloc(frames, sizeof(float)); s.R = (float *)calloc(frames, sizeof(float)); s.n = frames; + for (long i = 0; i < frames; i++) for (int c = 0; c < 2; c++) { + const long o = dOff + (i * ch + (c < ch ? c : 0)) * bps; double v; + if (fmt == 3 && bits == 32) { float x; memcpy(&x, buf + o, 4); v = x; } + else if (bits == 32) { int32_t x; memcpy(&x, buf + o, 4); v = x / 2147483648.0; } + else if (bits == 24) { int32_t x = buf[o] | (buf[o+1] << 8) | ((int32_t)(int8_t)buf[o+2] << 16); v = x / 8388608.0; } + else { int16_t x; memcpy(&x, buf + o, 2); v = x / 32768.0; } + (c ? s.R : s.L)[i] = (float)v; + } + free(buf); + return s; +} +static void write_wav_f32(const char *path, const float *L, const float *R, long n) { + FILE *f = fopen(path, "wb"); + if (!f) { fprintf(stderr, "! cannot write %s\n", path); exit(1); } + uint32_t dsz = (uint32_t)(n * 8), riff = 36 + dsz, sr = SR, br = SR * 8, fsz = 16; + uint16_t fmt = 3, ch = 2, ba = 8, bits = 32; + fwrite("RIFF", 1, 4, f); fwrite(&riff, 4, 1, f); fwrite("WAVE", 1, 4, f); + fwrite("fmt ", 1, 4, f); fwrite(&fsz, 4, 1, f); + fwrite(&fmt, 2, 1, f); fwrite(&ch, 2, 1, f); fwrite(&sr, 4, 1, f); + fwrite(&br, 4, 1, f); fwrite(&ba, 2, 1, f); fwrite(&bits, 2, 1, f); + fwrite("data", 1, 4, f); fwrite(&dsz, 4, 1, f); + for (long i = 0; i < n; i++) { fwrite(&L[i], 4, 1, f); fwrite(&R[i], 4, 1, f); } + fclose(f); +} +static float sample(const float *b, long len, long i) { return (b && i >= 0 && i < len) ? b[i] : 0.0f; } + +// ── the form + the arrangement ────────────────────────────────────────── +enum { INTRO, VERSE1, CHORUS1, VERSE2, CHORUS2, BREAK, BRIDGE, OUTRO, NSEC }; +static const char *SEC_NAME[NSEC] = { "intro", "verse1", "chorus1", "verse2", "chorus2", "break", "bridge", "outro" }; +static const int SEC_FROM[NSEC] = { 1, 11, 28, 44, 52, 68, 73, 81 }; // v7: "kiss me on the mouth" lands on the downbeat of 28 and 52; the bridge line on 73 +static int section_of(int bar) { int s = 0; for (int k = 0; k < NSEC; k++) if (bar >= SEC_FROM[k]) s = k; return s; } + +// her own harmonies: which stem sings, how loud, where (azimuth °) +enum { H_UP3, H_DOWN3, H_UP5, H_DOWN6, H_DOWN8, H_UP8, NHARM }; +static const char *HARM_FILE[NHARM] = { "harm-up3", "harm-down3", "harm-up5", "harm-down6", "harm-down8", "harm-up8" }; +static const double HARM_DELAY[NHARM] = { 0.018, 0.026, 0.022, 0.030, 0.012, 0.008 }; // a breath apart +typedef struct { double g, az; } HarmSet; + +typedef struct { + double her, acg, elg; // her guitar / acoustic replay / electric replay + double kick, snare, clap, rim, hat, trap, b808; + double sub, tenor, high, third, octave, descant, hook, vib, bell; + HarmSet harm[NHARM]; + double send, air; // vocal room send, air lift +} Arr; +#define H(i, g, az) [i] = { g, az } +static const Arr ARR[NSEC] = { + [INTRO] = { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // v6: bedroom — her guitar alone + { { 0 } }, .10, .3 }, + [VERSE1] = { .95, .15, 0, 0, .4, 0, 0, 0, 0, 0, .8, .7, 0, 0, 0, 0, 0, 0, .9, // v6: end state of the evolution (gated per bar below) + { H(H_DOWN3, .16, 40) }, .07, .45 }, + [CHORUS1] = { .45, .55, .6, .9, .8, .6, 0, 0, .35, .8, 1, .8, .7, .2, 0, 0, 0, .5, .6, + { H(H_UP3, .30, -50), H(H_DOWN3, .26, 50), H(H_DOWN6, .18, 0) }, .14, .3 }, + [VERSE2] = { .8, .2, .2, .8, .5, 0, 0, .3, 0, .4, .7, .5, 0, 0, 0, 0, 0, .3, .9, // v6.6: a verse, not a chorus + { H(H_DOWN3, .24, 45) }, .08, .45 }, + [CHORUS2] = { .4, .6, .85, 1, .9, .8, 0, 0, .5, 1, 1, .8, .9, .2, .2, 1, 0, .6, .5, + { H(H_UP3, .32, -60), H(H_DOWN3, .30, 60), H(H_DOWN6, .22, -20), H(H_DOWN8, .20, 0) }, .15, .35 }, + [BREAK] = { .9, 0, 0, 0, 0, 0, 0, 0, 0, 0, .8, 1, 1, 0, 0, .6, 1, .6, .8, + { { 0 } }, .20, .2 }, + [BRIDGE] = { .5, .5, .75, .8, .7, .5, 0, 0, .4, .8, 1, .8, .9, 0, .2, .5, 0, .7, .6, + { { 0 } }, .09, .5 }, // harmonies rove, see harm_at() + [OUTRO] = { .9, 0, .4, .5, 0, 0, .4, 0, 0, .3, .8, .8, .6, 0, 0, 0, 0, .4, 1, + { H(H_DOWN8, .30, 0), H(H_DOWN3, .22, 40), H(H_UP3, .16, -40) }, .20, .15 }, +}; +// "pitching around": in the bridge her harmony changes interval and place each bar +static double evo(int bar, int from, int to); +static HarmSet harm_at(int bar, int h) { + int s = section_of(bar); + if (s == VERSE1) { HarmSet r = ARR[s].harm[h]; r.g *= evo(bar, 18, 24); return r; } // v6: her harmony creeps in + if (h == H_UP8) { // v6.2: her voice an octave up + if (bar >= 41 && bar <= 43) return (HarmSet){ .70, 0 }; // end of chorus 1 + if (bar >= 60 && bar <= 67) return (HarmSet){ 1.0, 0 }; // second half of chorus 2 + if (bar >= 77 && bar <= 80) return (HarmSet){ 1.0, 0 }; // the bridge's climb + if (bar >= 81) return (HarmSet){ .6, 0 }; + return (HarmSet){ 0, 0 }; + } + if (s != BRIDGE) return ARR[s].harm[h]; + static const HarmSet ROVE[4][NHARM] = { + { H(H_UP3, .36, -60) }, { H(H_UP5, .30, 60) }, { H(H_DOWN3, .34, -35) }, { H(H_UP3, .30, 45), H(H_DOWN6, .24, -50) } }; + return ROVE[(bar - 73) % 4][h]; +} + +// ── buses ──────────────────────────────────────────────────────────────── +static long N; +static float *dL, *dR, *tL, *tR, *b808, *sL, *sR, *hiM, *bellM, *vibM, *duck; +static void add(float *b, long i, double v) { if (i >= 0 && i < N) b[i] += (float)v; } +static long at(double t) { return lround(t * SR); } +static double mtof(double m) { return 440.0 * pow(2.0, (m + CHART_TUNE - 69.0) / 12.0); } + +// score events → out/…events.json for the score video +static FILE *EV; +static int evFirst = 1; +static void ev(double t, const char *voice, double dur, double gain, int midi) { + if (!EV) return; + fprintf(EV, "%s{\"t\":%.3f,\"voice\":\"%s\",\"dur\":%.3f,\"gain\":%.2f%s", evFirst ? "" : ",\n", t, voice, dur, gain, ""); + if (midi >= 0) fprintf(EV, ",\"midi\":%d", midi); + fputc('}', EV); evFirst = 0; +} + +// ── the kit ────────────────────────────────────────────────────────────── +static void kick(double t, double g) { + long a = at(t); double ph = 0; + for (long i = 0; i < 0.36 * SR; i++) { double u = (double)i / SR; + ph += 2 * PI * (48 + 110 * exp(-u * 45)) / SR; // v5.1: a real pop kick — + double click = i < 0.004 * SR ? rnd() * 0.5 * (1 - u / 0.004) : 0; // beater click + double v = (tanh(sin(ph) * 1.6) * exp(-u * 6.5) + click) * g; add(dL, a + i, v); add(dR, a + i, v); } + for (long i = 0; i < 0.25 * SR; i++) add(duck, a + i, g * exp(-(double)i / SR * 9)); +} +static void snare(double t, double g) { // brushy: slow swell-in, dark noise, soft body + long a = at(t); double prev = 0, lp = 0, ph = 0; + for (long i = 0; i < 0.24 * SR; i++) { double u = (double)i / SR, nz = rnd(), hp = nz - prev; prev = nz; + lp += (hp - lp) * 0.45; ph += 2 * PI * (200 + 60 * exp(-u * 60)) / SR; // v5.1: crisp, not brushed + double v = (lp * exp(-u * 17) * 1.4 + sin(ph) * 0.55 * exp(-u * 26)) * g; + add(dL, a + i, v * 0.92); add(dR, a + i, v); } +} +static void rim(double t, double g) { + long a = at(t); + for (long i = 0; i < 0.07 * SR; i++) { double u = (double)i / SR; + double v = (sin(2 * PI * 1650 * u) * 0.5 + sin(2 * PI * 520 * u)) * exp(-u * 70) * g; + add(dL, a + i, v * 1.1); add(dR, a + i, v * 0.8); } +} +static void soft_hat(double t, double g, double pan) { + long a = at(t); double prev = 0; + for (long i = 0; i < 0.03 * SR; i++) { double nz = rnd(), hp = nz - prev; prev = nz; + double v = hp * exp(-(double)i / SR * 300) * g * 0.6; add(dL, a + i, v * (1 - pan)); add(dR, a + i, v * (1 + pan)); } +} +static void trap_hat(double t, double g, double pan) { // two-stage highpass: brighter, tighter + long a = at(t); double p1 = 0, p2 = 0; + for (long i = 0; i < 0.022 * SR; i++) { double nz = rnd(), h1 = nz - p1; p1 = nz; double h2 = h1 - p2; p2 = h1; + double v = h2 * exp(-(double)i / SR * 220) * g * 0.35; add(tL, a + i, v * (1 - pan)); add(tR, a + i, v * (1 + pan)); } +} +static void clap(double t, double g) { + static const double OFF[3] = { 0, 0.011, 0.022 }, AMP[3] = { 0.7, 0.8, 1.0 }; + for (int k = 0; k < 3; k++) { + long a = at(t + OFF[k]); double lp = 0, prev = 0; + for (long i = 0; i < 0.16 * SR; i++) { double u = (double)i / SR, nz = rnd(), hp = nz - prev; prev = nz; + lp += (hp - lp) * 0.45; + double v = lp * exp(-u * (k < 2 ? 90 : 16)) * g * AMP[k] * 0.9; add(tL, a + i, v); add(tR, a + i, v * 0.95); } + } +} +// 808: drops ~a 4th into the root, long tail, tanh so its harmonics speak +// above 180 Hz on a phone +static void eight08(double t, int midi, double dur, double g) { + long a = at(t); double f1 = mtof(midi), f0 = f1 * 1.33, ph = 0; + for (long i = 0; i < (dur + 0.15) * SR; i++) { double u = (double)i / SR; + ph += 2 * PI * (f1 + (f0 - f1) * exp(-u * 26)) / SR; + double env = fmin(1, u / 0.002) * (u > dur ? exp(-(u - dur) * 30) : exp(-u * 1.1)); + add(b808, a + i, tanh(sin(ph) * 3.4) * 0.7 * env * g); /* v5.1: drive 3.4 so harmonics carry it on phones */ } + for (long i = 0; i < 0.3 * SR; i++) add(duck, a + i, g * exp(-(double)i / SR * 7)); +} + +// ── sines ──────────────────────────────────────────────────────────────── +// slow swell, 4.5 Hz tremolo, a ±det¢ twin. R == NULL → mono (to a spatial bus) +static void sine(float *L, float *R, double t0, double dur, double midi, double g, double pan, + double atk, double rel, double det) { + double f = mtof(midi), fa = f * pow(2, det / 1200), fb = f * pow(2, -det / 1200); + double pa = (rnd() + 1) * 3, pb = (rnd() + 1) * 3, pc_ = 0, pa0 = pa, pb0 = pb; + double gl = g * sqrt((1 - pan) / 2), gr = g * sqrt((1 + pan) / 2); + long a = at(t0), len = (long)((dur + rel) * SR); + for (long i = 0; i < len; i++) { + double u = (double)i / SR; + double env = fmin(1, u / atk) * (u > dur ? exp(-(u - dur) / (rel / 3)) : 1); + // analog: each voice drifts on its own slow wander; a centre third voice; soft saturation + double drift = det > 0 ? 1 + 0.0012 * sin(2 * PI * 0.13 * u + pa0) + 0.0007 * sin(2 * PI * 0.31 * u + pb0) : 1; + pa += 2 * PI * fa * drift / SR; pb += 2 * PI * fb / drift / SR; pc_ += 2 * PI * f * drift / SR; + double trem = 1 - 0.08 * (0.5 + 0.5 * sin(2 * PI * 4.5 * u)); + double c3 = det > 0 ? sin(pc_) * 0.5 : 0, xa = tanh((sin(pa) + c3) * 2.2) / 1.0, xb = tanh((sin(pb) + c3) * 2.2) / 1.0; // v6.2: power sines + if (!R) add(L, a + i, (xa + xb) * 0.5 * env * trem * g); + else { add(L, a + i, xa * env * trem * gl); add(R, a + i, xb * env * trem * gr); } + } +} +#define PAD(t, d, m, g, pan) sine(sL, sR, t, d, m, g, pan, 0.35, 0.9, 7) + +// ── AC voices ──────────────────────────────────────────────────────────── +// FEM bells from the baked bank (bin/bells.sh). Loner rule: ≤ E5, fade tails. +static Stereo BELLS[128]; +static void fem_bell(double t, int midi, double g) { + while (midi > 76) midi -= 12; + while (midi < 63) midi += 12; + if (!BELLS[midi].L) { + char p[256]; snprintf(p, sizeof p, LANE "/src/bells/bell-%d.wav", midi); + BELLS[midi] = load_wav(p); + if (!BELLS[midi].L) return; + long f = (long)(0.6 * SR); + for (long i = 0; i < f && i < BELLS[midi].n; i++) { double k = (double)i / f; + BELLS[midi].L[BELLS[midi].n - 1 - i] *= (float)k; BELLS[midi].R[BELLS[midi].n - 1 - i] *= (float)k; } + } + const Stereo *b = &BELLS[midi]; long a = at(t); + for (long i = 0; i < b->n; i++) add(bellM, a + i, (b->L[i] + b->R[i]) * 0.5 * g); + ev(t, "bell", 1.2, g, midi); +} +// vibraphone — pop/marimba's modal preset, ported: partials 1/4/10, decays +// 4.5/0.9/0.25 s, 5.5 Hz motor tremolo at 0.55; damper after `hold` +static void vib(double t, int midi, double g, double hold) { + static const double P[3] = { 1, 4, 10 }, AMP[3] = { 1, 0.22, 0.08 }, DEC[3] = { 4.5, 0.9, 0.25 }; + double f = mtof(midi); long a = at(t), len = (long)((hold + 0.5) * SR); + for (long i = 0; i < len; i++) { double u = (double)i / SR, v = 0; + for (int k = 0; k < 3; k++) if (f * P[k] < SR / 2) v += AMP[k] * sin(2 * PI * f * P[k] * u) * exp(-u / DEC[k]); + double damp = u > hold ? exp(-(u - hold) * 10) : 1; + double trem = 1 - 0.55 * (0.5 + 0.5 * sin(2 * PI * 5.5 * u)); + add(vibM, a + i, v * fmin(1, u / 0.001) * damp * trem * g * 0.6); } + ev(t, "vib", 0.25, g, midi); +} + +// ── harmony ────────────────────────────────────────────────────────────── +static const int CHORD_PCS[3][3] = { { 8, 11, 3 }, { 8, 11, 3 }, { 11, 3, 6 } }; // Emaj7 bars sound G#m (bass G#) +static const int ROOT[3] = { 44, 44, 47 }; // G#2, B2 +static const int SCALE[7] = { 8, 10, 11, 1, 3, 4, 6 }; // G# natural minor +static int is_pc(const int *pcs, int np, int m) { int pc = ((m % 12) + 12) % 12; for (int k = 0; k < np; k++) if (pcs[k] == pc) return 1; return 0; } +static int nearest(const int *pcs, int np, double target, int lo, int hi) { + int best = -1; for (int m = lo; m <= hi; m++) if (is_pc(pcs, np, m) && (best < 0 || fabs(m - target) < fabs(best - target))) best = m; + return best; +} +static void lead(int *voices, int nv, const int *pcs, int lo, int hi) { // nearest chord tone per voice, no doubling + int used[3] = { -1, -1, -1 }, nu = 0; + for (int v = 0; v < nv; v++) { + int freeP[3], nf = 0; + for (int k = 0; k < 3; k++) { int u = 0; for (int j = 0; j < nu; j++) if (used[j] == pcs[k]) u = 1; if (!u) freeP[nf++] = pcs[k]; } + int m = nf ? nearest(freeP, nf, voices[v], lo, hi) : nearest(pcs, 3, voices[v], lo, hi); + voices[v] = m; if (nu < 3) used[nu++] = ((m % 12) + 12) % 12; + } +} +static int third_above(int m) { + int pc = ((m % 12) + 12) % 12, oct = m / 12 - (m < 0), i = -1; + for (int k = 0; k < 7; k++) if (SCALE[k] == pc) i = k; + if (i < 0) return m + 3; + int t = SCALE[(i + 2) % 7]; + return oct * 12 + t + (t < pc ? 12 : 0); +} +static const ChartBar *bar_at(double t) { + for (int k = 0; k < CHART_NBARS; k++) if (t >= CHART_BARS[k].t && t < CHART_BARS[k].t + CHART_BARS[k].dur) return &CHART_BARS[k]; + return NULL; +} + +// ── automation: a per-bar target, glided over 150 ms ───────────────────── +typedef double (*BarValue)(const ChartBar *, int); +static float *automate(BarValue fn, int arg) { + float *a = (float *)malloc(N * sizeof(float)); double cur = -1, glide = 1 - exp(-1.0 / (0.15 * SR)); + const ChartBar *b = &CHART_BARS[0]; + for (long i = 0; i < N; i++) { + double t = (double)i / SR; + while (b < &CHART_BARS[CHART_NBARS - 1] && t >= b->t + b->dur) b++; + double tg = fn(b, arg); + cur = cur < 0 ? tg : cur + (tg - cur) * glide; a[i] = (float)cur; + } + return a; +} +#define ARRV(field) static double v_##field(const ChartBar *b, int x) { (void)x; return ARR[section_of(b->n)].field; } +ARRV(send) ARRV(air) ARRV(acg) ARRV(elg) +static double v_her(const ChartBar *b, int x) { (void)x; int s = section_of(b->n); double g = ARR[s].her; return s == VERSE1 ? g * evo(b->n, 13, 17) : g; } // v6.6: no guitar to start — it creeps in after her first line +// v6.6: warm — the opening vocal close to the mic: proximity below 350 Hz, no air, almost no room; eases out by the floor +// v7: the choir stands behind her in the choruses, the bridge and the outro; a breath of it in verse 2 +static double v_choir(const ChartBar *b, int x) { (void)x; int s = section_of(b->n); + return s == CHORUS1 ? 0.55 * (0.4 + 0.6 * evo(b->n, FLOOR_BAR, FLOOR_BAR + 4)) : s == VERSE2 ? 0.22 : s == CHORUS2 ? 0.7 : s == BRIDGE ? 0.6 : s == OUTRO ? 0.5 : 0; } +static double v_jeff(const ChartBar *b, int x) { (void)x; int s = section_of(b->n); + return s == CHORUS1 ? 0.30 * evo(b->n, FLOOR_BAR + 2, FLOOR_BAR + 6) : s == CHORUS2 ? 0.42 : s == BRIDGE ? 0.45 : s == OUTRO ? 0.3 : 0; } +static double seat_l(double t) { (void)t; return -48; } +static double seat_c(double t) { (void)t; return 0; } +static double seat_r(double t) { (void)t; return 48; } +static double el_back(double t) { (void)t; return -8; } +static double v_warm(const ChartBar *b, int x) { (void)x; int s = section_of(b->n); return s == INTRO ? 1 : s == VERSE1 ? 1 - evo(b->n, 20, FLOOR_BAR) : 0; } +static double v_thick(const ChartBar *b, int x) { (void)x; int s = section_of(b->n); return s == INTRO ? 0 : s == VERSE1 ? 0.35 * evo(b->n, 16, 24) : s == BREAK ? 0 : s == VERSE2 ? 0.5 : 0.8; } +static double v_harm_g(const ChartBar *b, int h) { return harm_at(b->n, h).g; } +static double v_harm_az(const ChartBar *b, int h) { return harm_at(b->n, h).az; } + +// ── space: ac_hrtf over a whole bus ───────────────────────────────────── +static double impact_disp(double t); +typedef double (*Path)(double t); +static void spatialize(const float *mono, const float *azA, Path az, Path el, double dist, float *L, float *R, double g) { + ACHrtf h; memset(&h, 0, sizeof h); + for (long i = 0; i < N; i++) { + double t = (double)i / SR, a = azA ? azA[i] : az(t); + float l, r; + a += impact_disp(t); // v6: the ring is displaced and springs back + ac_hrtf_process(&h, mono[i], a * PI / 180, el(t) * PI / 180, dist, &l, &r); + L[i] += l * (float)g; R[i] += r * (float)g; + } +} +static double orbit_az(double t) { return 70 * sin(2 * PI * t / 24); } +static double orbit_el(double t) { return 25 + 10 * sin(2 * PI * t / 17); } +static double sweep_az(double t) { return 65 * sin(2 * PI * t / 8); } +static double wide_az(double t) { return 80 * sin(2 * PI * t / 31 + 1); } +static double el_zero(double t) { (void)t; return 0; } +static double el_up(double t) { (void)t; return 35; } +static double el_near(double t) { (void)t; return 5; } + +// ── v6 · impact (chamber-04 rule 10): displace every ring seat, spring back ── +// kick and sub never move (they are not on a spatial bus); ring tones open +// with a short noise event — the explosion below. +static const int IMPACT_BARS[] = { 28, 52, 73, 81 }; +#define VOICE_LAG 0.05 // v7: her sung onset sits ~50 ms behind the strum (ANALYSIS.md §2); the explosion waits for it +// v6: the evolution — 0 before `from`, 1 at `to`, cosine between (one lane per phrase) +static double evo(int bar, int from, int to) { if (bar < from) return 0; if (bar >= to) return 1; double x = (double)(bar - from) / (to - from); return 0.5 - 0.5 * cos(PI * x); } +#define NIMPACT ((int)(sizeof IMPACT_BARS / sizeof *IMPACT_BARS)) +static double IMPACT_T[NIMPACT]; +static double impact_disp(double t) { // degrees of azimuth displacement + double d = 0; + for (int k = 0; k < NIMPACT; k++) { double u = t - IMPACT_T[k]; + if (u >= 0 && u < 6) d += 110 * exp(-1.2 * u) * sin(2 * PI * 1.1 * u); } + return d; +} +static double last_impact(double t) { double best = -1e9; for (int k = 0; k < NIMPACT; k++) if (IMPACT_T[k] <= t && IMPACT_T[k] > best) best = IMPACT_T[k]; return best; } +// the explosion: one full lap around the head in 0.7 s, climbing overhead +static double expl_az(double t) { double u = t - last_impact(t); return u < 0 ? 0 : 360 * fmin(1, u / 0.7) * (1 - 0.15 * u); } +static double expl_el(double t) { double u = t - last_impact(t); return u < 0 ? 0 : 60 * fmin(1, u / 0.5) * exp(-u * 0.8); } +static float *exM; +static void explosion(double t, double g) { // noise chiff: bandpass falling 6 kHz → 250 Hz over 0.9 s, a sub thump underneath on the drums bus + long a = at(t); double b1 = 0, b2 = 0, ph = 0; + for (long i = 0; i < 1.1 * SR; i++) { double u = (double)i / SR, fc = 250 + 5750 * exp(-u * 4.2), k = 1 - exp(-2 * PI * fc / SR); + double nz = rnd(); b1 += (nz - b1) * k; b2 += (b1 - b2) * k; + double env = fmin(1, u / 0.004) * exp(-u * 3.2); + add(exM, a + i, (b1 - b2) * env * g * 2.2); } + for (long i = 0; i < 0.5 * SR; i++) { double u = (double)i / SR; ph += 2 * PI * (38 + 90 * exp(-u * 20)) / SR; + double v = sin(ph) * exp(-u * 7) * g * 0.7; add(dL, a + i, v); add(dR, a + i, v); } + ev(t, "impact", 1.0, g, -1); +} + +// ── v6 · vocal arpeggios: her harmony stems gated into figures on her grid ── +// -1 = rest. div = steps per beat (2 = 8ths, 3 = triplets, 4 = 16ths). +typedef struct { int div; int len; int step[8]; double g; double hold; } Arp; // hold: step lengths each note sings (v6.1: > 1 so the stack rolls, never chops) +static const Arp ARP[NSEC] = { + [INTRO] = { 0 }, [VERSE1] = { 0 }, + [CHORUS1] = { 2, 4, { H_UP3, H_DOWN3, H_UP3, H_DOWN6 }, .34, 1.8 }, // 8ths, a rocking third, each held 1.8 steps + [VERSE2] = { 0 }, // the sustained down3 sings here + [CHORUS2] = { 4, 8, { H_DOWN3, H_UP3, H_UP5, H_DOWN6, H_UP3, H_DOWN3, H_DOWN8, H_UP3 }, .30, 3.0 }, // 16ths, up then down, three steps deep + [BREAK] = { 0 }, + [BRIDGE] = { 3, 3, { H_UP3, H_DOWN3, H_DOWN6 }, .30, 2.0 }, // triplets, roving seats + [OUTRO] = { 2, 2, { H_DOWN8, H_UP3 }, .22, 2.0 }, +}; +static const double SEAT[5] = { -70, -35, 0, 35, 70 }; +static float *arpG[NHARM], *arpAz[NHARM]; +static void arp_step(int h, double t0, double t1, double g, double az, double hold) { + double step = t1 - t0; long a = at(t0), z = at(t0 + step * hold), rel = (long)(0.09 * SR); // 12 ms in, 90 ms out + for (long i = a; i < z + rel && i < N; i++) { double u = (double)(i - a) / SR, e = fmin(1, u / 0.012); + if (i >= z) e *= exp(-(double)(i - z) / rel * 4); + if (i >= 0) { arpG[h][i] = (float)fmin(g, arpG[h][i] + e * g); if (arpAz[h][i] == 0) arpAz[h][i] = (float)az; } } +} + +// ── reverb: 8-line FDN (Hadamard feedback, damped) ────────────────────── +static void fdn(const float *inL, const float *inR, float *oL, float *oR, double t60, double damp, double pre) { + static const int LENS[8] = { 1433, 1601, 1867, 2053, 2251, 2399, 2617, 2797 }; + float *buf[8]; int len[8], idx[8] = { 0 }; double lp[8] = { 0 }, gain[8], o[8], hh[8]; + for (int k = 0; k < 8; k++) { len[k] = (int)lround(LENS[k] * (double)SR / 44100); buf[k] = (float *)calloc(len[k], sizeof(float)); + gain[k] = pow(10, -3.0 * len[k] / (SR * t60)); } + long P = lround(pre * SR); + for (long i = 0; i < N; i++) { + double xl = i >= P ? inL[i - P] : 0, xr = i >= P ? inR[i - P] : 0; + for (int k = 0; k < 8; k++) { lp[k] += (buf[k][idx[k]] - lp[k]) * (1 - damp); o[k] = lp[k]; } + double a0 = o[0] + o[1], a1 = o[0] - o[1], a2 = o[2] + o[3], a3 = o[2] - o[3], a4 = o[4] + o[5], a5 = o[4] - o[5], a6 = o[6] + o[7], a7 = o[6] - o[7]; + double b0 = a0 + a2, b1 = a1 + a3, b2 = a0 - a2, b3 = a1 - a3, b4 = a4 + a6, b5 = a5 + a7, b6 = a4 - a6, b7 = a5 - a7; + hh[0] = b0 + b4; hh[1] = b1 + b5; hh[2] = b2 + b6; hh[3] = b3 + b7; hh[4] = b0 - b4; hh[5] = b1 - b5; hh[6] = b2 - b6; hh[7] = b3 - b7; + for (int k = 0; k < 8; k++) { buf[k][idx[k]] = (float)(hh[k] * 0.35355 * gain[k] + (k % 2 ? xr : xl) * 0.5); idx[k] = (idx[k] + 1) % len[k]; } + oL[i] = (float)(o[0] + o[2] + o[4] + o[6]); oR[i] = (float)(o[1] + o[3] + o[5] + o[7]); + } + for (int k = 0; k < 8; k++) free(buf[k]); +} + +static float *zeros(void) { return (float *)calloc(N, sizeof(float)); } + +// humanize: ±4 ms, ±12 % — a hand, not a sequencer ("a bit midi like") +static double hum_t(double t) { return t + rnd() * 0.004; } +static double hum_g(double g) { return g * (1 + rnd() * 0.12); } +static double hum_t2(double t, double ms) { return t + rnd() * ms / 1000; } // v6.4: looser hands +static double hum_g2(double g, double pct) { return g * (1 + rnd() * pct); } + +// ── the dance layer (v5.2: "turn the song into a dance mix") ───────────── +// Her ~120 is house tempo, so four on HER floor: a kick on every one of her +// beats, claps 2 + 4, open hats on every offbeat, an offbeat house bass on +// her roots, risers into the drops. +static void dance_kick(double t, double g) { + long a = at(t); double ph = 0; + for (long i = 0; i < 0.42 * SR; i++) { double u = (double)i / SR; + ph += 2 * PI * (50 + 160 * exp(-u * 38) + 40 * exp(-u * 300)) / SR; + double click = i < 0.003 * SR ? rnd() * 0.6 * (1 - u / 0.003) : 0; + double v = (tanh(sin(ph) * 2.4) * 0.85 * exp(-u * 5.5) + click) * g; + add(dL, a + i, v); add(dR, a + i, v); } + for (long i = 0; i < 0.3 * SR; i++) add(duck, a + i, g * 1.6 * exp(-(double)i / SR * 8)); +} +static void open_hat(double t, double g) { + long a = at(t); double p1 = 0, p2 = 0; + for (long i = 0; i < 0.16 * SR; i++) { double u = (double)i / SR, nz = rnd(), h1 = nz - p1; p1 = nz; double h2 = h1 - p2; p2 = h1; + double v = h2 * fmin(1, u / 0.002) * exp(-u * 16) * g * 0.3; add(tL, a + i, v * 0.8); add(tR, a + i, v * 1.1); } +} +// house bass: a saturated sine pluck with a closing lowpass, on the offbeat +static void house_bass(double t, int midi, double dur, double g) { + long a = at(t); double f = mtof(midi), ph = 0, lp = 0; + for (long i = 0; i < (dur + 0.05) * SR; i++) { double u = (double)i / SR; + ph += 2 * PI * f / SR; + double raw = tanh(sin(ph) * 4.0 + 0.6 * sin(2 * ph)); + double fc = 180 + 2400 * exp(-u * 14), k = 1 - exp(-2 * PI * fc / SR); + lp += (raw - lp) * k; + double env = fmin(1, u / 0.003) * (u > dur ? exp(-(u - dur) * 60) : 1); + add(b808, a + i, lp * env * g); } +} +// riser: white noise through a rising bandpass, swelling into the downbeat +static void riser(double t0, double t1, double g) { + long a = at(t0), len = at(t1) - a; double b1 = 0, b2 = 0; + for (long i = 0; i < len; i++) { double x = (double)i / len, fc = 300 * pow(40, x), k = 1 - exp(-2 * PI * fc / SR); + double nz = rnd(); b1 += (nz - b1) * k; b2 += (b1 - b2) * k; + double v = (b1 - b2) * x * x * g; add(tL, a + i, v * (1 - 0.3 * sin(PI * 8 * x))); add(tR, a + i, v * (1 + 0.3 * sin(PI * 8 * x))); } +} +// 4otf clap ohat hbass (v5.2) +static const double DANCE[NSEC][4] = { + [INTRO] = { 0, 0, 0, 0 }, [VERSE1] = { .8, 0, .5, .7 }, [CHORUS1] = { 1, 1, .8, 1 }, [VERSE2] = { .65, .3, .3, .5 }, + [CHORUS2] = { 1, 1, 1, 1 }, [BREAK] = { 0, 0, 0, 0 }, [BRIDGE] = { 1, .8, .8, .9 }, [OUTRO] = { .7, .4, .3, .5 } }; + +// ── hand percussion (v5.1: "we have no actual percussion yet") ─────────── +static float *pL, *pR; +// shaker: band-passed noise with a swell-in (the bead mass lags the hand) +static void shaker(double t, double g, double pan) { + long a = at(t); double b1 = 0, b2 = 0, prev = 0; + for (long i = 0; i < 0.07 * SR; i++) { double u = (double)i / SR, nz = rnd(), hp = nz - prev; prev = nz; + b1 += (hp - b1) * 0.55; b2 += (b1 - b2) * 0.55; + double env = fmin(1, u / 0.012) * exp(-u * 45); + double v = (b1 - b2 * 0.6) * env * g; add(pL, a + i, v * (1 - pan)); add(pR, a + i, v * (1 + pan)); } +} +// tambourine: jingles = noise through three metallic partials, a short hit + ring +static void tamb(double t, double g, double pan) { + static const double F[3] = { 7040, 9360, 11870 }; + long a = at(t); double prev = 0; + for (long i = 0; i < 0.22 * SR; i++) { double u = (double)i / SR, nz = rnd(), hp = nz - prev, v = 0; prev = nz; + for (int k = 0; k < 3; k++) v += sin(2 * PI * F[k] * u + k) * 0.25; + v = (v * (0.6 + 0.4 * nz) + hp * 0.5) * exp(-u * 18) * g; + add(pL, a + i, v * (1 - pan)); add(pR, a + i, v * (1 + pan)); } +} +// conga: a sine membrane with a pitch drop, tuned to the chord; slap = + noise +static void conga(double t, int midi, double g, double pan, int slap) { + long a = at(t); double f = mtof(midi), ph = 0, prev = 0; + for (long i = 0; i < 0.3 * SR; i++) { double u = (double)i / SR, nz = rnd(), hp = nz - prev; prev = nz; + ph += 2 * PI * f * (1 + 0.35 * exp(-u * 60)) / SR; + double v = sin(ph) * exp(-u * (slap ? 22 : 11)) + (slap ? hp * exp(-u * 80) * 0.8 : 0); + v *= g; add(pL, a + i, v * (1 - pan)); add(pR, a + i, v * (1 + pan)); } +} +// woodblock: two short resonances, for the tresillo x..x..x. (E(3,8)) that +// runs 3-against-her-2 across the bar (rhythm-platter 01: the Cuban tresillo) +static void block(double t, double g, double pan) { + long a = at(t); + for (long i = 0; i < 0.05 * SR; i++) { double u = (double)i / SR; + double v = (sin(2 * PI * 1180 * u) * 0.7 + sin(2 * PI * 2350 * u) * 0.4) * exp(-u * 110) * g; + add(pL, a + i, v * (1 - pan)); add(pR, a + i, v * (1 + pan)); } +} +// the jet: decorrelated white noise through a lowpass that opens 300 Hz → 9 kHz +// over the span, amplitude rising as x^1.6 — an airplane taking off into the drop +static float *jL, *jR; +static void jet(double t0, double t1, double g) { + long a = at(t0), len = at(t1) - a; double l1 = 0, l2 = 0, r1 = 0, r2 = 0; + for (long i = 0; i < len; i++) { double x = (double)i / len, fc = 300 * pow(30, x), k = 1 - exp(-2 * PI * fc / SR); + double nl = rnd(), nr = rnd(); l1 += (nl - l1) * k; l2 += (l1 - l2) * k; r1 += (nr - r1) * k; r2 += (r1 - r2) * k; + double env = pow(x, 1.6) * g; add(jL, a + i, l2 * env); add(jR, a + i, r2 * env); } +} +// air: a steady wide hiss (high-passed noise) under the choruses, pumped by the kick in the mix +static void air(double t0, double t1, double g) { + long a = at(t0), len = at(t1) - a; double pl1 = 0, pr1 = 0; + for (long i = 0; i < len; i++) { double nl = rnd(), nr = rnd(), hl = nl - pl1, hr = nr - pr1; pl1 = nl; pr1 = nr; + double x = (double)i / len, env = fmin(1, x / 0.1) * fmin(1, (1 - x) / 0.1) * g; + add(jL, a + i, hl * env * 0.5); add(jR, a + i, hr * env * 0.5); } +} +// shaker tamb conga +static const double PERC[NSEC][3] = { + [INTRO] = { 0, 0, 0 }, [VERSE1] = { .45, 0, .5 }, [CHORUS1] = { .8, .8, .7 }, [VERSE2] = { .45, .2, .5 }, + [CHORUS2] = { 1, 1, .8 }, [BREAK] = { .4, 0, .5 }, [BRIDGE] = { .8, .7, .8 }, [OUTRO] = { .3, 0, .5 } }; + +// ── pop acoustic chain for her guitar (v5.1: "doesn't feel poppy") ─────── +// RBJ biquads: HP 110 Hz · −4 dB bell at 400 Hz (the box) · +4 dB shelf at +// 10 kHz (sparkle) → fast 4:1 compressor (snap + sustain) → M/S width 1.5. +typedef struct { double b0, b1, b2, a1, a2, x1, x2, y1, y2; } Biquad; +static Biquad bq(int type, double f, double q, double db) { + double A = pow(10, db / 40), w = 2 * PI * f / SR, c = cos(w), sn = sin(w), al = sn / (2 * q), b0, b1, b2, a0, a1, a2; + if (type == 0) { b0 = (1 + c) / 2; b1 = -(1 + c); b2 = b0; a0 = 1 + al; a1 = -2 * c; a2 = 1 - al; } // highpass + else if (type == 1) { b0 = 1 + al * A; b1 = -2 * c; b2 = 1 - al * A; a0 = 1 + al / A; a1 = -2 * c; a2 = 1 - al / A; } // bell + else { double sq = 2 * sqrt(A) * al; // high shelf + b0 = A * ((A + 1) + (A - 1) * c + sq); b1 = -2 * A * ((A - 1) + (A + 1) * c); b2 = A * ((A + 1) + (A - 1) * c - sq); + a0 = (A + 1) - (A - 1) * c + sq; a1 = 2 * ((A - 1) - (A + 1) * c); a2 = (A + 1) - (A - 1) * c - sq; } + Biquad b = { b0 / a0, b1 / a0, b2 / a0, a1 / a0, a2 / a0, 0, 0, 0, 0 }; + return b; +} +static double bq_run(Biquad *b, double x) { + double y = b->b0 * x + b->b1 * b->x1 + b->b2 * b->x2 - b->a1 * b->y1 - b->a2 * b->y2; + b->x2 = b->x1; b->x1 = x; b->y2 = b->y1; b->y1 = y; return y; +} +// v6: bedroom leveler — her opening strums are 20 dB under her playing at +// bar 11. Before bar 16 the guitar rides a slow RMS leveler (target −18 dBFS, +// up to +16 dB, 30 ms / 500 ms) that eases back to unity by UNTIL. +static void bedroom_level(Stereo *g, double until) { + if (!g->L) return; + double env = 0, gain = 1; const double aA = exp(-1 / (0.03 * SR)), aR = exp(-1 / (0.5 * SR)), target = 0.126; + for (long i = 0; i < g->n; i++) { + double t = (double)i / SR, m = (g->L[i] + g->R[i]) / 2, pw = m * m; + env = pw > env ? aA * env + (1 - aA) * pw : aR * env + (1 - aR) * pw; + double want = env > 1e-7 ? fmin(6.3, target / sqrt(env)) : 6.3; // ≤ +16 dB + double w = t >= until ? 0 : t < until - 8 ? 1 : 0.5 + 0.5 * cos(PI * (t - (until - 8)) / 8); + gain += ((1 + (want - 1) * w) - gain) * 0.0005; + g->L[i] *= (float)gain; g->R[i] *= (float)gain; + } +} +static void pop_guitar(Stereo *g, double width) { + if (!g->L) return; + for (int ch = 0; ch < 2; ch++) { + float *x = ch ? g->R : g->L; + Biquad hp = bq(0, 110, 0.707, 0), box = bq(1, 400, 1.0, -4), air = bq(2, 10000, 0.707, 4); + double env = 0, gc = 1, aA = exp(-1 / (0.003 * SR)), aR = exp(-1 / (0.08 * SR)); + double rms = 0; for (long i = 0; i < g->n; i++) rms += (double)x[i] * x[i]; + double th = sqrt(rms / g->n) * 1.2; + for (long i = 0; i < g->n; i++) { + double y = bq_run(&air, bq_run(&box, bq_run(&hp, x[i]))); + double lv = fabs(y); env = lv > env ? aA * env + (1 - aA) * lv : aR * env + (1 - aR) * lv; + double gt = env > th ? pow(env / th, 1.0 / 4 - 1) : 1; + gc += (gt - gc) * 0.02; + x[i] = (float)(y * gc * 1.8); + } + } + for (long i = 0; i < g->n; i++) { double m = (g->L[i] + g->R[i]) / 2, sd = (g->L[i] - g->R[i]) / 2 * width; + g->L[i] = (float)(m + sd); g->R[i] = (float)(m - sd); } +} + +int main(void) { + // ── her ── + // v6: stems on the regularized clock (bin/regularize.mjs → src/vox/reg); SAILOR_STEMS=locked to follow her raw + const char *stems = getenv("SAILOR_STEMS") ? getenv("SAILOR_STEMS") : "reg"; + const int regd = strcmp(stems, "reg") == 0; + char pth[512]; + #define STEM(name) (snprintf(pth, sizeof pth, LANE "/src/vox/%s/%s.wav", stems, name), pth) + #define GSTEM(name) (regd ? STEM(name) : (snprintf(pth, sizeof pth, LANE "/src/vox/%s.wav", name), pth)) + Stereo vox = load_wav(STEM("vocals-aesthetivox")); + Stereo halo = load_wav(STEM("vocals-halo")); + Stereo gtr = load_wav(GSTEM("guitar-48k")); + Stereo acg = load_wav(GSTEM("replay-acoustic")); + Stereo elg = load_wav(GSTEM("replay-electric")); + Stereo harm[NHARM]; + for (int h = 0; h < NHARM; h++) harm[h] = load_wav(STEM(HARM_FILE[h])); + // v7: accompaniment voices (bin/choir.py) — her own vocoder choir and @jeffrey's vowel drone + static const char *CHOIR_FILE[3] = { "choir-low", "choir-mid", "choir-high" }; + Stereo choir[3], jeff[2]; + for (int c = 0; c < 3; c++) choir[c] = load_wav(STEM(CHOIR_FILE[c])); + jeff[0] = load_wav(STEM("jeffrey-root")); jeff[1] = load_wav(STEM("jeffrey-fifth")); + fprintf(stderr, "stems: %s\n", stems); + // v7: her first sung word = the first 30 ms window of the lead stem after bar START_BAR + // above −45 dBFS RMS (guitar bleed in the stem sits near −60; her voice enters near −35) + double startSecOut = CHART_BARS[START_BAR - 1].t; + { long w = (long)(0.03 * SR), from = at(CHART_BARS[START_BAR - 1].t); + for (long i = from; i + w < vox.n; i += w / 3) { double acc = 0; for (long j = 0; j < w; j++) acc += (double)vox.L[i + j] * vox.L[i + j]; + if (sqrt(acc / w) > 0.0056) { startSecOut = (double)i / SR - 0.06; break; } } } + if (!vox.L || !gtr.L) { fprintf(stderr, "! run bin/aesthetivox.py + bin/lock-vox.mjs first\n"); return 1; } + N = vox.n; + pL = zeros(); pR = zeros(); + bedroom_level(>r, CHART_BARS[15].t); // v6: until bar 16 + pop_guitar(>r, 1.5); pop_guitar(&acg, 1.3); + dL = zeros(); dR = zeros(); tL = zeros(); tR = zeros(); b808 = zeros(); sL = zeros(); sR = zeros(); + hiM = zeros(); bellM = zeros(); vibM = zeros(); duck = zeros(); exM = zeros(); jL = zeros(); jR = zeros(); + for (int h = 0; h < NHARM; h++) { arpG[h] = zeros(); arpAz[h] = zeros(); } + for (int q = 0; q < NIMPACT; q++) IMPACT_T[q] = -1e9; + EV = fopen(LANE "/out/sailor-song-" VERSION ".events.json", "w"); + if (EV) fprintf(EV, "{\"tempoBPM\":120,\"seconds\":%.2f,\"startSec\":%.3f,\"startBar\":%d,\"floorBar\":%d,\"stems\":\"%s\",\"sections\":[", (double)N / SR, startSecOut, START_BAR, FLOOR_BAR, stems); + if (EV) for (int s = 0; s < NSEC; s++) { + int a = SEC_FROM[s] - 1, z = (s + 1 < NSEC ? SEC_FROM[s + 1] : CHART_NBARS + 1) - 2; + fprintf(EV, "%s{\"name\":\"%s\",\"start\":%.3f,\"end\":%.3f}", s ? "," : "", SEC_NAME[s], CHART_BARS[a].t, CHART_BARS[z].t + CHART_BARS[z].dur); + } + if (EV) fprintf(EV, "],\"bars\":["); + if (EV) for (int k = 0; k < CHART_NBARS; k++) { static const char *CN[3] = { "G#m", "Emaj7", "B" }; + fprintf(EV, "%s{\"n\":%d,\"t\":%.3f,\"dur\":%.3f,\"chord\":\"%s\",\"section\":\"%s\"}", k ? "," : "", CHART_BARS[k].n, CHART_BARS[k].t, CHART_BARS[k].dur, CN[CHART_BARS[k].chord], SEC_NAME[section_of(CHART_BARS[k].n)]); } + if (EV) fprintf(EV, "],\"events\":[\n"); + + // ── score the bars ── + int tenor[3] = { 68, 71, 75 }, high[2] = { 80, 83 }, desc = 90; // v6.3: the tenor trio clears her top (G#4 = 68) + lead(tenor, 3, CHORD_PCS[0], 67, 79); + for (int k = 0; k < CHART_NBARS; k++) { + const ChartBar *b = &CHART_BARS[k]; + const int s = section_of(b->n); const Arr *o = &ARR[s]; + const double *bt = b->beats; const int nb = b->nb; const int *pcs = CHORD_PCS[b->chord]; + const double beatDur = b->dur / nb; + const double swell = s == BRIDGE ? 0.75 + 0.25 * (b->n - 72) / 8.0 : 1; + #define MID(j) (bt[j] + (bt[(j) + 1] - bt[j]) / 2) + // pocket on her strum points + const double *dz = DANCE[s]; + for (int q = 0; q < NIMPACT; q++) if (IMPACT_BARS[q] == b->n) { IMPACT_T[q] = bt[0] + VOICE_LAG; explosion(bt[0] + VOICE_LAG, b->n == FLOOR_BAR ? 0.45 : 0.6); } // v6.5: subtler + const double gDrop = s == CHORUS1 ? 0.55 + 0.45 * evo(b->n, FLOOR_BAR, FLOOR_BAR + 3) : 1; // v6.5: the first floor eases in under her line + // v6.6: hills — every section rises to its middle and settles into its edge + const int secLen = (s + 1 < NSEC ? SEC_FROM[s + 1] : CHART_NBARS + 1) - SEC_FROM[s]; + const double hill = s == INTRO ? 1 : 0.84 + 0.16 * sin(PI * (b->n - SEC_FROM[s] + 0.5) / secLen); + // v6: the evolution gates (verse 1 only; every other section is its table) + const int v1 = s == VERSE1; + const double gKick = evo(b->n, 24, 26), gHat = evo(b->n, 25, 28), gBass = evo(b->n, 24, 27); + const double gSub = evo(b->n, 16, 22), gTen = evo(b->n, 20, 26), gShk = evo(b->n, 14, 18), gCon = evo(b->n, 18, 22); // v7: her first line alone; the shuffle from bar 14, congas from 18 + // v6 HOCKET: the floor doubles her 1 and 3 and lands in her empty 2 and 4; + // open hats on the complement's &1/&3; her own &2/&4 get a soft accent at her bin + if (dz[0] && gKick > 0) for (int j = 0; j < nb; j++) { if (v1 && j % 2) continue; // verse 1: her 1 and 3 only + double g = 0.95 * dz[0] * (j % 2 ? 0.9 : 1) * gKick * hill; + dance_kick(bt[j], g); ev(bt[j], "kick", 0.1, g, -1); } + if (dz[1]) for (int j = 1; j < nb; j += 2) { clap(hum_t2(bt[j], 6), hum_g2(0.55 * dz[1] * swell * gDrop * hill, 0.15)); ev(bt[j], "clap", 0.1, dz[1], -1); } + if (dz[2] && gHat > 0) for (int j = 0; j < nb; j++) { + if (j % 2 == 0) { open_hat(hum_t2(MID(j) + BIN_OFF, 6), hum_g2(0.5 * dz[2] * swell * gHat * gDrop * hill, 0.18)); ev(MID(j), "hat", 0.1, dz[2], -1); } + else { trap_hat(hum_t2(MID(j) + BIN_OFF, 5), hum_g2(0.28 * dz[2] * swell * gHat * gDrop, 0.18), 0.3); ev(MID(j), "hat", 0.02, dz[2] * 0.5, -1); } } + // house bass: her pickups (&2/&4) in the verses, every offbeat in the choruses + if (dz[3] && gBass > 0) for (int j = 0; j < nb; j++) { if (dz[3] < 0.95 && j % 2 == 0) continue; + int r = ROOT[b->chord] - 12 + (j == nb - 1 && b->n % 2 ? 7 : 0); + house_bass(MID(j) + BIN_OFF, r, beatDur * 0.42, 0.55 * dz[3] * gBass * gDrop * hill); ev(MID(j), "bass", beatDur * 0.42, dz[3], r); } + // her arpeggios + if (ARP[s].div) { const Arp *A = &ARP[s]; int c = ((b->n - SEC_FROM[s]) * nb * A->div) % A->len; + for (int j = 0; j < nb; j++) for (int q = 0; q < A->div; q++) { + double t0 = bt[j] + (bt[j + 1] - bt[j]) * q / A->div + (q ? BIN_OFF : 0), t1 = bt[j] + (bt[j + 1] - bt[j]) * (q + 1) / A->div; + int h = A->step[c % A->len]; c++; + if (h < 0) continue; + double az = s == BRIDGE ? SEAT[(b->n - 73 + q) % 5] : SEAT[(j * A->div + q) % 5]; + arp_step(h, t0, t1, A->g * swell * gDrop, az, A->hold); ev(t0, "arp", (t1 - t0) * A->hold, A->g, h); } } + // risers: into each chorus + out of the break; a snare roll into the bridge's last bars + if (b->n == 26 || b->n == 50 || b->n == 71 || b->n == 79) riser(b->t, b->t + b->dur + CHART_BARS[k + 1].dur, 0.22); + // v6.4: airplanes — an 8-bar jet into chorus 1, chorus 2 and the bridge (4 bars out of the break) + if (b->n == 20 || b->n == 44) jet(b->t, CHART_BARS[k + 8].t, 0.8); + if (b->n == 68) jet(b->t, CHART_BARS[k + 5].t, 1.0); + if (b->n == 28 || b->n == 52 || b->n == 73) air(b->t, CHART_BARS[k + (b->n == 73 ? 8 : 16) - 1].t + CHART_BARS[k + (b->n == 73 ? 8 : 16) - 1].dur, 0.10); + // (v6.6: the snare roll into the bridge is gone — the jet is the hill) + if (o->kick && !dz[0]) { kick(bt[0], 0.95 * o->kick); ev(bt[0], "kick", 0.1, o->kick, -1); + if (nb > 2) { kick(MID(1), 0.6 * o->kick); ev(MID(1), "kick", 0.1, o->kick * 0.6, -1); } } + if (o->snare && gKick > 0) for (int j = 1; j < nb; j += 2) { snare(hum_t2(bt[j], 4), hum_g2(0.62 * o->snare * swell * gKick * hill, 0.1)); ev(bt[j], "snare", 0.1, o->snare, -1); } // v6.6: the rock backbeat + if (o->clap && nb > 2 && !dz[1]) { clap(bt[2], 0.5 * o->clap * swell); ev(bt[2], "clap", 0.1, o->clap, -1); } + if (o->rim && nb > 3) { rim(MID(3) + BIN_OFF, 0.22 * o->rim); ev(MID(3), "rim", 0.05, o->rim, -1); } + if (o->hat) for (int j = 0; j < nb; j++) { soft_hat(bt[j], 0.15 * o->hat, -0.35); soft_hat(MID(j), 0.09 * o->hat, 0.35); } + { // hand percussion on her grid: shaker 16ths, tambourine backbeat, congas on her &2 / &4 + const double *pc = PERC[s]; + // v6.5: the shuffle starts on 8ths and thickens into 16ths as the floor nears + // (odd 16ths appear with a probability rising bar 14 → 28), then loses one 16th in + // twelve at random so it never runs like a machine + if (pc[0] && gShk > 0) for (int j = 0; j < nb; j++) for (int q = 0; q < 4; q++) { + double t = bt[j] + (bt[j + 1] - bt[j]) * q / 4; + double fill = evo(b->n, 20, FLOOR_BAR); + if (q % 2 && (rnd() + 1) / 2 > fill) continue; // the odd 16ths fill in (bars 20 → 28) + if (b->n >= FLOOR_BAR && (rnd() + 1) / 2 < 0.08) continue; // a dropped grain now and then + int c16 = (b->n - 1) * 16 + j * 4 + q; // accents on a 3-cycle over the 16ths, floor only + double acc = b->n >= FLOOR_BAR ? (c16 % 3 == 0 ? 1 : (q == 2 ? 0.7 : 0.45)) : (q == 0 ? 0.9 : 0.6); // plain 8ths before the floor + shaker(hum_t2(t, 10), hum_g2(0.35 * pc[0] * acc * swell * gShk, 0.22), 0.55); ev(t, "shaker", 0.03, pc[0] * acc, -1); } + if (dz[0] && gHat > 0 && nb > 3) { // v6.4: tresillo x..x..x. on a woodblock + static const int TRES[3] = { 0, 3, 6 }; + for (int q = 0; q < 3; q++) { double t = bt[0] + (bt[4] - bt[0]) * TRES[q] / 8.0 + (TRES[q] % 2 ? BIN_OFF : 0); + block(hum_t2(t, 7), hum_g2(0.22 * dz[0] * gHat * (q == 0 ? 0.8 : 1), 0.25), q == 1 ? -0.5 : 0.5); ev(t, "block", 0.03, dz[0], -1); } } + if (pc[1] && nb > 2) { tamb(hum_t2(bt[2], 8), hum_g2(0.30 * pc[1] * swell, 0.2), -0.4); ev(bt[2], "tamb", 0.1, pc[1], -1); + if (nb > 3) tamb(hum_t2(MID(3) + BIN_OFF, 8), hum_g2(0.14 * pc[1], 0.2), -0.4); } + if (pc[2] && gCon > 0) { int lo = ROOT[b->chord] + 12, hi = lo + 7; double cg = pc[2] * gCon; // tuned to her root / fifth + if (nb > 1) { conga(hum_t2(MID(1) + BIN_OFF, 9), hi, hum_g2(0.30 * cg, 0.22), 0.35, 0); ev(MID(1), "conga", 0.1, cg, hi); } + if (nb > 3) { conga(hum_t2(bt[3], 9), lo, hum_g2(0.34 * cg, 0.22), 0.25, 0); conga(hum_t2(MID(3) + BIN_OFF, 9), hi, hum_g2(0.26 * cg, 0.22), 0.35, 1); ev(bt[3], "conga", 0.1, cg, lo); } + if (b->n % 2 == 0 && nb > 2) { conga(hum_t2(bt[1] + (bt[2] - bt[1]) * 2 / 3.0, 9), hi, hum_g2(0.2 * cg, 0.25), 0.3, 1); } } // v6.4: a triplet slap every other bar + } + // 808 on the kick points, gliding into her root + if (o->b808 && !dz[3]) { int r = ROOT[b->chord] - 12; + eight08(bt[0], r, beatDur * 1.4, 0.8 * o->b808); ev(bt[0], "808", beatDur * 1.4, o->b808, r); + if (nb > 2) { eight08(MID(1), r, beatDur * 1.3, 0.6 * o->b808); ev(MID(1), "808", beatDur * 1.3, o->b808 * 0.6, r); } } + // trap hats: 16ths, accents on the beat, a roll into every other bar line + if (o->trap) { + int roll = 0, trip = 0; // v6.6: no rushes + for (int j = 0; j < nb; j++) { + int last = j == nb - 1 && roll, div = last ? (trip ? 6 : 8) : 4; + for (int q = 0; q < div; q++) { + double t = bt[j] + (bt[j + 1] - bt[j]) * q / div; + double acc = q == 0 ? 1 : (q % 2 ? 0.55 : 0.75); + double g = 0.5 * o->trap * acc * (last ? 0.6 + 0.4 * q / div : 1) * swell; + trap_hat(t, g, q % 2 ? 0.45 : -0.25); ev(t, "hat", 0.02, g, -1); + } + } + } + // sine choir + const double dur = b->dur * 0.97; + if (o->sub && gSub > 0) { sine(sL, sR, b->t, dur, ROOT[b->chord] - 12, 0.18 * o->sub * gSub, 0, 0.35, 0.9, 0); + sine(sL, sR, b->t, dur, ROOT[b->chord], 0.12 * o->sub * gSub, 0, 0.35, 0.9, 1); ev(b->t, "sub", dur, o->sub * gSub, ROOT[b->chord] - 12); } + lead(tenor, 3, pcs, 67, 79); + if (o->tenor && gTen > 0) for (int v = 0; v < 3; v++) { PAD(b->t, dur, tenor[v], 0.16 * o->tenor * swell * gTen * gDrop * hill, (v - 1) * 0.6); ev(b->t, "pad", dur, o->tenor * gTen, tenor[v]); } + lead(high, 2, pcs, 76, 88); + if (o->high) for (int v = 0; v < 2; v++) { sine(hiM, NULL, b->t, dur, high[v], 0.08 * o->high * swell * gDrop, 0, 0.8, 1.4, 7); ev(b->t, "high", dur, o->high, high[v]); } + if (o->descant) { // one long high chord tone per bar, leaning down by step + int c = -1; for (int d = -1; d >= -2 && c < 0; d--) { int m = nearest(pcs, 3, desc + d, 83, 93); if (m <= desc) c = m; } + if (c < 0) c = nearest(pcs, 3, desc, 83, 93); + desc = c <= 84 ? 92 : c; + sine(hiM, NULL, b->t, dur, c, 0.04 * o->descant, 0, 1.0, 1.8, 4); ev(b->t, "descant", dur, o->descant, c); + } + // vibraphone arpeggio on her 8ths (16ths in the last bridge bars) + if (o->vib) { + int tones[6]; for (int j = 0; j < 6; j++) tones[j] = nearest(pcs, 3, 76 + j * 3, 76, 93); // v6.3: vibes an octave up, out of her way + int dens = (s == BRIDGE && b->n >= 77) ? 4 : 2, c = 0; + for (int j = 0; j < nb; j++) for (int q = 0; q < dens; q++) + vib(hum_t(bt[j] + (bt[j + 1] - bt[j]) * q / dens), tones[c++ % 6], hum_g(0.10 * o->vib * swell), beatDur / dens * 1.5); + } + // a fem bell on every other downbeat where the trap plays + if (o->bell && o->trap && b->n % 2 == 0) fem_bell(bt[0], nearest(pcs, 3, 72, 68, 76), 0.18 * o->bell); + ev(b->t, "gtr", dur, o->her, -1); + } + // her melody, shadowed a 3rd up and an octave up + for (int k = 0; k < CHART_NNOTES; k++) { + const ChartNote *v = &CHART_NOTES[k]; const ChartBar *b = bar_at(v->t); + if (!b || v->dur < 0.18) continue; + const Arr *o = &ARR[section_of(b->n)]; + ev(v->t, "vox", v->dur, 1, v->midi); + if (o->third) sine(sL, sR, v->t, v->dur, third_above(v->midi), 0.06 * o->third, 0.3, 0.06, 0.35, 2); + if (o->octave) sine(hiM, NULL, v->t, v->dur, v->midi + 12, 0.03 * o->octave, 0, 0.08, 0.5, 4); + } + // fem bell answers: in every breath > 1 s her last three notes ring back + for (int k = 1; k < CHART_NNOTES; k++) { + const ChartNote *p = &CHART_NOTES[k - 1]; double end = p->t + p->dur, gap = CHART_NOTES[k].t - end; + const ChartBar *b = bar_at(end); + if (gap < 1.0 || !b) continue; + const Arr *o = &ARR[section_of(b->n)]; + if (!o->bell) continue; + const double gBell = evo(b->n, 14, 20); // v6.2: no bells in the bedroom bars + if (gBell <= 0) continue; + double e8 = (b->beats[1] - b->beats[0]) / 2; + for (int j = 0, from = k >= 3 ? k - 3 : 0; from + j < k; j++) { + double t = end + 0.12 + j * e8; + if (t < CHART_NOTES[k].t - 0.1) fem_bell(hum_t(t), CHART_NOTES[from + j].midi + 12, hum_g(0.16 * o->bell * gBell)); + } + } + // the hook on sines over the guitar break: chorus 1 bars 28–31 re-laid on 68–71 + for (int k = 0; k < CHART_NNOTES; k++) { + const ChartNote *v = &CHART_NOTES[k]; const ChartBar *sb = bar_at(v->t); + if (!sb || sb->n < 28 || sb->n > 31) continue; + const ChartBar *db = &CHART_BARS[sb->n + 40 - 1]; + double t = db->t + (v->t - sb->t) / sb->dur * db->dur, d = v->dur * db->dur / sb->dur; + sine(sL, sR, t, d, v->midi, 0.09, 0, 0.05, 0.6, 3); + sine(hiM, NULL, t, d, v->midi + 12, 0.035, 0, 0.08, 0.8, 5); + ev(t, "hook", d, 1, v->midi); + } + + // ── space ── + float *spL = zeros(), *spR = zeros(); + spatialize(bellM, NULL, orbit_az, orbit_el, 1.2, spL, spR, 0.9); // bells orbit overhead + spatialize(vibM, NULL, sweep_az, el_zero, 1.2, spL, spR, 0.8); // vibes sweep ear to ear + float *hiL = zeros(), *hiR = zeros(); + spatialize(hiM, NULL, wide_az, el_up, 1.2, hiL, hiR, 1.3); // highs float up + wide + float *exL = zeros(), *exR = zeros(); + spatialize(exM, NULL, expl_az, expl_el, 0.8, exL, exR, 1.0); // the explosion laps the head + float *hL = zeros(), *hR = zeros(); + for (int h = 0; h < NHARM; h++) { + if (!harm[h].L) continue; + float *g = automate(v_harm_g, h), *az = automate(v_harm_az, h); + int any = 0; for (long i = 0; i < N; i += 480) if (g[i] > 0.002) { any = 1; break; } + if (any) { + float *m = zeros(); long d = lround(HARM_DELAY[h] * SR); + for (long i = 0; i < N; i++) m[i] = sample(harm[h].L, harm[h].n, i - d) * g[i] * 0.6f; + spatialize(m, az, NULL, el_near, 1.0, hL, hR, 1.0); + free(m); + } + free(g); free(az); + // v6: the arpeggiated copy of the same stem, each step at its own seat + int anyArp = 0; for (long i = 0; i < N; i += 480) if (arpG[h][i] > 0.002) { anyArp = 1; break; } + if (anyArp) { + float *m = zeros(); + for (long i = 0; i < N; i++) m[i] = sample(harm[h].L, harm[h].n, i) * arpG[h][i]; + spatialize(m, arpAz[h], NULL, el_near, 0.9, hL, hR, 1.0); + free(m); + } + } + + // v7: the choir, three seats behind her (further than the harmonies), keyed and pumped in the mix + float *cL = zeros(), *cR = zeros(); + { float *cg = automate(v_choir, 0); Path seats[3] = { seat_l, seat_c, seat_r }; + for (int c = 0; c < 3; c++) { if (!choir[c].L) continue; float *m = zeros(); + for (long i = 0; i < N; i++) m[i] = sample(choir[c].L, choir[c].n, i) * cg[i] * (c == 1 ? 0.8f : 1.0f); + spatialize(m, NULL, seats[c], el_back, 1.6, cL, cR, 1.0); free(m); } + free(cg); } + float *jg = automate(v_jeff, 0); + + // ── mix ── + float *sendA = automate(v_send, 0), *airA = automate(v_air, 0), *thickA = automate(v_thick, 0), *warmA = automate(v_warm, 0); + float *herA = automate(v_her, 0), *acgA = automate(v_acg, 0), *elgA = automate(v_elg, 0); + float *L = zeros(), *R = zeros(), *xL = zeros(), *xR = zeros(); + double env = 0, gcur = 1, e = 0, vlp = 0, dlp = 0, drp = 0, venv = 0, vwarm = 0; + const double k350 = 1 - exp(-2 * PI * 350 / SR); + const double aA = exp(-1 / (0.01 * SR)), aR = exp(-1 / (0.2 * SR)); + const double k7 = 1 - exp(-2 * PI * 7000 / SR); + for (long i = 0; i < N; i++) { + // sidechain: kick + 808 pump the sines and the replay guitars + e = fmax(duck[i], e * 0.99990); double pump = 1 - 0.85 * fmin(1, e), hpump = 1 - 0.5 * fmin(1, e); // v6.2: K-pop pump + // vocal leveler: RMS 4:1 over threshold, 10 ms / 200 ms (v6.3: harder, louder — she is the record) + double x = vox.L[i], pw = x * x; + env = pw > env ? aA * env + (1 - aA) * pw : aR * env + (1 - aR) * pw; + double lv = sqrt(env), gt = lv > 0.06 ? pow(lv / 0.06, 1.0 / 4 - 1) : 1; + gcur = gt < gcur ? gt : gcur + (gt - gcur) * 0.0005; + double v0 = x * gcur * 2.0 * (1 - 0.3 * fmin(1, e)); vlp += (v0 - vlp) * k7; // her voice rides the kick's pump, lightly + vwarm += (v0 - vwarm) * k350; + const double warm = warmA[i]; + double v = v0 + (v0 - vlp) * airA[i] * (1 - 0.85 * warm) * 1.6 + vwarm * 0.75 * warm; // air above 7 kHz; proximity when warm + // v6.3: HOLLOW — the bed is keyed to her: while she sings, sines, guitars, + // harmonies and halo step back (up to −6 dB, 10 ms in, 200 ms out via env) + venv = lv > venv ? venv + (lv - venv) * 0.02 : venv + (lv - venv) * 0.0006; + const double vd = 1 - 0.5 * fmin(1, venv / 0.05), hd = 1 - 0.3 * fmin(1, venv / 0.05); + double hv = sample(halo.L, halo.n, i) * 0.13 * hd; + // v6.4: THICK — two drifting copies of her lead, 28 and 41 ms late (the platter's + // unison spacing), each wandering ±1.2 ms so they detune a few cents, panned apart + double tt = (double)i / SR; + double d1 = (0.028 + 0.0012 * sin(2 * PI * 0.13 * tt)) * SR, d2 = (0.041 + 0.0012 * sin(2 * PI * 0.31 * tt + 1)) * SR; + long j1 = i - (long)d1, j2 = i - (long)d2; double f1 = d1 - floor(d1), f2 = d2 - floor(d2); + double c1 = (sample(vox.L, vox.n, j1) * (1 - f1) + sample(vox.L, vox.n, j1 - 1) * f1) * gcur * 2.0 * thickA[i]; + double c2 = (sample(vox.L, vox.n, j2) * (1 - f2) + sample(vox.L, vox.n, j2 - 1) * f2) * gcur * 2.0 * thickA[i]; + double thL = c1 * 0.42 + c2 * 0.18, thR = c1 * 0.18 + c2 * 0.42; + dlp = dL[i]; drp = dR[i]; // v5.1: kit full-band + forward (was 4.5 kHz lowpass) + double sl = tanh(sL[i] * 2.4 * 1.4) / tanh(1.4) * pump * vd, sr = tanh(sR[i] * 2.4 * 1.4) / tanh(1.4) * pump * vd; + double gl = (sample(gtr.L, gtr.n, i) * herA[i] * 0.8 * hpump + (sample(acg.L, acg.n, i) * acgA[i] * 0.55 + sample(elg.L, elg.n, i) * elgA[i] * 0.8) * pump) * vd; + double gr = (sample(gtr.R, gtr.n, i) * herA[i] * 0.8 * hpump + (sample(acg.R, acg.n, i) * acgA[i] * 0.55 + sample(elg.R, elg.n, i) * elgA[i] * 0.8) * pump) * vd; + double pl = (spL[i] + hiL[i] * pump) * vd, pr = (spR[i] + hiR[i] * pump) * vd; + double jl = jL[i] * pump, jr = jR[i] * pump; + double jd = (sample(jeff[0].L, jeff[0].n, i) * 0.7 + sample(jeff[1].L, jeff[1].n, i) * 0.4) * jg[i] * pump * hd; // the drone: centre, never moves + double chl = cL[i] * hpump * hd, chr = cR[i] * hpump * hd; + L[i] = (float)(v + thL + hL[i] * hpump * hd + hv * 0.6 * hpump + gl + dlp * 1.25 + tL[i] * 1.1 + pL[i] * 0.9 + b808[i] * 0.42 + sl * 0.85 + pl + exL[i] * 0.5 + jl + chl + jd); + R[i] = (float)(v + thR + hR[i] * hpump * hd + hv * 0.6 * hpump + gr + drp * 1.25 + tR[i] * 1.1 + pR[i] * 0.9 + b808[i] * 0.42 + sr * 0.85 + pr + exR[i] * 0.5 + jr + chr + jd); + // v6.4: SPACE — more of everything into the room, the doubles and the jets too + xL[i] = (float)(v * sendA[i] * 1.5 * (1 - 0.7 * warm) + thL * 0.8 + hL[i] * 0.5 + hv * 1.2 + sl * 0.5 + pl * 0.7 + dlp * 0.06 + gl * 0.1 + exL[i] * 0.8 + jl * 0.6 + chl * 0.9 + jd * 0.5); + xR[i] = (float)(v * sendA[i] * 1.5 * (1 - 0.7 * warm) + thR * 0.8 + hR[i] * 0.5 + hv * 1.2 + sr * 0.5 + pr * 0.7 + drp * 0.06 + gr * 0.1 + exR[i] * 0.8 + jr * 0.6 + chr * 0.9 + jd * 0.5); + } + // the room: near early reflections always on, a 2.2 s dark tail behind + float *wL = zeros(), *wR = zeros(); + fdn(xL, xR, wL, wR, 3.4, 0.45, 0.03); // v6.4: a bigger room + static const double ROOM[6][3] = { { 0.011, 0.16, 0 }, { 0.017, 0.14, 1 }, { 0.023, 0.11, 0 }, { 0.031, 0.10, 1 }, { 0.043, 0.07, 0 }, { 0.053, 0.06, 1 } }; + for (long i = N - 1; i >= 0; i--) { + double el = 0, er = 0; + for (int r = 0; r < 6; r++) { long j = i - lround(ROOM[r][0] * SR); if (j >= 0) { double y = (xL[j] + xR[j]) * ROOM[r][1]; if (ROOM[r][2] > 0) er += y; else el += y; } } + L[i] += (float)(wL[i] * 0.5 + el); R[i] += (float)(wR[i] * 0.5 + er); + } + // bass mono below 120 Hz (house rule) + { double ml = 0, mr = 0, k = 1 - exp(-2 * PI * 120 / SR); + for (long i = 0; i < N; i++) { ml += (L[i] - ml) * k; mr += (R[i] - mr) * k; double m = (ml + mr) / 2; L[i] += (float)(m - ml); R[i] += (float)(m - mr); } } + // v6.6: the record starts on her first sung word (the first charted note after 23 s), 60 ms early + long start = at(startSecOut); if (start < 0) start = 0; + long M = N - start; float *oL = L + start, *oR = R + start; + long fin = (long)(0.005 * SR), fout = SR; + for (long i = 0; i < fin; i++) { double g = 0.5 - 0.5 * cos(PI * i / fin); oL[i] *= (float)g; oR[i] *= (float)g; } + for (long i = 0; i < fout; i++) { double g = 0.5 - 0.5 * cos(PI * i / fout); oL[M - 1 - i] *= (float)g; oR[M - 1 - i] *= (float)g; } + double peak = 0; for (long i = 0; i < M; i++) { peak = fmax(peak, fabs(oL[i])); peak = fmax(peak, fabs(oR[i])); } + for (long i = 0; i < M; i++) { oL[i] *= (float)(0.7 / peak); oR[i] *= (float)(0.7 / peak); } + write_wav_f32(LANE "/out/sailor-song-" VERSION "-full.wav", oL, oR, M); + if (EV) { fprintf(EV, "\n]}\n"); fclose(EV); } + printf("✓ %s/out/sailor-song-" VERSION "-full.wav %.1f s (from %.2f s of the take, floor at bar %d), %d bars, %d notes\n", LANE, (double)M / SR, (double)start / SR, FLOOR_BAR, CHART_NBARS, CHART_NNOTES); + return 0; +} diff --git a/pop/sailor-song/c/sailorremix.c b/pop/sailor-song/c/sailorremix.c new file mode 100644 index 0000000000..796020d16f --- /dev/null +++ b/pop/sailor-song/c/sailorremix.c @@ -0,0 +1,1065 @@ +// sailorremix.c — "sailor song (outside remix)", the C engine. +// +// A cover of Gigi Perez's "Sailor Song", played by @jeffrey's niece: her +// voice and a capo-4 nylon guitar on one phone mic. This file is the score. +// It follows the /pop single-file renderer shape (pop/loner/c, +// pop/cult/c): the measuring side bakes, this engine arranges and mixes. +// +// THE DEAL bin/aesthetivox.py owns WORLD (her lead regulated onto G# +// minor in her guitar's +13¢ frame, a dark octave halo, five +// self-harmony stems); bin/lock-vox.mjs pulls every onset onto +// her own strum 8ths; bin/replay-guitar.mjs re-plays her part +// bar for bar on pop/guitar/c/strum; bin/bells.sh bakes a FEM +// bell bank (pop/bell/c); bin/chart.mjs emits sailor-chart.h — +// her bars, her snapped beats, her chords, her tuned notes. +// FOLLOW Nothing is warped to a grid. Every hit lands on HER strum +// points (her motif X..XX..X: 1, &2, 3, &4). The clock breathes +// from 110 to 125 because she does. +// POCKET kick 1 + &2, brush snare 3, rim &4 — her own strum points. +// TRAP a gliding 808 on the kick points tuned to her roots; bright +// 16th hats with 32nd / triplet rolls into every other bar +// line; a layered clap on 3. +// SINES voice-led sine choir — sub on the roots, a tenor trio under +// her, a high pair over her, her register (G#3–G#4) left clear +// — plus a descant, her melody shadowed a 3rd and an octave up, +// and her chorus hook re-laid over the guitar-only break. +// Saturated and pumped by the kick so they hold in a pop mix. +// AC FEM bells answer her in the breath gaps (her last three +// notes ring back); a vibraphone (pop/marimba's modal preset, +// ported) arpeggiates her chords on her 8ths. +// SPACE pop/nullabye/c/ac_hrtf.h: bells orbit overhead, the vibes +// sweep ear to ear, the highs float up and wide, and her +// harmonies stand at fixed places — in the bridge they rove. +// ROOM near early reflections always on, a 2.2 s dark FDN behind. +// Verses close and bright (air above 7 kHz), choruses roomier. +// +// Form (bars from sailor-chart.h, 2:59): +// intro 1–10 · verse1 11–27 · chorus1 28–43 · verse2 44–51 · +// chorus2 52–67 · break 68–71 · bridge 72–80 · outro 81–84 +// +// v6 (ANALYSIS.md, the platter reading of her take): +// START bedroom first. The record opens on her first strum (1.65 s), +// her guitar and then her voice alone, dry and close — nothing +// says electronic. From bar 16 the mix grows in one lane at a +// time (chamber-03 rule 1: one change per phrase): sub 16, pads +// + shaker 20, congas 22, a kick on her 1 and 3 only at 24, the +// pickup bass 24, hats 25, riser 26, and the floor lands whole +// with an explosion on chorus 1 (bar 28). +// CLOCK bin/regularize.mjs: her rubato is kept through the bedroom +// bars; from bar 24 the clock keeps 30% of her sway around +// 120 (verse 2 119, choruses 120–121, outro 122 instead of 130). +// Every stem and the chart go through the same time map. +// HOCKET her hand is the periodic necklace X..XX..X (1 &2 3 &4, LHL 0). +// The kit now plays her COMPLEMENT .XX..XX. (&1 2 &3 4): open hats +// on &1/&3, claps on 2/4, so every 8th is struck exactly once +// between her and the kit (rhythm-platter 06); the floor kick +// doubles her 1 and 3. Her pickups &2/&4 get only a soft accent +// and the house bass — the rim that fought her anacrusis is gone +// where she sings. +// BINS off-beat hits sit at her measured bin, +5 ms (rhythm 07): the +// necklace says which pulse, BIN_OFF says where in it. +// IMPACT chamber-04 rule 10: at every drop the whole ring is displaced +// and springs back (1.1 Hz, damping 1.2); kick and sub never +// move; an explosion (a noise chiff hurled once around the head) +// opens it. +// VOX ARPS her own harmony stems gated into arpeggios on her 8ths/16ths/ +// triplets, each step at its own seat — a different figure per +// section (down3 is the resident interval, up5 the rare one: +// chord-tone shares .60 / .35 against her chords). +// +// v8: NO DROPS. Everything below her is a sine (the beds, the sub, an +// offbeat sine bass, the halo/harmony/choir voices) plus a minimal electro +// kit — 808 kick, 808 clap, closed/open hats, a rim on her pickups — that +// grows in over twelve bars and then rides hills. No guitars but a trace +// of her own, no vibes, no bells, no hand percussion, no risers, jets, +// explosions or impacts. Tight, not humanized. +// +// Build: bash pop/sailor-song/c/build.sh +// Run: pop/sailor-song/c/sailorremix (from the repo root) +// → pop/sailor-song/out/sailor-song-v5-full.wav + .events.json +// bash pop/sailor-song/c/cut.sh → master (−10 LUFS) + mp3 w/ cover + +#include +#include +#include +#include +#include +#include + +#include "../../nullabye/c/ac_hrtf.h" +#include "sailor-chart.h" + +#define SR 48000 +#define VERSION "v19" +#define END_BAR 82 // v9: the record ends one bar after her last line, 1.5 s fade — no drawn-out tail +#define START_BAR 11 // v6.6: (the pre-roll is now her first WORD; see main) +#define FLOOR_BAR 52 // v10: the one chorus — the floor drops on its "kiss" +#define BIN_OFF 0.005 // v6: her off-beat bin, measured (ANALYSIS.md §2) +#define LANE "pop/sailor-song" +#define PI 3.14159265358979323846 + +// ── rng (seeded, so the same score renders the same bytes) ────────────── +static uint32_t rng_state = 0x5a11025u; +static double rnd(void) { // −1..1 + rng_state ^= rng_state << 13; rng_state ^= rng_state >> 17; rng_state ^= rng_state << 5; + return (double)rng_state / 2147483648.0 - 1.0; +} + +// ── WAV IO (the fleet loader, loner/cult's, stereo) ────────────────────── +typedef struct { float *L, *R; long n; } Stereo; +static Stereo load_wav(const char *path) { + Stereo s = { 0 }; + FILE *f = fopen(path, "rb"); + if (!f) { fprintf(stderr, " ! missing %s\n", path); return s; } + fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET); + uint8_t *buf = (uint8_t *)malloc(sz); + if (!buf || (long)fread(buf, 1, sz, f) != sz) { fclose(f); free(buf); return s; } + fclose(f); + long p = 12; int fmt = 1, ch = 1, bits = 16; long dOff = 0, dLen = 0; + while (p + 8 <= sz) { + uint32_t c = (uint32_t)buf[p+4] | (uint32_t)buf[p+5] << 8 | (uint32_t)buf[p+6] << 16 | (uint32_t)buf[p+7] << 24; + if (!memcmp(buf + p, "fmt ", 4)) { fmt = buf[p+8] | (buf[p+9] << 8); ch = buf[p+10] | (buf[p+11] << 8); bits = buf[p+22] | (buf[p+23] << 8); } + else if (!memcmp(buf + p, "data", 4)) { dOff = p + 8; dLen = c; if (dOff + dLen > sz) dLen = sz - dOff; break; } + p += 8 + c + (c & 1); + } + const int bps = bits / 8; const long frames = dLen / (bps * ch); + s.L = (float *)calloc(frames, sizeof(float)); s.R = (float *)calloc(frames, sizeof(float)); s.n = frames; + for (long i = 0; i < frames; i++) for (int c = 0; c < 2; c++) { + const long o = dOff + (i * ch + (c < ch ? c : 0)) * bps; double v; + if (fmt == 3 && bits == 32) { float x; memcpy(&x, buf + o, 4); v = x; } + else if (bits == 32) { int32_t x; memcpy(&x, buf + o, 4); v = x / 2147483648.0; } + else if (bits == 24) { int32_t x = buf[o] | (buf[o+1] << 8) | ((int32_t)(int8_t)buf[o+2] << 16); v = x / 8388608.0; } + else { int16_t x; memcpy(&x, buf + o, 2); v = x / 32768.0; } + (c ? s.R : s.L)[i] = (float)v; + } + free(buf); + return s; +} +static void write_wav_f32(const char *path, const float *L, const float *R, long n) { + FILE *f = fopen(path, "wb"); + if (!f) { fprintf(stderr, "! cannot write %s\n", path); exit(1); } + uint32_t dsz = (uint32_t)(n * 8), riff = 36 + dsz, sr = SR, br = SR * 8, fsz = 16; + uint16_t fmt = 3, ch = 2, ba = 8, bits = 32; + fwrite("RIFF", 1, 4, f); fwrite(&riff, 4, 1, f); fwrite("WAVE", 1, 4, f); + fwrite("fmt ", 1, 4, f); fwrite(&fsz, 4, 1, f); + fwrite(&fmt, 2, 1, f); fwrite(&ch, 2, 1, f); fwrite(&sr, 4, 1, f); + fwrite(&br, 4, 1, f); fwrite(&ba, 2, 1, f); fwrite(&bits, 2, 1, f); + fwrite("data", 1, 4, f); fwrite(&dsz, 4, 1, f); + for (long i = 0; i < n; i++) { fwrite(&L[i], 4, 1, f); fwrite(&R[i], 4, 1, f); } + fclose(f); +} +static float sample(const float *b, long len, long i) { return (b && i >= 0 && i < len) ? b[i] : 0.0f; } + +// ── the form + the arrangement ────────────────────────────────────────── +enum { INTRO, VERSE1, CHORUS1, VERSE2, CHORUS2, BREAK, BRIDGE, OUTRO, NSEC }; +static const char *SEC_NAME[NSEC] = { "intro", "verse1", "chorus1", "verse2", "chorus2", "break", "bridge", "outro" }; +static const int SEC_FROM[NSEC] = { 1, 11, 28, 44, 52, 68, 73, 81 }; // v7: "kiss me on the mouth" lands on the downbeat of 28 and 52; the bridge line on 73 +static int section_of(int bar) { int s = 0; for (int k = 0; k < NSEC; k++) if (bar >= SEC_FROM[k]) s = k; return s; } + +// her own harmonies: which stem sings, how loud, where (azimuth °) +enum { H_UP3, H_DOWN3, H_UP5, H_DOWN6, H_DOWN8, H_UP8, NHARM }; +static const char *HARM_FILE[NHARM] = { "harm-up3", "harm-down3", "harm-up5", "harm-down6", "harm-down8", "harm-up8" }; +static const double HARM_DELAY[NHARM] = { 0.018, 0.026, 0.022, 0.030, 0.012, 0.008 }; // a breath apart +typedef struct { double g, az; } HarmSet; + +typedef struct { + double her, acg, elg; // her guitar / acoustic replay / electric replay + double kick, snare, clap, rim, hat, trap, b808; + double sub, tenor, high, third, octave, descant, hook, vib, bell; + HarmSet harm[NHARM]; + double send, air; // vocal room send, air lift +} Arr; +#define H(i, g, az) [i] = { g, az } +static const Arr ARR[NSEC] = { + [INTRO] = { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // v6: bedroom — her guitar alone + { { 0 } }, .10, .3 }, + [VERSE1] = { .9, 0, 0, 0, 0, 0, 0, 0, .25, 0, .8, .7, .3, 0, 0, 0, 0, 0, 0, // v8: end state of the evolution (gated per bar below) + { H(H_DOWN3, .16, 40) }, .07, .45 }, + [CHORUS1] = { .5, 0, 0, 1, 0, .8, .5, 0, .5, 1, 1, 1, 1, .3, .2, 0, 0, 0, 0, + { H(H_UP3, .45, -50), H(H_DOWN3, .40, 50), H(H_DOWN6, .28, 0) }, .30, 1.0 }, // v16: harmonies up + [VERSE2] = { .45, 0, 0, .8, 0, 0, .35, 0, .2, .4, .7, .6, .3, 0, 0, 0, 0, 0, 0, // a verse, not a chorus + { H(H_DOWN3, .30, 45), H(H_UP3, .30, -45), H(H_DOWN6, .18, 0) }, .16, .8 }, // v19: harmonies come in with the cathedral + [CHORUS2] = { .5, 0, 0, 1.1, 0, .9, .5, 0, .6, 1, 1, 1, 1, .3, .3, 1, 0, 0, 0, + { H(H_UP3, .48, -60), H(H_DOWN3, .45, 60), H(H_DOWN6, .33, -20), H(H_DOWN8, .30, 0) }, .32, 1.0 }, + [BREAK] = { .6, 0, 0, 0, 0, 0, 0, 0, 0, 0, .8, 1, 1, 0, 0, .6, 1, 0, 0, + { { 0 } }, .20, .2 }, + [BRIDGE] = { .4, 0, 0, .8, 0, .5, .4, 0, .4, .8, 1, .9, .9, 0, .2, .5, 0, 0, 0, + { { 0 } }, .26, .9 }, // harmonies rove, see harm_at() + [OUTRO] = { .6, 0, 0, .5, 0, 0, .4, 0, 0, .3, .8, .8, .6, 0, 0, 0, 0, 0, 0, + { H(H_DOWN8, .30, 0), H(H_DOWN3, .22, 40), H(H_UP3, .16, -40) }, .30, .6 }, +}; +// "pitching around": in the bridge her harmony changes interval and place each bar +static double evo(int bar, int from, int to); +static HarmSet harm_at(int bar, int h) { + int s = section_of(bar); + if (s == VERSE1) { HarmSet r = ARR[s].harm[h]; r.g *= evo(bar, 18, 24); return r; } // v6: her harmony creeps in + if (h == H_UP8) { // v6.2: her voice an octave up + if (bar >= 36 && bar <= 43) return (HarmSet){ .85, 0 }; // second half of chorus 1 + if (bar >= 52 && bar <= 67) return (HarmSet){ 1.0, 0 }; // all of chorus 2 + if (bar >= 77 && bar <= 80) return (HarmSet){ 1.0, 0 }; // the bridge's climb + if (bar >= 81) return (HarmSet){ .6, 0 }; + return (HarmSet){ 0, 0 }; + } + if (s != BRIDGE) return ARR[s].harm[h]; + static const HarmSet ROVE[4][NHARM] = { + { H(H_UP3, .36, -60) }, { H(H_UP5, .30, 60) }, { H(H_DOWN3, .34, -35) }, { H(H_UP3, .30, 45), H(H_DOWN6, .24, -50) } }; + return ROVE[(bar - 73) % 4][h]; +} + +// ── buses ──────────────────────────────────────────────────────────────── +static long N; +static float *dL, *dR, *tL, *tR, *b808, *sL, *sR, *hiM, *bellM, *vibM, *duck; +static float *pL, *pR; // hand-percussion / bubble bus +static void add(float *b, long i, double v) { if (i >= 0 && i < N) b[i] += (float)v; } +static long at(double t) { return lround(t * SR); } +static double mtof(double m) { return 440.0 * pow(2.0, (m + CHART_TUNE - 69.0) / 12.0); } + +// score events → out/…events.json for the score video +static FILE *EV; +static int evFirst = 1; +static void ev(double t, const char *voice, double dur, double gain, int midi) { + if (!EV) return; + fprintf(EV, "%s{\"t\":%.3f,\"voice\":\"%s\",\"dur\":%.3f,\"gain\":%.2f%s", evFirst ? "" : ",\n", t, voice, dur, gain, ""); + if (midi >= 0) fprintf(EV, ",\"midi\":%d", midi); + fputc('}', EV); evFirst = 0; +} + +// ── the kit ────────────────────────────────────────────────────────────── +static void kick(double t, double g) { + long a = at(t); double ph = 0; + for (long i = 0; i < 0.36 * SR; i++) { double u = (double)i / SR; + ph += 2 * PI * (48 + 110 * exp(-u * 45)) / SR; // v5.1: a real pop kick — + double click = i < 0.004 * SR ? rnd() * 0.5 * (1 - u / 0.004) : 0; // beater click + double v = (tanh(sin(ph) * 1.6) * exp(-u * 6.5) + click) * g; add(dL, a + i, v); add(dR, a + i, v); } + for (long i = 0; i < 0.25 * SR; i++) add(duck, a + i, g * exp(-(double)i / SR * 9)); +} +static void snare(double t, double g) { // brushy: slow swell-in, dark noise, soft body + long a = at(t); double prev = 0, lp = 0, ph = 0; + for (long i = 0; i < 0.24 * SR; i++) { double u = (double)i / SR, nz = rnd(), hp = nz - prev; prev = nz; + lp += (hp - lp) * 0.45; ph += 2 * PI * (200 + 60 * exp(-u * 60)) / SR; // v5.1: crisp, not brushed + double v = (lp * exp(-u * 17) * 1.4 + sin(ph) * 0.55 * exp(-u * 26)) * g; + add(dL, a + i, v * 0.92); add(dR, a + i, v); } +} +static void bubble(double t, double g, double pan) { // v11: a bubble pop — sine 1400 → 380 Hz in 45 ms, a 1 ms click, gone in 90 ms + long a = at(t); double ph = 0, f0 = 1100 + rnd() * 300; + for (long i = 0; i < 0.09 * SR; i++) { double u = (double)i / SR; ph += 2 * PI * (380 + (f0 - 380) * exp(-u * 70)) / SR; + double v = (sin(ph) * exp(-u * 32) + (i < 0.001 * SR ? rnd() * 0.5 : 0)) * g; + add(pL, a + i, v * (1 - pan)); add(pR, a + i, v * (1 + pan)); } +} +static void rim(double t, double g) { + long a = at(t); + for (long i = 0; i < 0.07 * SR; i++) { double u = (double)i / SR; + double v = (sin(2 * PI * 1650 * u) * 0.5 + sin(2 * PI * 520 * u)) * exp(-u * 70) * g; + add(dL, a + i, v * 1.1); add(dR, a + i, v * 0.8); } +} +static void soft_hat(double t, double g, double pan) { + long a = at(t); double prev = 0; + for (long i = 0; i < 0.03 * SR; i++) { double nz = rnd(), hp = nz - prev; prev = nz; + double v = hp * exp(-(double)i / SR * 300) * g * 0.6; add(dL, a + i, v * (1 - pan)); add(dR, a + i, v * (1 + pan)); } +} +static void trap_hat(double t, double g, double pan) { // two-stage highpass: brighter, tighter + long a = at(t); double p1 = 0, p2 = 0; + for (long i = 0; i < 0.022 * SR; i++) { double nz = rnd(), h1 = nz - p1; p1 = nz; double h2 = h1 - p2; p2 = h1; + double v = h2 * exp(-(double)i / SR * 220) * g * 0.35; add(tL, a + i, v * (1 - pan)); add(tR, a + i, v * (1 + pan)); } +} +static void clap(double t, double g) { + static const double OFF[3] = { 0, 0.011, 0.022 }, AMP[3] = { 0.7, 0.8, 1.0 }; + for (int k = 0; k < 3; k++) { + long a = at(t + OFF[k]); double lp = 0, prev = 0; + for (long i = 0; i < 0.16 * SR; i++) { double u = (double)i / SR, nz = rnd(), hp = nz - prev; prev = nz; + lp += (hp - lp) * 0.45; + double v = lp * exp(-u * (k < 2 ? 90 : 16)) * g * AMP[k] * 0.9; add(tL, a + i, v); add(tR, a + i, v * 0.95); } + } +} +// 808: drops ~a 4th into the root, long tail, tanh so its harmonics speak +// above 180 Hz on a phone +static void eight08(double t, int midi, double dur, double g) { + long a = at(t); double f1 = mtof(midi), f0 = f1 * 1.33, ph = 0; + for (long i = 0; i < (dur + 0.15) * SR; i++) { double u = (double)i / SR; + ph += 2 * PI * (f1 + (f0 - f1) * exp(-u * 26)) / SR; + double env = fmin(1, u / 0.002) * (u > dur ? exp(-(u - dur) * 30) : exp(-u * 1.1)); + add(b808, a + i, tanh(sin(ph) * 3.4) * 0.7 * env * g); /* v5.1: drive 3.4 so harmonics carry it on phones */ } + for (long i = 0; i < 0.3 * SR; i++) add(duck, a + i, g * exp(-(double)i / SR * 7)); +} + +// ── sines ──────────────────────────────────────────────────────────────── +// slow swell, 4.5 Hz tremolo, a ±det¢ twin. R == NULL → mono (to a spatial bus) +static void sine(float *L, float *R, double t0, double dur, double midi, double g, double pan, + double atk, double rel, double det) { + double f = mtof(midi), fa = f * pow(2, det / 1200), fb = f * pow(2, -det / 1200); + double pa = (rnd() + 1) * 3, pb = (rnd() + 1) * 3, pc_ = 0, pa0 = pa, pb0 = pb; + double gl = g * sqrt((1 - pan) / 2), gr = g * sqrt((1 + pan) / 2); + long a = at(t0), len = (long)((dur + rel) * SR); + for (long i = 0; i < len; i++) { + double u = (double)i / SR; + double env = fmin(1, u / atk) * (u > dur ? exp(-(u - dur) / (rel / 3)) : 1); + // analog: each voice drifts on its own slow wander; a centre third voice; soft saturation + double drift = det > 0 ? 1 + 0.0012 * sin(2 * PI * 0.13 * u + pa0) + 0.0007 * sin(2 * PI * 0.31 * u + pb0) : 1; + pa += 2 * PI * fa * drift / SR; pb += 2 * PI * fb / drift / SR; pc_ += 2 * PI * f * drift / SR; + double trem = 1 - 0.08 * (0.5 + 0.5 * sin(2 * PI * 4.5 * u)); + double c3 = det > 0 ? sin(pc_) * 0.5 : 0, xa = tanh((sin(pa) + c3) * 1.5) / 1.0, xb = tanh((sin(pb) + c3) * 1.5) / 1.0; // v8.1: natural + if (!R) add(L, a + i, (xa + xb) * 0.5 * env * trem * g); + else { add(L, a + i, xa * env * trem * gl); add(R, a + i, xb * env * trem * gr); } + } +} +#define PAD(t, d, m, g, pan) sine(sL, sR, t, d, m, g, pan, 0.35, 0.9, 7) +static float *padM[3]; // v11: the tenor trio, one mono bus per voice, spatialized on a tour + +// ── AC voices ──────────────────────────────────────────────────────────── +// FEM bells from the baked bank (bin/bells.sh). Loner rule: ≤ E5, fade tails. +static Stereo BELLS[128]; +static void fem_bell(double t, int midi, double g) { + while (midi > 76) midi -= 12; + while (midi < 63) midi += 12; + if (!BELLS[midi].L) { + char p[256]; snprintf(p, sizeof p, LANE "/src/bells/bell-%d.wav", midi); + BELLS[midi] = load_wav(p); + if (!BELLS[midi].L) return; + long f = (long)(0.6 * SR); + for (long i = 0; i < f && i < BELLS[midi].n; i++) { double k = (double)i / f; + BELLS[midi].L[BELLS[midi].n - 1 - i] *= (float)k; BELLS[midi].R[BELLS[midi].n - 1 - i] *= (float)k; } + } + const Stereo *b = &BELLS[midi]; long a = at(t); + for (long i = 0; i < b->n; i++) add(bellM, a + i, (b->L[i] + b->R[i]) * 0.5 * g); + ev(t, "bell", 1.2, g, midi); +} +// vibraphone — pop/marimba's modal preset, ported: partials 1/4/10, decays +// 4.5/0.9/0.25 s, 5.5 Hz motor tremolo at 0.55; damper after `hold` +static void vib(double t, int midi, double g, double hold) { + static const double P[3] = { 1, 4, 10 }, AMP[3] = { 1, 0.22, 0.08 }, DEC[3] = { 4.5, 0.9, 0.25 }; + double f = mtof(midi); long a = at(t), len = (long)((hold + 0.5) * SR); + for (long i = 0; i < len; i++) { double u = (double)i / SR, v = 0; + for (int k = 0; k < 3; k++) if (f * P[k] < SR / 2) v += AMP[k] * sin(2 * PI * f * P[k] * u) * exp(-u / DEC[k]); + double damp = u > hold ? exp(-(u - hold) * 10) : 1; + double trem = 1 - 0.55 * (0.5 + 0.5 * sin(2 * PI * 5.5 * u)); + add(vibM, a + i, v * fmin(1, u / 0.001) * damp * trem * g * 0.6); } + ev(t, "vib", 0.25, g, midi); +} + +// ── harmony ────────────────────────────────────────────────────────────── +static const int CHORD_PCS[3][3] = { { 8, 11, 3 }, { 8, 11, 3 }, { 11, 3, 6 } }; // Emaj7 bars sound G#m (bass G#) +static const int ROOT[3] = { 44, 44, 47 }; // G#2, B2 +static const int SCALE[7] = { 8, 10, 11, 1, 3, 4, 6 }; // G# natural minor +static int is_pc(const int *pcs, int np, int m) { int pc = ((m % 12) + 12) % 12; for (int k = 0; k < np; k++) if (pcs[k] == pc) return 1; return 0; } +static int nearest(const int *pcs, int np, double target, int lo, int hi) { + int best = -1; for (int m = lo; m <= hi; m++) if (is_pc(pcs, np, m) && (best < 0 || fabs(m - target) < fabs(best - target))) best = m; + return best; +} +static void lead(int *voices, int nv, const int *pcs, int lo, int hi) { // nearest chord tone per voice, no doubling + int used[3] = { -1, -1, -1 }, nu = 0; + for (int v = 0; v < nv; v++) { + int freeP[3], nf = 0; + for (int k = 0; k < 3; k++) { int u = 0; for (int j = 0; j < nu; j++) if (used[j] == pcs[k]) u = 1; if (!u) freeP[nf++] = pcs[k]; } + int m = nf ? nearest(freeP, nf, voices[v], lo, hi) : nearest(pcs, 3, voices[v], lo, hi); + voices[v] = m; if (nu < 3) used[nu++] = ((m % 12) + 12) % 12; + } +} +static int third_above(int m) { + int pc = ((m % 12) + 12) % 12, oct = m / 12 - (m < 0), i = -1; + for (int k = 0; k < 7; k++) if (SCALE[k] == pc) i = k; + if (i < 0) return m + 3; + int t = SCALE[(i + 2) % 7]; + return oct * 12 + t + (t < pc ? 12 : 0); +} +static const ChartBar *bar_n(int n) { // v13: the bar numbered n (exact first — the arrangement reorders bars), else the first after it + for (int k = 0; k < CHART_NBARS; k++) if (CHART_BARS[k].n == n) return &CHART_BARS[k]; + for (int k = 0; k < CHART_NBARS; k++) if (CHART_BARS[k].n >= n) return &CHART_BARS[k]; + return &CHART_BARS[CHART_NBARS - 1]; +} +static int pos_n_after(int n, int from) { for (int k = from; k < CHART_NBARS; k++) if (CHART_BARS[k].n == n) return k + 1; return CHART_NBARS; } // v19: verse 1 now ends on a partial bar 27 too +static int pos_n(int n) { for (int k = 0; k < CHART_NBARS; k++) if (CHART_BARS[k].n == n) return k + 1; return CHART_NBARS; } // record position of a bar +static const ChartBar *bar_at(double t) { + for (int k = 0; k < CHART_NBARS; k++) if (t >= CHART_BARS[k].t && t < CHART_BARS[k].t + CHART_BARS[k].dur) return &CHART_BARS[k]; + return NULL; +} + +// ── automation: a per-bar target, glided over 150 ms ───────────────────── +typedef double (*BarValue)(const ChartBar *, int); +static float *automate(BarValue fn, int arg) { + float *a = (float *)malloc(N * sizeof(float)); double cur = -1, glide = 1 - exp(-1.0 / (0.15 * SR)); + const ChartBar *b = &CHART_BARS[0]; + for (long i = 0; i < N; i++) { + double t = (double)i / SR; + while (b < &CHART_BARS[CHART_NBARS - 1] && t >= b->t + b->dur) b++; + double tg = fn(b, arg); + cur = cur < 0 ? tg : cur + (tg - cur) * glide; a[i] = (float)cur; + } + return a; +} +#define ARRV(field) static double v_##field(const ChartBar *b, int x) { (void)x; return ARR[section_of(b->n)].field; } +ARRV(send) ARRV(air) ARRV(acg) ARRV(elg) +static double v_her(const ChartBar *b, int x) { (void)x; int s = section_of(b->n); double g = ARR[s].her; return s == VERSE1 ? g * evo(b->n, 13, 17) : g; } // v6.6: no guitar to start — it creeps in after her first line +// v6.6: warm — the opening vocal close to the mic: proximity below 350 Hz, no air, almost no room; eases out by the floor +// v7: the choir stands behind her in the choruses, the bridge and the outro; a breath of it in verse 2 +static double v_choir(const ChartBar *b, int x) { (void)x; int s = section_of(b->n); + return s == CHORUS1 ? 0.5 * evo(b->n, 30, 38) : s == VERSE2 ? 0.3 : s == CHORUS2 ? 0.95 : s == BRIDGE ? 0.7 : s == OUTRO ? 0.5 : 0; } +static double v_jeff(const ChartBar *b, int x) { (void)x; int s = section_of(b->n); + return s == CHORUS1 ? 0 : s == CHORUS2 ? 0.6 * evo(b->n, FLOOR_BAR + 1, FLOOR_BAR + 4) : s == BRIDGE ? 0.5 : s == OUTRO ? 0.3 : 0; } +// v11: the TOUR — seat assignments advance one seat every four bars, eased over the last half of the fourth bar +static double SEAT5[5] = { -70, -35, 0, 35, 70 }; +static double tour_az(double t, int voice) { + const ChartBar *b = bar_at(t); int step = b ? (b->n - 1) / 4 : 0; double x = b ? (t - b->t) / fmax(0.5, b->dur) : 0; + int s0 = (voice + step) % 5, s1 = (voice + step + 1) % 5; double e = (b && ((b->n - 1) % 4 == 3)) ? fmin(1, fmax(0, (x - 0.5) * 2)) : 0; + e = e * e * (3 - 2 * e); return SEAT5[s0] + (SEAT5[s1] - SEAT5[s0]) * e; +} +static double tour0(double t) { return tour_az(t, 0); } +static double tour1(double t) { return tour_az(t, 2); } +static double tour2(double t) { return tour_az(t, 4); } +// the SWAY (rule 4): every harmony seat leans ±22° at 0.18 Hz, each on its own phase +static double sway(double t, int h) { return 22 * sin(2 * PI * 0.18 * t + h * 1.3); } +// the choir ORBITS: its three seats make one lap every 32 s +static double orbit0(double t) { return -48 + 360 * t / 32; } +static double orbit1(double t) { return 0 + 360 * t / 32; } +static double orbit2(double t) { return 48 + 360 * t / 32; } +// the sister DALLIES around her: two slow wanders summed, never still, never far +static double dally_hi(double t) { return 55 * sin(2 * PI * t / 9) + 30 * sin(2 * PI * t / 23 + 1); } +static double dally_lo(double t) { return -55 * sin(2 * PI * t / 11 + 2) + 25 * sin(2 * PI * t / 19); } +static double dally_el(double t) { return 12 + 10 * sin(2 * PI * t / 13); } +// the hook ROTATES through the break: 8 turns over 16 s, quintic ease (nullabye) +static double ROT_T0 = 0; +static double rot_az(double t) { double x = fmin(1, fmax(0, (t - ROT_T0) / 16)); double e = x * x * x * (x * (6 * x - 15) + 10); return 360 * 8 * e; } +static double seat_l(double t) { (void)t; return -48; } +static double seat_c(double t) { (void)t; return 0; } +static double seat_r(double t) { (void)t; return 48; } +static double el_back(double t) { (void)t; return -8; } +static double v_sis_hi(const ChartBar *b, int x) { (void)x; int s = section_of(b->n); return s == CHORUS2 ? 0.5 * evo(b->n, FLOOR_BAR + 3, FLOOR_BAR + 7) : s == BRIDGE ? 0.5 : s == OUTRO ? 0.45 : 0; } +static double v_sis_lo(const ChartBar *b, int x) { (void)x; int s = section_of(b->n); return s == VERSE2 ? 0.3 * evo(b->n, 46, 50) : s == CHORUS2 ? 0.45 : s == BRIDGE ? 0.5 : s == OUTRO ? 0.4 : 0; } +static double v_sis_hum(const ChartBar *b, int x) { (void)x; int s = section_of(b->n); return s == VERSE1 ? 0.3 * evo(b->n, 16, 22) : s == VERSE2 ? 0.4 : s == CHORUS2 ? 0.35 : s == BREAK ? 0.55 : s == BRIDGE ? 0.45 : s == OUTRO ? 0.45 : 0; } +// v19: the distance arc, one slow build over the record (by record position, not section): +// she starts quiet and right next to you — dry, intimate, "in the car" — comes up to full +// level across verse 1, then the room opens a little every bar: the dry closeness gives +// way and a long stone tail grows under everything until it is a stadium by the end +static int rec_pos(const ChartBar *b) { return (int)(b - CHART_BARS) + 1; } +static double v_far(const ChartBar *b, int x) { (void)x; return rec_pos(b) < pos_n(11) ? 1 : rec_pos(b) < pos_n(44) ? 1 - evo(b->n, 13, 25) : 0; } // far = quiet +static double v_cath(const ChartBar *b, int x) { (void)x; return 1.3 * evo(rec_pos(b), pos_n(22), CHART_NBARS - 6); } +static double v_warm(const ChartBar *b, int x) { (void)x; return 1 - 0.85 * evo(rec_pos(b), pos_n(20), CHART_NBARS - 6); } +static double v_thick(const ChartBar *b, int x) { (void)x; int s = section_of(b->n); return s == INTRO ? 0 : s == VERSE1 ? 0.25 * evo(b->n, 16, 24) : s == BREAK ? 0 : s == VERSE2 ? 0.4 : 0.5; } // v15: the doubles trimmed so the lead stays in front +static double v_harm_g(const ChartBar *b, int h) { return harm_at(b->n, h).g; } +static double v_harm_az(const ChartBar *b, int h) { return harm_at(b->n, h).az; } + +// ── space: ac_hrtf over a whole bus ───────────────────────────────────── +static double impact_disp(double t); +static double ring_pull(double t); +typedef double (*Path)(double t); +static void spatialize(const float *mono, const float *azA, Path az, Path el, double dist, float *L, float *R, double g) { + ACHrtf h; memset(&h, 0, sizeof h); + for (long i = 0; i < N; i++) { + double t = (double)i / SR, a = azA ? azA[i] : az(t); + float l, r; + a += impact_disp(t); // v6: the ring is displaced and springs back + double dd = dist * ring_pull(t); if (dd < 0.35) dd = 0.35; // v11: implosions pull every seat in, then it springs back + ac_hrtf_process(&h, mono[i], a * PI / 180, el(t) * PI / 180, dd, &l, &r); + L[i] += l * (float)g; R[i] += r * (float)g; + } +} +static double orbit_az(double t) { return 70 * sin(2 * PI * t / 24); } +static double orbit_el(double t) { return 25 + 10 * sin(2 * PI * t / 17); } +static double sweep_az(double t) { return 65 * sin(2 * PI * t / 8); } +static double wide_az(double t) { return 80 * sin(2 * PI * t / 31 + 1); } +static double el_zero(double t) { (void)t; return 0; } +static double el_up(double t) { (void)t; return 35; } +static double el_near(double t) { (void)t; return 5; } + +// ── v6 · impact (chamber-04 rule 10): displace every ring seat, spring back ── +// kick and sub never move (they are not on a spatial bus); ring tones open +// with a short noise event — the explosion below. +static const int IMPACT_BARS[] = { 52, 73 }; // v11: IMPLOSIONS at the chorus and the bridge (chamber-04 rule 10, inverted) +#define VOICE_LAG 0.05 // v7: her sung onset sits ~50 ms behind the strum (ANALYSIS.md §2); the explosion waits for it +// v6: the evolution — 0 before `from`, 1 at `to`, cosine between (one lane per phrase) +static double evo(int bar, int from, int to) { if (bar < from) return 0; if (bar >= to) return 1; double x = (double)(bar - from) / (to - from); return 0.5 - 0.5 * cos(PI * x); } +#define NIMPACT ((int)(sizeof IMPACT_BARS / sizeof *IMPACT_BARS)) +static double IMPACT_T[NIMPACT]; +static double impact_disp(double t) { // degrees of azimuth displacement + double d = 0; + for (int k = 0; k < NIMPACT; k++) { double u = t - IMPACT_T[k]; + if (u >= 0 && u < 6) d += 110 * exp(-1.2 * u) * sin(2 * PI * 1.1 * u); } + return d; +} +static double last_impact(double t) { double best = -1e9; for (int k = 0; k < NIMPACT; k++) if (IMPACT_T[k] <= t && IMPACT_T[k] > best) best = IMPACT_T[k]; return best; } +// the explosion: one full lap around the head in 0.7 s, climbing overhead +static double next_impact(double t) { double best = 1e9; for (int k = 0; k < NIMPACT; k++) if (IMPACT_T[k] >= t && IMPACT_T[k] < best) best = IMPACT_T[k]; return best; } +// the implosion: 1.5 laps spiralling IN from the far ring to the centre over the 0.9 s before the downbeat, coming down from overhead +static double expl_az(double t) { double u = next_impact(t) - t; return u > 0.9 ? 0 : 540 * u / 0.9; } +static double expl_el(double t) { double u = next_impact(t) - t; return u > 0.9 ? 0 : 55 * u / 0.9; } +// every seat's distance: pulled in over the 0.9 s before an implosion (to 0.45 of its place), then springs back at 1.1 Hz +static double ring_pull(double t) { + double p = 1; + for (int k = 0; k < NIMPACT; k++) { double u = t - IMPACT_T[k]; + if (u >= -0.9 && u < 0) { double x = (u + 0.9) / 0.9; p -= 0.55 * x * x; } + else if (u >= 0 && u < 5) p += 0.35 * exp(-1.2 * u) * cos(2 * PI * 1.1 * u) - 0.35 * exp(-1.2 * u); } + return p; +} +static float *exM, *hookM; +static void explosion(double t, double g) { // v11: an IMPLOSION — a reversed whoosh: bandpass rising 250 Hz → 6 kHz over the 0.9 s before t, cut dead on the downbeat, then a sub thump + long a = at(t - 0.9); double b1 = 0, b2 = 0, ph = 0; + for (long i = 0; i < 0.9 * SR; i++) { double x = (double)i / (0.9 * SR), fc = 250 * pow(24, x), k = 1 - exp(-2 * PI * fc / SR); + double nz = rnd(); b1 += (nz - b1) * k; b2 += (b1 - b2) * k; + double env = x * x * x * (x > 0.985 ? (1 - x) / 0.015 : 1); + add(exM, a + i, (b1 - b2) * env * g * 2.4); } + a = at(t); + for (long i = 0; i < 0.5 * SR; i++) { double u = (double)i / SR; ph += 2 * PI * (38 + 90 * exp(-u * 20)) / SR; + double v = sin(ph) * exp(-u * 7) * g * 0.7; add(dL, a + i, v); add(dR, a + i, v); } + ev(t, "impact", 1.0, g, -1); +} + +// ── v6 · vocal arpeggios: her harmony stems gated into figures on her grid ── +// -1 = rest. div = steps per beat (2 = 8ths, 3 = triplets, 4 = 16ths). +typedef struct { int div; int len; int step[8]; double g; double hold; } Arp; // hold: step lengths each note sings (v6.1: > 1 so the stack rolls, never chops) +static const Arp ARP[NSEC] = { + [INTRO] = { 0 }, [VERSE1] = { 0 }, + [CHORUS1] = { 2, 4, { H_UP3, H_DOWN3, H_UP3, H_DOWN6 }, .45, 1.8 }, // 8ths, a rocking third, each held 1.8 steps + [VERSE2] = { 0 }, // the sustained down3 sings here + [CHORUS2] = { 4, 8, { H_DOWN3, H_UP3, H_UP5, H_DOWN6, H_UP3, H_DOWN3, H_DOWN8, H_UP3 }, .40, 3.0 }, // 16ths, up then down, three steps deep + [BREAK] = { 0 }, + [BRIDGE] = { 3, 3, { H_UP3, H_DOWN3, H_DOWN6 }, .30, 2.0 }, // triplets, roving seats + [OUTRO] = { 2, 2, { H_DOWN8, H_UP3 }, .22, 2.0 }, +}; +static const double SEAT[5] = { -70, -35, 0, 35, 70 }; +static float *arpG[NHARM], *arpAz[NHARM]; +static void arp_step(int h, double t0, double t1, double g, double az, double hold) { + double step = t1 - t0; long a = at(t0), z = at(t0 + step * hold), rel = (long)(0.09 * SR); // 12 ms in, 90 ms out + for (long i = a; i < z + rel && i < N; i++) { double u = (double)(i - a) / SR, e = fmin(1, u / 0.012); + if (i >= z) e *= exp(-(double)(i - z) / rel * 4); + if (i >= 0) { arpG[h][i] = (float)fmin(g, arpG[h][i] + e * g); if (arpAz[h][i] == 0) arpAz[h][i] = (float)az; } } +} + +// ── reverb: 8-line FDN (Hadamard feedback, damped) ────────────────────── +static void fdn(const float *inL, const float *inR, float *oL, float *oR, double t60, double damp, double pre) { + static const int LENS[8] = { 1433, 1601, 1867, 2053, 2251, 2399, 2617, 2797 }; + float *buf[8]; int len[8], idx[8] = { 0 }; double lp[8] = { 0 }, gain[8], o[8], hh[8]; + for (int k = 0; k < 8; k++) { len[k] = (int)lround(LENS[k] * (double)SR / 44100); buf[k] = (float *)calloc(len[k], sizeof(float)); + gain[k] = pow(10, -3.0 * len[k] / (SR * t60)); } + long P = lround(pre * SR); + for (long i = 0; i < N; i++) { + double xl = i >= P ? inL[i - P] : 0, xr = i >= P ? inR[i - P] : 0; + for (int k = 0; k < 8; k++) { lp[k] += (buf[k][idx[k]] - lp[k]) * (1 - damp); o[k] = lp[k]; } + double a0 = o[0] + o[1], a1 = o[0] - o[1], a2 = o[2] + o[3], a3 = o[2] - o[3], a4 = o[4] + o[5], a5 = o[4] - o[5], a6 = o[6] + o[7], a7 = o[6] - o[7]; + double b0 = a0 + a2, b1 = a1 + a3, b2 = a0 - a2, b3 = a1 - a3, b4 = a4 + a6, b5 = a5 + a7, b6 = a4 - a6, b7 = a5 - a7; + hh[0] = b0 + b4; hh[1] = b1 + b5; hh[2] = b2 + b6; hh[3] = b3 + b7; hh[4] = b0 - b4; hh[5] = b1 - b5; hh[6] = b2 - b6; hh[7] = b3 - b7; + for (int k = 0; k < 8; k++) { buf[k][idx[k]] = (float)(hh[k] * 0.35355 * gain[k] + (k % 2 ? xr : xl) * 0.5); idx[k] = (idx[k] + 1) % len[k]; } + oL[i] = (float)(o[0] + o[2] + o[4] + o[6]); oR[i] = (float)(o[1] + o[3] + o[5] + o[7]); + } + for (int k = 0; k < 8; k++) free(buf[k]); +} + +static float *zeros(void) { return (float *)calloc(N, sizeof(float)); } + +// humanize: ±4 ms, ±12 % — a hand, not a sequencer ("a bit midi like") +static double hum_t(double t) { return t + rnd() * 0.004; } +static double hum_g(double g) { return g * (1 + rnd() * 0.12); } +static double hum_t2(double t, double ms) { return t + rnd() * ms / 1000; } // v6.4: looser hands +static double hum_g2(double g, double pct) { return g * (1 + rnd() * pct); } + +// ── the dance layer (v5.2: "turn the song into a dance mix") ───────────── +// Her ~120 is house tempo, so four on HER floor: a kick on every one of her +// beats, claps 2 + 4, open hats on every offbeat, an offbeat house bass on +// her roots, risers into the drops. +static void dance_kick(double t, double g) { + long a = at(t); double ph = 0; + for (long i = 0; i < 0.42 * SR; i++) { double u = (double)i / SR; + ph += 2 * PI * (50 + 160 * exp(-u * 38) + 40 * exp(-u * 300)) / SR; + double click = i < 0.003 * SR ? rnd() * 0.6 * (1 - u / 0.003) : 0; + double v = (tanh(sin(ph) * 2.4) * 0.85 * exp(-u * 5.5) + click) * g; + add(dL, a + i, v); add(dR, a + i, v); } + for (long i = 0; i < 0.3 * SR; i++) add(duck, a + i, g * 1.6 * exp(-(double)i / SR * 8)); +} +static void open_hat(double t, double g) { + long a = at(t); double p1 = 0, p2 = 0; + for (long i = 0; i < 0.16 * SR; i++) { double u = (double)i / SR, nz = rnd(), h1 = nz - p1; p1 = nz; double h2 = h1 - p2; p2 = h1; + double v = h2 * fmin(1, u / 0.002) * exp(-u * 16) * g * 0.3; add(tL, a + i, v * 0.8); add(tR, a + i, v * 1.1); } +} +// house bass: a saturated sine pluck with a closing lowpass, on the offbeat +static void house_bass(double t, int midi, double dur, double g) { + long a = at(t); double f = mtof(midi), ph = 0, lp = 0; + for (long i = 0; i < (dur + 0.05) * SR; i++) { double u = (double)i / SR; + ph += 2 * PI * f / SR; + double raw = tanh(sin(ph) * 4.0 + 0.6 * sin(2 * ph)); + double fc = 180 + 2400 * exp(-u * 14), k = 1 - exp(-2 * PI * fc / SR); + lp += (raw - lp) * k; + double env = fmin(1, u / 0.003) * (u > dur ? exp(-(u - dur) * 60) : 1); + add(b808, a + i, lp * env * g); } +} +// riser: white noise through a rising bandpass, swelling into the downbeat +static void riser(double t0, double t1, double g) { + long a = at(t0), len = at(t1) - a; double b1 = 0, b2 = 0; + for (long i = 0; i < len; i++) { double x = (double)i / len, fc = 300 * pow(40, x), k = 1 - exp(-2 * PI * fc / SR); + double nz = rnd(); b1 += (nz - b1) * k; b2 += (b1 - b2) * k; + double v = (b1 - b2) * x * x * g; add(tL, a + i, v * (1 - 0.3 * sin(PI * 8 * x))); add(tR, a + i, v * (1 + 0.3 * sin(PI * 8 * x))); } +} +// 4otf clap ohat hbass (v5.2) +static const double DANCE[NSEC][4] = { + [INTRO] = { 0, 0, 0, 0 }, [VERSE1] = { .8, 0, .5, .7 }, [CHORUS1] = { 1.1, 1, .9, 1.1 }, [VERSE2] = { .8, .4, .5, .7 }, + [CHORUS2] = { 1.15, 1, 1, 1.1 }, [BREAK] = { 0, 0, 0, 0 }, [BRIDGE] = { 1, .8, .8, .9 }, [OUTRO] = { .7, .4, .3, .5 } }; + +// ── hand percussion (v5.1: "we have no actual percussion yet") ─────────── +// shaker: band-passed noise with a swell-in (the bead mass lags the hand) +static void shaker(double t, double g, double pan) { + long a = at(t); double b1 = 0, b2 = 0, prev = 0; + for (long i = 0; i < 0.07 * SR; i++) { double u = (double)i / SR, nz = rnd(), hp = nz - prev; prev = nz; + b1 += (hp - b1) * 0.55; b2 += (b1 - b2) * 0.55; + double env = fmin(1, u / 0.012) * exp(-u * 45); + double v = (b1 - b2 * 0.6) * env * g; add(pL, a + i, v * (1 - pan)); add(pR, a + i, v * (1 + pan)); } +} +// tambourine: jingles = noise through three metallic partials, a short hit + ring +static void tamb(double t, double g, double pan) { + static const double F[3] = { 7040, 9360, 11870 }; + long a = at(t); double prev = 0; + for (long i = 0; i < 0.22 * SR; i++) { double u = (double)i / SR, nz = rnd(), hp = nz - prev, v = 0; prev = nz; + for (int k = 0; k < 3; k++) v += sin(2 * PI * F[k] * u + k) * 0.25; + v = (v * (0.6 + 0.4 * nz) + hp * 0.5) * exp(-u * 18) * g; + add(pL, a + i, v * (1 - pan)); add(pR, a + i, v * (1 + pan)); } +} +// conga: a sine membrane with a pitch drop, tuned to the chord; slap = + noise +static void conga(double t, int midi, double g, double pan, int slap) { + long a = at(t); double f = mtof(midi), ph = 0, prev = 0; + for (long i = 0; i < 0.3 * SR; i++) { double u = (double)i / SR, nz = rnd(), hp = nz - prev; prev = nz; + ph += 2 * PI * f * (1 + 0.35 * exp(-u * 60)) / SR; + double v = sin(ph) * exp(-u * (slap ? 22 : 11)) + (slap ? hp * exp(-u * 80) * 0.8 : 0); + v *= g; add(pL, a + i, v * (1 - pan)); add(pR, a + i, v * (1 + pan)); } +} +// woodblock: two short resonances, for the tresillo x..x..x. (E(3,8)) that +// runs 3-against-her-2 across the bar (rhythm-platter 01: the Cuban tresillo) +static void block(double t, double g, double pan) { + long a = at(t); + for (long i = 0; i < 0.05 * SR; i++) { double u = (double)i / SR; + double v = (sin(2 * PI * 1180 * u) * 0.7 + sin(2 * PI * 2350 * u) * 0.4) * exp(-u * 110) * g; + add(pL, a + i, v * (1 - pan)); add(pR, a + i, v * (1 + pan)); } +} +// the jet: decorrelated white noise through a lowpass that opens 300 Hz → 9 kHz +// over the span, amplitude rising as x^1.6 — an airplane taking off into the drop +static float *jL, *jR; +static void jet(double t0, double t1, double g) { + long a = at(t0), len = at(t1) - a; double l1 = 0, l2 = 0, r1 = 0, r2 = 0; + for (long i = 0; i < len; i++) { double x = (double)i / len, fc = 300 * pow(30, x), k = 1 - exp(-2 * PI * fc / SR); + double nl = rnd(), nr = rnd(); l1 += (nl - l1) * k; l2 += (l1 - l2) * k; r1 += (nr - r1) * k; r2 += (r1 - r2) * k; + double env = pow(x, 1.6) * g; add(jL, a + i, l2 * env); add(jR, a + i, r2 * env); } +} +// air: a steady wide hiss (high-passed noise) under the choruses, pumped by the kick in the mix +static void air(double t0, double t1, double g) { + long a = at(t0), len = at(t1) - a; double pl1 = 0, pr1 = 0; + for (long i = 0; i < len; i++) { double nl = rnd(), nr = rnd(), hl = nl - pl1, hr = nr - pr1; pl1 = nl; pr1 = nr; + double x = (double)i / len, env = fmin(1, x / 0.1) * fmin(1, (1 - x) / 0.1) * g; + add(jL, a + i, hl * env * 0.5); add(jR, a + i, hr * env * 0.5); } +} +// shaker tamb conga +static const double PERC[NSEC][3] = { + [INTRO] = { 0, 0, 0 }, [VERSE1] = { 0, 0, 0 }, [CHORUS1] = { 0, 0, 0 }, [VERSE2] = { 0, 0, 0 }, + [CHORUS2] = { 0, 0, 0 }, [BREAK] = { 0, 0, 0 }, [BRIDGE] = { 0, 0, 0 }, [OUTRO] = { 0, 0, 0 } }; + +// ── pop acoustic chain for her guitar (v5.1: "doesn't feel poppy") ─────── +// RBJ biquads: HP 110 Hz · −4 dB bell at 400 Hz (the box) · +4 dB shelf at +// 10 kHz (sparkle) → fast 4:1 compressor (snap + sustain) → M/S width 1.5. +typedef struct { double b0, b1, b2, a1, a2, x1, x2, y1, y2; } Biquad; +static Biquad bq(int type, double f, double q, double db) { + double A = pow(10, db / 40), w = 2 * PI * f / SR, c = cos(w), sn = sin(w), al = sn / (2 * q), b0, b1, b2, a0, a1, a2; + if (type == 0) { b0 = (1 + c) / 2; b1 = -(1 + c); b2 = b0; a0 = 1 + al; a1 = -2 * c; a2 = 1 - al; } // highpass + else if (type == 1) { b0 = 1 + al * A; b1 = -2 * c; b2 = 1 - al * A; a0 = 1 + al / A; a1 = -2 * c; a2 = 1 - al / A; } // bell + else { double sq = 2 * sqrt(A) * al; // high shelf + b0 = A * ((A + 1) + (A - 1) * c + sq); b1 = -2 * A * ((A - 1) + (A + 1) * c); b2 = A * ((A + 1) + (A - 1) * c - sq); + a0 = (A + 1) - (A - 1) * c + sq; a1 = 2 * ((A - 1) - (A + 1) * c); a2 = (A + 1) - (A - 1) * c - sq; } + Biquad b = { b0 / a0, b1 / a0, b2 / a0, a1 / a0, a2 / a0, 0, 0, 0, 0 }; + return b; +} +static double bq_run(Biquad *b, double x) { + double y = b->b0 * x + b->b1 * b->x1 + b->b2 * b->x2 - b->a1 * b->y1 - b->a2 * b->y2; + b->x2 = b->x1; b->x1 = x; b->y2 = b->y1; b->y1 = y; return y; +} +// v6: bedroom leveler — her opening strums are 20 dB under her playing at +// bar 11. Before bar 16 the guitar rides a slow RMS leveler (target −18 dBFS, +// up to +16 dB, 30 ms / 500 ms) that eases back to unity by UNTIL. +static void bedroom_level(Stereo *g, double until) { + if (!g->L) return; + double env = 0, gain = 1; const double aA = exp(-1 / (0.03 * SR)), aR = exp(-1 / (0.5 * SR)), target = 0.126; + for (long i = 0; i < g->n; i++) { + double t = (double)i / SR, m = (g->L[i] + g->R[i]) / 2, pw = m * m; + env = pw > env ? aA * env + (1 - aA) * pw : aR * env + (1 - aR) * pw; + double want = env > 1e-7 ? fmin(6.3, target / sqrt(env)) : 6.3; // ≤ +16 dB + double w = t >= until ? 0 : t < until - 8 ? 1 : 0.5 + 0.5 * cos(PI * (t - (until - 8)) / 8); + gain += ((1 + (want - 1) * w) - gain) * 0.0005; + g->L[i] *= (float)gain; g->R[i] *= (float)gain; + } +} +static void pop_guitar(Stereo *g, double width) { + if (!g->L) return; + for (int ch = 0; ch < 2; ch++) { + float *x = ch ? g->R : g->L; + Biquad hp = bq(0, 110, 0.707, 0), box = bq(1, 400, 1.0, -4), air = bq(2, 10000, 0.707, 4); + double env = 0, gc = 1, aA = exp(-1 / (0.003 * SR)), aR = exp(-1 / (0.08 * SR)); + double rms = 0; for (long i = 0; i < g->n; i++) rms += (double)x[i] * x[i]; + double th = sqrt(rms / g->n) * 1.2; + for (long i = 0; i < g->n; i++) { + double y = bq_run(&air, bq_run(&box, bq_run(&hp, x[i]))); + double lv = fabs(y); env = lv > env ? aA * env + (1 - aA) * lv : aR * env + (1 - aR) * lv; + double gt = env > th ? pow(env / th, 1.0 / 4 - 1) : 1; + gc += (gt - gc) * 0.02; + x[i] = (float)(y * gc * 1.8); + } + } + for (long i = 0; i < g->n; i++) { double m = (g->L[i] + g->R[i]) / 2, sd = (g->L[i] - g->R[i]) / 2 * width; + g->L[i] = (float)(m + sd); g->R[i] = (float)(m - sd); } +} + +int main(void) { + // ── her ── + // v6: stems on the regularized clock (bin/regularize.mjs → src/vox/reg); SAILOR_STEMS=locked to follow her raw + // v9: the edit (bin/splice.mjs) lives in src/vox/cut — played by default when it exists + const char *stems = getenv("SAILOR_STEMS") ? getenv("SAILOR_STEMS") : (access(LANE "/src/vox/cut/vocals-aesthetivox.wav", F_OK) == 0 ? "cut" : "reg"); + const int regd = strcmp(stems, "reg") == 0 || strcmp(stems, "cut") == 0; // raw + locked: her guitar from src/vox + char pth[512]; + #define STEM(name) (snprintf(pth, sizeof pth, LANE "/src/vox/%s/%s.wav", stems, name), pth) + #define GSTEM(name) (regd ? STEM(name) : (snprintf(pth, sizeof pth, LANE "/src/vox/%s.wav", name), pth)) + // v19: her recorded voice, gently pitch-mapped (bin/natural-vox.py) — not the WORLD resynthesis + snprintf(pth, sizeof pth, LANE "/src/vox/%s/vocals-natural.wav", stems); + Stereo vox = load_wav(access(pth, F_OK) == 0 ? pth : STEM("vocals-aesthetivox")); + Stereo halo = load_wav(STEM("vocals-halo")); + Stereo gtr = load_wav(GSTEM("guitar-48k")); + Stereo acg = load_wav(GSTEM("replay-acoustic")); + Stereo elg = load_wav(GSTEM("replay-electric")); + Stereo harm[NHARM]; + for (int h = 0; h < NHARM; h++) harm[h] = load_wav(STEM(HARM_FILE[h])); + // v7: accompaniment voices (bin/choir.py) — her own vocoder choir and @jeffrey's vowel drone + static const char *CHOIR_FILE[3] = { "choir-low", "choir-mid", "choir-high" }; + Stereo choir[3], jeff[2]; + for (int c = 0; c < 3; c++) choir[c] = load_wav(STEM(CHOIR_FILE[c])); + jeff[0] = load_wav(STEM("jeffrey-root")); jeff[1] = load_wav(STEM("jeffrey-fifth")); + Stereo sisHi = load_wav(STEM("sister-high")), sisLo = load_wav(STEM("sister-low")), sisHum = load_wav(STEM("sister-hum")); // v11: bin/sister.py + fprintf(stderr, "stems: %s\n", stems); + // v7: her first sung word = the first 30 ms window of the lead stem after bar START_BAR + // above −45 dBFS RMS (guitar bleed in the stem sits near −60; her voice enters near −35) + double startSecOut = bar_n(START_BAR)->t; + { long w = (long)(0.03 * SR), from = at(bar_n(START_BAR)->t); + for (long i = from; i + w < vox.n; i += w / 3) { double acc = 0; for (long j = 0; j < w; j++) acc += (double)vox.L[i + j] * vox.L[i + j]; + if (sqrt(acc / w) > 0.0056) { startSecOut = (double)i / SR - 0.06; break; } } } + if (!vox.L || !gtr.L) { fprintf(stderr, "! run bin/aesthetivox.py + bin/lock-vox.mjs first\n"); return 1; } + N = vox.n; + pL = zeros(); pR = zeros(); + bedroom_level(>r, bar_n(16)->t); // v6: until bar 16 + pop_guitar(>r, 1.5); pop_guitar(&acg, 1.3); + dL = zeros(); dR = zeros(); tL = zeros(); tR = zeros(); b808 = zeros(); sL = zeros(); sR = zeros(); + hiM = zeros(); bellM = zeros(); vibM = zeros(); duck = zeros(); exM = zeros(); jL = zeros(); jR = zeros(); hookM = zeros(); + for (int v = 0; v < 3; v++) padM[v] = zeros(); + for (int h = 0; h < NHARM; h++) { arpG[h] = zeros(); arpAz[h] = zeros(); } + for (int q = 0; q < NIMPACT; q++) IMPACT_T[q] = -1e9; + EV = fopen(LANE "/out/sailor-song-" VERSION ".events.json", "w"); + if (EV) fprintf(EV, "{\"tempoBPM\":120,\"seconds\":%.2f,\"startSec\":%.3f,\"startBar\":%d,\"floorBar\":%d,\"endBar\":%d,\"stems\":\"%s\",\"sections\":[", (double)N / SR, startSecOut, START_BAR, FLOOR_BAR, END_BAR, stems); + if (EV) for (int s = 0; s < NSEC; s++) { + // v13: a section is the longest contiguous run of bars carrying its numbers (the arrangement reorders bars) + int lo = SEC_FROM[s], hi = s + 1 < NSEC ? SEC_FROM[s + 1] - 1 : 999, bi = -1, bl = 0; + for (int k = 0; k < CHART_NBARS;) { if (CHART_BARS[k].n < lo || CHART_BARS[k].n > hi) { k++; continue; } + int j = k; while (j < CHART_NBARS && CHART_BARS[j].n >= lo && CHART_BARS[j].n <= hi) j++; + if (j - k > bl) { bl = j - k; bi = k; } k = j; } + if (bi < 0) continue; + const ChartBar *a = &CHART_BARS[bi], *z = &CHART_BARS[bi + bl - 1]; + fprintf(EV, "%s{\"name\":\"%s\",\"start\":%.3f,\"end\":%.3f}", s ? "," : "", SEC_NAME[s], a->t, z->t + z->dur); + } + if (EV) fprintf(EV, "],\"bars\":["); + if (EV) for (int k = 0; k < CHART_NBARS; k++) { static const char *CN[3] = { "G#m", "Emaj7", "B" }; + fprintf(EV, "%s{\"n\":%d,\"t\":%.3f,\"dur\":%.3f,\"chord\":\"%s\",\"section\":\"%s\"}", k ? "," : "", CHART_BARS[k].n, CHART_BARS[k].t, CHART_BARS[k].dur, CN[CHART_BARS[k].chord], SEC_NAME[section_of(CHART_BARS[k].n)]); } + if (EV) fprintf(EV, "],\"events\":[\n"); + + // ── score the bars ── + int tenor[3] = { 68, 71, 75 }, high[2] = { 80, 83 }, desc = 90; // v6.3: the tenor trio clears her top (G#4 = 68) + lead(tenor, 3, CHORD_PCS[0], 67, 79); + for (int k = 0; k < CHART_NBARS; k++) { + const ChartBar *b = &CHART_BARS[k]; + const int s = section_of(b->n); const Arr *o = &ARR[s]; + const double *bt = b->beats; const int nb = b->nb; const int *pcs = CHORD_PCS[b->chord]; + const double beatDur = b->dur / nb; + const double swell = s == BRIDGE ? 0.75 + 0.25 * (b->n - 72) / 8.0 : 1; + #define MID(j) (bt[j] + (bt[(j) + 1] - bt[j]) / 2) + // pocket on her strum points + const double *dz = DANCE[s]; + for (int q = 0; q < NIMPACT; q++) if (IMPACT_BARS[q] == b->n) { IMPACT_T[q] = bt[0] + VOICE_LAG; explosion(bt[0] + VOICE_LAG, b->n == FLOOR_BAR ? 0.8 : 0.6); } // v11: implosions + const int pos = k + 1, posV2 = pos_n(44), posC1 = pos_n_after(27, posV2); // v13: positions in the record + const double gDrop = b->n >= FLOOR_BAR ? 1 : pos >= posC1 ? 0.55 + 0.15 * evo(pos, posC1, posC1 + 8) : pos >= posV2 ? 0.45 + 0.1 * evo(pos, posV2, posV2 + 4) : 0; // v19: verse 2 kicks off (was 0.3, eased in) // verse 2 light, chorus 1 half, chorus 2 whole + // v6.6: hills — every section rises to its middle and settles into its edge + const int secLen = (s + 1 < NSEC ? SEC_FROM[s + 1] : CHART_NBARS + 1) - SEC_FROM[s]; + const double hill = s == INTRO ? 1 : 0.84 + 0.16 * sin(PI * (b->n - SEC_FROM[s] + 0.5) / secLen); + // v6: the evolution gates (verse 1 only; every other section is its table) + const int v1 = s == VERSE1; + const double gKick = pos >= posV2 ? 1 : b->n >= 21 ? 0.55 + 0.45 * evo(b->n, 21, 25) : 0, gHat = pos >= posV2 ? 1 : 0, gBass = evo(pos, posV2, posV2 + 3); + const double gSub = evo(b->n, 16, 22), gTen = evo(b->n, 20, 26), gShk = evo(b->n, 14, 18), gCon = evo(b->n, 18, 22); // v7: her first line alone; the shuffle from bar 14, congas from 18 + // v6 HOCKET: the floor doubles her 1 and 3 and lands in her empty 2 and 4; + // open hats on the complement's &1/&3; her own &2/&4 get a soft accent at her bin + if (dz[0] && gKick > 0) for (int j = 0; j < nb; j++) { if (pos < posV2 - 2 && j % 2) continue; // v19: her 1 and 3 through verse 1; every beat from the screw bar and pickup on, into four on the floor + double g = 0.95 * dz[0] * (j % 2 ? 0.9 : 1) * gKick * hill; + dance_kick(bt[j], g); ev(bt[j], "kick", 0.1, g, -1); } + if (dz[1] && gKick > 0) for (int j = 1; j < nb; j += 2) { clap(bt[j], 0.75 * dz[1] * swell * gDrop * hill * gKick); ev(bt[j], "clap", 0.1, dz[1], -1); } + if (dz[2] && gHat > 0) for (int j = 0; j < nb; j++) { + if (j % 2 == 0) { open_hat(MID(j) + BIN_OFF, 0.42 * dz[2] * swell * gHat * gDrop * hill); ev(MID(j), "hat", 0.1, dz[2], -1); } + else { trap_hat(MID(j) + BIN_OFF, 0.24 * dz[2] * swell * gHat * gDrop, 0.3); ev(MID(j), "hat", 0.02, dz[2] * 0.5, -1); } } + // v19: rolling hats from verse 2 on — ghost 16ths on every "e" and "a", and a 32nd roll into every other downbeat + if (dz[2] && gHat > 0) for (int j = 0; j < nb; j++) { + const double bd = bt[j + 1] - bt[j]; + for (int q = 1; q < 4; q += 2) trap_hat(bt[j] + bd * q / 4 + BIN_OFF, (q == 3 ? 0.16 : 0.11) * dz[2] * swell * gDrop * hill, q == 3 ? 0.35 : -0.35); + if (j == nb - 1 && b->n % 2) for (int q = 0; q < 8; q++) trap_hat(bt[j] + bd * q / 8 + BIN_OFF, (0.07 + 0.02 * q) * dz[2] * swell * gDrop, -0.5 + q / 7.0); } + // house bass: her pickups (&2/&4) in the verses, every offbeat in the choruses + if (dz[3] && gBass > 0) for (int j = 0; j < nb; j++) { if (dz[3] < 0.95 && j % 2 == 0) continue; + int r = ROOT[b->chord] - 12 + (j == nb - 1 && b->n % 2 ? 7 : 0); + eight08(MID(j) + BIN_OFF, r, beatDur * 0.38, 0.7 * dz[3] * gBass * gDrop * hill); ev(MID(j), "bass", beatDur * 0.38, dz[3], r); } // v8: a sine bass + // her arpeggios + if (ARP[s].div) { const Arp *A = &ARP[s]; int c = ((b->n - SEC_FROM[s]) * nb * A->div) % A->len; + for (int j = 0; j < nb; j++) for (int q = 0; q < A->div; q++) { + double t0 = bt[j] + (bt[j + 1] - bt[j]) * q / A->div + (q ? BIN_OFF : 0), t1 = bt[j] + (bt[j + 1] - bt[j]) * (q + 1) / A->div; + int h = A->step[c % A->len]; c++; + if (h < 0) continue; + double az = s == BRIDGE ? SEAT[(b->n - 73 + q) % 5] : SEAT[(j * A->div + q) % 5]; + arp_step(h, t0, t1, A->g * swell * gDrop, az, A->hold); ev(t0, "arp", (t1 - t0) * A->hold, A->g, h); } } + // risers: into each chorus + out of the break; a snare roll into the bridge's last bars + // v8: no risers + // v6.4: airplanes — an 8-bar jet into chorus 1, chorus 2 and the bridge (4 bars out of the break) + if (0) air(b->t, CHART_BARS[k + (b->n == 73 ? 8 : 16) - 1].t + CHART_BARS[k + (b->n == 73 ? 8 : 16) - 1].dur, 0.10); + // (v6.6: the snare roll into the bridge is gone — the jet is the hill) + if (o->kick && !dz[0]) { kick(bt[0], 0.95 * o->kick); ev(bt[0], "kick", 0.1, o->kick, -1); + if (nb > 2) { kick(MID(1), 0.6 * o->kick); ev(MID(1), "kick", 0.1, o->kick * 0.6, -1); } } + // v8: no snare — the 808 clap is the backbeat + if (o->clap && nb > 2 && !dz[1]) { clap(bt[2], 0.5 * o->clap * swell); ev(bt[2], "clap", 0.1, o->clap, -1); } + if (o->rim && nb > 3 && gHat > 0) { rim(MID(1) + BIN_OFF, 0.16 * o->rim * gHat); rim(MID(3) + BIN_OFF, 0.2 * o->rim * gHat); ev(MID(1), "rim", 0.05, o->rim, -1); ev(MID(3), "rim", 0.05, o->rim, -1); } // v8: 808 rim on her &2 / &4 + if (dz[0] && gHat > 0 && nb > 3) { bubble(MID(1) + BIN_OFF, 0.22 * gHat * gDrop, b->n % 2 ? -0.6 : 0.6); ev(MID(1), "bubble", 0.05, gHat, -1); + if (b->n % 2 == 0) { bubble(MID(3) + BIN_OFF, 0.16 * gHat * gDrop, b->n % 4 ? 0.4 : -0.4); ev(MID(3), "bubble", 0.05, gHat, -1); } } // v11: bubble pops + if (o->hat) for (int j = 0; j < nb; j++) { soft_hat(bt[j], 0.15 * o->hat, -0.35); soft_hat(MID(j), 0.09 * o->hat, 0.35); } + { // hand percussion on her grid: shaker 16ths, tambourine backbeat, congas on her &2 / &4 + const double *pc = PERC[s]; + // v6.5: the shuffle starts on 8ths and thickens into 16ths as the floor nears + // (odd 16ths appear with a probability rising bar 14 → 28), then loses one 16th in + // twelve at random so it never runs like a machine + if (pc[0] && gShk > 0) for (int j = 0; j < nb; j++) for (int q = 0; q < 4; q++) { + double t = bt[j] + (bt[j + 1] - bt[j]) * q / 4; + double fill = evo(b->n, 20, FLOOR_BAR); + if (q % 2 && (rnd() + 1) / 2 > fill) continue; // the odd 16ths fill in (bars 20 → 28) + if (b->n >= FLOOR_BAR && (rnd() + 1) / 2 < 0.08) continue; // a dropped grain now and then + int c16 = (b->n - 1) * 16 + j * 4 + q; // accents on a 3-cycle over the 16ths, floor only + double acc = b->n >= FLOOR_BAR ? (c16 % 3 == 0 ? 1 : (q == 2 ? 0.7 : 0.45)) : (q == 0 ? 0.9 : 0.6); // plain 8ths before the floor + shaker(hum_t2(t, 10), hum_g2(0.35 * pc[0] * acc * swell * gShk, 0.22), 0.55); ev(t, "shaker", 0.03, pc[0] * acc, -1); } + if (0) { // v8: no woodblock + static const int TRES[3] = { 0, 3, 6 }; + for (int q = 0; q < 3; q++) { double t = bt[0] + (bt[4] - bt[0]) * TRES[q] / 8.0 + (TRES[q] % 2 ? BIN_OFF : 0); + block(hum_t2(t, 7), hum_g2(0.22 * dz[0] * gHat * (q == 0 ? 0.8 : 1), 0.25), q == 1 ? -0.5 : 0.5); ev(t, "block", 0.03, dz[0], -1); } } + if (pc[1] && nb > 2) { tamb(hum_t2(bt[2], 8), hum_g2(0.30 * pc[1] * swell, 0.2), -0.4); ev(bt[2], "tamb", 0.1, pc[1], -1); + if (nb > 3) tamb(hum_t2(MID(3) + BIN_OFF, 8), hum_g2(0.14 * pc[1], 0.2), -0.4); } + if (pc[2] && gCon > 0) { int lo = ROOT[b->chord] + 12, hi = lo + 7; double cg = pc[2] * gCon; // tuned to her root / fifth + if (nb > 1) { conga(hum_t2(MID(1) + BIN_OFF, 9), hi, hum_g2(0.30 * cg, 0.22), 0.35, 0); ev(MID(1), "conga", 0.1, cg, hi); } + if (nb > 3) { conga(hum_t2(bt[3], 9), lo, hum_g2(0.34 * cg, 0.22), 0.25, 0); conga(hum_t2(MID(3) + BIN_OFF, 9), hi, hum_g2(0.26 * cg, 0.22), 0.35, 1); ev(bt[3], "conga", 0.1, cg, lo); } + if (b->n % 2 == 0 && nb > 2) { conga(hum_t2(bt[1] + (bt[2] - bt[1]) * 2 / 3.0, 9), hi, hum_g2(0.2 * cg, 0.25), 0.3, 1); } } // v6.4: a triplet slap every other bar + } + // 808 on the kick points, gliding into her root + if (o->b808 && !dz[3]) { int r = ROOT[b->chord] - 12; + eight08(bt[0], r, beatDur * 1.4, 0.8 * o->b808); ev(bt[0], "808", beatDur * 1.4, o->b808, r); + if (nb > 2) { eight08(MID(1), r, beatDur * 1.3, 0.6 * o->b808); ev(MID(1), "808", beatDur * 1.3, o->b808 * 0.6, r); } } + // trap hats: 16ths, accents on the beat, a roll into every other bar line + if (o->trap) { + int roll = 0, trip = 0; + for (int j = 0; j < nb; j++) { + int last = j == nb - 1 && roll, div = last ? (trip ? 6 : 8) : ((s == CHORUS1 || s == CHORUS2) ? 4 : 2); // v9: 16ths in the choruses, 8ths elsewhere + for (int q = 0; q < div; q++) { + double t = bt[j] + (bt[j + 1] - bt[j]) * q / div; + double acc = q == 0 ? 1 : (q % 2 ? 0.55 : 0.75); + double g = 0.5 * o->trap * acc * (last ? 0.6 + 0.4 * q / div : 1) * swell; + trap_hat(t, g, q % 2 ? 0.45 : -0.25); ev(t, "hat", 0.02, g, -1); + } + } + } + // sine choir + const double dur = b->dur * 0.97; + if (o->sub && gSub > 0) { sine(sL, sR, b->t, dur, ROOT[b->chord] - 12, 0.26 * o->sub * gSub, 0, 0.35, 0.9, 0); + sine(sL, sR, b->t, dur, ROOT[b->chord], 0.16 * o->sub * gSub, 0, 0.35, 0.9, 1); ev(b->t, "sub", dur, o->sub * gSub, ROOT[b->chord] - 12); } + lead(tenor, 3, pcs, 67, 79); + if (o->tenor && gTen > 0) for (int v = 0; v < 3; v++) { sine(padM[v], NULL, b->t, dur, tenor[v], 0.26 * o->tenor * swell * gTen * gDrop * hill, 0, 0.35, 0.9, 7); ev(b->t, "pad", dur, o->tenor * gTen, tenor[v]); } + lead(high, 2, pcs, 76, 88); + if (o->high) for (int v = 0; v < 2; v++) { sine(hiM, NULL, b->t, dur, high[v], 0.10 * o->high * swell * gDrop, 0, 0.8, 1.4, 7); ev(b->t, "high", dur, o->high, high[v]); } + if (o->descant) { // one long high chord tone per bar, leaning down by step + int c = -1; for (int d = -1; d >= -2 && c < 0; d--) { int m = nearest(pcs, 3, desc + d, 83, 93); if (m <= desc) c = m; } + if (c < 0) c = nearest(pcs, 3, desc, 83, 93); + desc = c <= 84 ? 92 : c; + sine(hiM, NULL, b->t, dur, c, 0.04 * o->descant, 0, 1.0, 1.8, 4); ev(b->t, "descant", dur, o->descant, c); + } + // vibraphone arpeggio on her 8ths (16ths in the last bridge bars) + if (o->vib) { + int tones[6]; for (int j = 0; j < 6; j++) tones[j] = nearest(pcs, 3, 76 + j * 3, 76, 93); // v6.3: vibes an octave up, out of her way + int dens = (s == BRIDGE && b->n >= 77) ? 4 : 2, c = 0; + for (int j = 0; j < nb; j++) for (int q = 0; q < dens; q++) + vib(hum_t(bt[j] + (bt[j + 1] - bt[j]) * q / dens), tones[c++ % 6], hum_g(0.10 * o->vib * swell), beatDur / dens * 1.5); + } + // a fem bell on every other downbeat where the trap plays + if (o->bell && o->trap && b->n % 2 == 0) fem_bell(bt[0], nearest(pcs, 3, 72, 68, 76), 0.18 * o->bell); + ev(b->t, "gtr", dur, o->her, -1); + } + // her melody, shadowed a 3rd up and an octave up + for (int k = 0; k < CHART_NNOTES; k++) { + const ChartNote *v = &CHART_NOTES[k]; const ChartBar *b = bar_at(v->t); + if (!b || v->dur < 0.18) continue; + const Arr *o = &ARR[section_of(b->n)]; + ev(v->t, "vox", v->dur, 1, v->midi); + if (o->third) sine(sL, sR, v->t, v->dur, third_above(v->midi), 0.06 * o->third, 0.3, 0.06, 0.35, 2); + if (o->octave) sine(hiM, NULL, v->t, v->dur, v->midi + 12, 0.03 * o->octave, 0, 0.08, 0.5, 4); + } + // fem bell answers: in every breath > 1 s her last three notes ring back + for (int k = 1; k < CHART_NNOTES; k++) { + const ChartNote *p = &CHART_NOTES[k - 1]; double end = p->t + p->dur, gap = CHART_NOTES[k].t - end; + const ChartBar *b = bar_at(end); + if (gap < 1.0 || !b) continue; + const Arr *o = &ARR[section_of(b->n)]; + if (!o->bell) continue; + const double gBell = evo(b->n, 14, 20); // v6.2: no bells in the bedroom bars + if (gBell <= 0) continue; + double e8 = (b->beats[1] - b->beats[0]) / 2; + for (int j = 0, from = k >= 3 ? k - 3 : 0; from + j < k; j++) { + double t = end + 0.12 + j * e8; + if (t < CHART_NOTES[k].t - 0.1) fem_bell(hum_t(t), CHART_NOTES[from + j].midi + 12, hum_g(0.16 * o->bell * gBell)); + } + } + // the hook on sines over the guitar break: chorus 1 bars 28–31 re-laid on 68–71 + for (int k = 0; k < CHART_NNOTES; k++) { + const ChartNote *v = &CHART_NOTES[k]; const ChartBar *sb = bar_at(v->t); + if (!sb || sb->n < 28 || sb->n > 31) continue; + const ChartBar *db = bar_n(sb->n + 40); + double t = db->t + (v->t - sb->t) / sb->dur * db->dur, d = v->dur * db->dur / sb->dur; + sine(hookM, NULL, t, d, v->midi, 0.12, 0, 0.05, 0.6, 3); + sine(hookM, NULL, t, d, v->midi + 12, 0.045, 0, 0.08, 0.8, 5); + ev(t, "hook", d, 1, v->midi); + } + + // ── space ── + float *spL = zeros(), *spR = zeros(); + ROT_T0 = bar_n(68)->t; + spatialize(hookM, NULL, rot_az, el_up, 1.3, spL, spR, 1.0); // v11: the hook rotates through the break + for (int v = 0; v < 3; v++) spatialize(padM[v], NULL, v == 0 ? tour0 : v == 1 ? tour1 : tour2, el_zero, 1.4, spL, spR, 1.0); // v11: the pads tour + spatialize(bellM, NULL, orbit_az, orbit_el, 1.2, spL, spR, 0.9); // bells orbit overhead + spatialize(vibM, NULL, sweep_az, el_zero, 1.2, spL, spR, 0.8); // vibes sweep ear to ear + float *hiL = zeros(), *hiR = zeros(); + spatialize(hiM, NULL, wide_az, el_up, 1.2, hiL, hiR, 1.3); // highs float up + wide + float *exL = zeros(), *exR = zeros(); + spatialize(exM, NULL, expl_az, expl_el, 0.8, exL, exR, 1.0); // the explosion laps the head + float *hL = zeros(), *hR = zeros(); + for (int h = 0; h < NHARM; h++) { + if (!harm[h].L) continue; + float *g = automate(v_harm_g, h), *az = automate(v_harm_az, h); + int any = 0; for (long i = 0; i < N; i += 480) if (g[i] > 0.002) { any = 1; break; } + if (any) { + float *m = zeros(); long d = lround(HARM_DELAY[h] * SR); + for (long i = 0; i < N; i++) { m[i] = sample(harm[h].L, harm[h].n, i - d) * g[i] * 0.6f; az[i] += (float)sway((double)i / SR, h); } // v11: sway + spatialize(m, az, NULL, el_near, 1.0, hL, hR, 1.0); + free(m); + } + free(g); free(az); + // v6: the arpeggiated copy of the same stem, each step at its own seat + int anyArp = 0; for (long i = 0; i < N; i += 480) if (arpG[h][i] > 0.002) { anyArp = 1; break; } + if (anyArp) { + float *m = zeros(); + for (long i = 0; i < N; i++) m[i] = sample(harm[h].L, harm[h].n, i) * arpG[h][i]; + spatialize(m, arpAz[h], NULL, el_near, 0.9, hL, hR, 1.0); + free(m); + } + } + + // v7: the choir, three seats behind her (further than the harmonies), keyed and pumped in the mix + float *cL = zeros(), *cR = zeros(); + { float *cg = automate(v_choir, 0); Path seats[3] = { orbit0, orbit1, orbit2 }; // v11: the choir orbits + for (int c = 0; c < 3; c++) { if (!choir[c].L) continue; float *m = zeros(); + for (long i = 0; i < N; i++) m[i] = sample(choir[c].L, choir[c].n, i) * cg[i] * (c == 1 ? 0.8f : 1.0f); + spatialize(m, NULL, seats[c], el_back, 1.6, cL, cR, 1.0); free(m); } + free(cg); } + float *jg = automate(v_jeff, 0); + // v11: the sister — high and low octaves dallying around her, the hum behind + float *sisL = zeros(), *sisR = zeros(); + { float *gh = automate(v_sis_hi, 0), *gl = automate(v_sis_lo, 0), *gm = automate(v_sis_hum, 0); + if (sisHi.L) { float *m = zeros(); for (long i = 0; i < N; i++) m[i] = sample(sisHi.L, sisHi.n, i) * gh[i]; spatialize(m, NULL, dally_hi, dally_el, 1.1, sisL, sisR, 1.0); free(m); } + if (sisLo.L) { float *m = zeros(); for (long i = 0; i < N; i++) m[i] = sample(sisLo.L, sisLo.n, i) * gl[i]; spatialize(m, NULL, dally_lo, el_near, 1.2, sisL, sisR, 1.0); free(m); } + if (sisHum.L) { float *m = zeros(); for (long i = 0; i < N; i++) m[i] = sample(sisHum.L, sisHum.n, i) * gm[i]; spatialize(m, NULL, seat_c, el_back, 2.0, sisL, sisR, 1.0); free(m); } + free(gh); free(gl); free(gm); } + + // ── mix ── + float *sendA = automate(v_send, 0), *airA = automate(v_air, 0), *thickA = automate(v_thick, 0), *warmA = automate(v_warm, 0); + float *herA = automate(v_her, 0), *acgA = automate(v_acg, 0), *elgA = automate(v_elg, 0); + float *farA = automate(v_far, 0), *cathA = automate(v_cath, 0); + float *L = zeros(), *R = zeros(), *xL = zeros(), *xR = zeros(); + double env = 0, gcur = 1, e = 0, vlp = 0, dlp = 0, drp = 0, venv = 0, vwarm = 0; + const double k350 = 1 - exp(-2 * PI * 350 / SR); + const double aA = exp(-1 / (0.01 * SR)), aR = exp(-1 / (0.2 * SR)); + const double k7 = 1 - exp(-2 * PI * 7000 / SR); + for (long i = 0; i < N; i++) { + // sidechain: kick + 808 pump the sines and the replay guitars + e = fmax(duck[i], e * 0.99990); double pump = 1 - 0.7 * fmin(1, e), hpump = 1 - 0.4 * fmin(1, e); // v9: a firmer pump + // vocal leveler: RMS 4:1 over threshold, 10 ms / 200 ms (v6.3: harder, louder — she is the record) + double x = vox.L[i], pw = x * x; + env = pw > env ? aA * env + (1 - aA) * pw : aR * env + (1 - aR) * pw; + double lv = sqrt(env), gt = lv > 0.06 ? pow(lv / 0.06, 1.0 / 4 - 1) : 1; + gcur = gt < gcur ? gt : gcur + (gt - gcur) * 0.0005; + double v0 = x * gcur * (2.2 + 0.25 * warmA[i]) * (1 - 0.42 * fmin(1, e)); // v19: her voice pumps with the kick, audibly (was 0.2) + // v15: a hair louder when close vlp += (v0 - vlp) * k7; // her voice rides the kick's pump, lightly + vwarm += (v0 - vwarm) * k350; + const double warm = warmA[i]; + double v = v0 + (v0 - vlp) * airA[i] * (1 - 0.85 * warm) * 1.6 + vwarm * 0.35 * warm; // v19: proximity halved (0.75) — less bedroom boom // air above 7 kHz; proximity when warm + v *= 1 - 0.65 * farA[i]; // v19: far = quiet (−9 dB), nothing else — she comes up as she comes closer + // v6.3: HOLLOW — the bed is keyed to her: while she sings, sines, guitars, + // harmonies and halo step back (up to −6 dB, 10 ms in, 200 ms out via env) + venv = lv > venv ? venv + (lv - venv) * 0.02 : venv + (lv - venv) * 0.0006; + const double vd = 1 - 0.5 * fmin(1, venv / 0.05), hd = 1 - 0.3 * fmin(1, venv / 0.05); + double hv = sample(halo.L, halo.n, i) * 0.22 * hd * (1 - 0.6 * warm); // v8.1: the halo up; v8.2: mostly out while she is up close + // v6.4: THICK — two drifting copies of her lead, 28 and 41 ms late (the platter's + // unison spacing), each wandering ±1.2 ms so they detune a few cents, panned apart + double tt = (double)i / SR; + double d1 = (0.028 + 0.0012 * sin(2 * PI * 0.13 * tt)) * SR, d2 = (0.041 + 0.0012 * sin(2 * PI * 0.31 * tt + 1)) * SR; + long j1 = i - (long)d1, j2 = i - (long)d2; double f1 = d1 - floor(d1), f2 = d2 - floor(d2); + double c1 = (sample(vox.L, vox.n, j1) * (1 - f1) + sample(vox.L, vox.n, j1 - 1) * f1) * gcur * 2.0 * thickA[i]; + double c2 = (sample(vox.L, vox.n, j2) * (1 - f2) + sample(vox.L, vox.n, j2 - 1) * f2) * gcur * 2.0 * thickA[i]; + double thL = c1 * 0.42 + c2 * 0.18, thR = c1 * 0.18 + c2 * 0.42; + dlp = dL[i]; drp = dR[i]; // v5.1: kit full-band + forward (was 4.5 kHz lowpass) + double sl = tanh(sL[i] * 1.8 * 1.4) / tanh(1.4) * pump * vd, sr = tanh(sR[i] * 1.8 * 1.4) / tanh(1.4) * pump * vd; + double gl = (sample(gtr.L, gtr.n, i) * herA[i] * 0.8 * hpump + (sample(acg.L, acg.n, i) * acgA[i] * 0.55 + sample(elg.L, elg.n, i) * elgA[i] * 0.8) * pump) * vd; + double gr = (sample(gtr.R, gtr.n, i) * herA[i] * 0.8 * hpump + (sample(acg.R, acg.n, i) * acgA[i] * 0.55 + sample(elg.R, elg.n, i) * elgA[i] * 0.8) * pump) * vd; + double pl = (spL[i] + hiL[i] * pump) * vd, pr = (spR[i] + hiR[i] * pump) * vd; + double jl = jL[i] * pump, jr = jR[i] * pump; + double jd = (sample(jeff[0].L, jeff[0].n, i) * 0.7 + sample(jeff[1].L, jeff[1].n, i) * 0.4) * jg[i] * pump * hd; // the drone: centre, never moves + double chl = cL[i] * hpump * hd, chr = cR[i] * hpump * hd; + double ssl = sisL[i] * hpump * hd * 0.9, ssr = sisR[i] * hpump * hd * 0.9; + L[i] = (float)(v + thL + hL[i] * hpump * hd + hv * 0.6 * hpump + gl + dlp * 1.45 + tL[i] * 1.25 + pL[i] * 0.9 + b808[i] * 0.42 + sl * 0.85 + pl + exL[i] * 0.5 + jl + chl + jd + ssl); + R[i] = (float)(v + thR + hR[i] * hpump * hd + hv * 0.6 * hpump + gr + drp * 1.45 + tR[i] * 1.25 + pR[i] * 0.9 + b808[i] * 0.42 + sr * 0.85 + pr + exR[i] * 0.5 + jr + chr + jd + ssr); + // v6.4: SPACE — more of everything into the room, the doubles and the jets too + // v8.2: no room on her in the opening — the send is closed while warm, and opens as the mix grows + xL[i] = (float)(1 - 0.97 * warm) * (float)(v * sendA[i] * 1.5 + thL * 0.8 + hL[i] * 0.5 + hv * 1.2 + sl * 0.5 + pl * 0.7 + dlp * 0.06 + gl * 0.1 + exL[i] * 0.8 + jl * 0.6 + chl * 0.9 + jd * 0.5 + ssl * 1.0); + xR[i] = (float)(1 - 0.97 * warm) * (float)(v * sendA[i] * 1.5 + thR * 0.8 + hR[i] * 0.5 + hv * 1.2 + sr * 0.5 + pr * 0.7 + drp * 0.06 + gr * 0.1 + exR[i] * 0.8 + jr * 0.6 + chr * 0.9 + jd * 0.5 + ssr * 1.0); + } + // the room: near early reflections always on, a 2.2 s dark tail behind + float *wL = zeros(), *wR = zeros(); + fdn(xL, xR, wL, wR, 4.2, 0.28, 0.035); // v8.1: angelic — longer, brighter + // v19: the cathedral — from verse 2's kick-off the same sends also feed a 7.5 s stone tail + float *catL = zeros(), *catR = zeros(), *cwL = zeros(), *cwR = zeros(); + for (long i = 0; i < N; i++) { catL[i] = xL[i] * cathA[i]; catR[i] = xR[i] * cathA[i]; } + fdn(catL, catR, cwL, cwR, 7.5, 0.2, 0.075); + for (long i = 0; i < N; i++) { L[i] += cwL[i] * 0.42f; R[i] += cwR[i] * 0.42f; } + free(catL); free(catR); free(cwL); free(cwR); + static const double ROOM[6][3] = { { 0.011, 0.16, 0 }, { 0.017, 0.14, 1 }, { 0.023, 0.11, 0 }, { 0.031, 0.10, 1 }, { 0.043, 0.07, 0 }, { 0.053, 0.06, 1 } }; + for (long i = N - 1; i >= 0; i--) { + double el = 0, er = 0; + for (int r = 0; r < 6; r++) { long j = i - lround(ROOM[r][0] * SR); if (j >= 0) { double y = (xL[j] + xR[j]) * ROOM[r][1]; if (ROOM[r][2] > 0) er += y; else el += y; } } + L[i] += (float)(wL[i] * 0.5 + el); R[i] += (float)(wR[i] * 0.5 + er); + } + // bass mono below 120 Hz (house rule) + { double ml = 0, mr = 0, k = 1 - exp(-2 * PI * 120 / SR); + for (long i = 0; i < N; i++) { ml += (L[i] - ml) * k; mr += (R[i] - mr) * k; double m = (ml + mr) / 2; L[i] += (float)(m - ml); R[i] += (float)(m - mr); } } + // v6.6: the record starts on her first sung word (the first charted note after 23 s), 60 ms early + long start = at(startSecOut); if (start < 0) start = 0; + long endAt = END_BAR <= CHART_BARS[CHART_NBARS - 1].n ? at(bar_n(END_BAR)->t + 1.6) : N; if (endAt > N) endAt = N; + long M = endAt - start; float *oL = L + start, *oR = R + start; + long fin = (long)(0.005 * SR), fout = (long)(1.5 * SR); + for (long i = 0; i < fin; i++) { double g = 0.5 - 0.5 * cos(PI * i / fin); oL[i] *= (float)g; oR[i] *= (float)g; } + for (long i = 0; i < fout; i++) { double g = 0.5 - 0.5 * cos(PI * i / fout); oL[M - 1 - i] *= (float)g; oR[M - 1 - i] *= (float)g; } + double peak = 0; for (long i = 0; i < M; i++) { peak = fmax(peak, fabs(oL[i])); peak = fmax(peak, fabs(oR[i])); } + for (long i = 0; i < M; i++) { oL[i] *= (float)(0.7 / peak); oR[i] *= (float)(0.7 / peak); } + write_wav_f32(LANE "/out/sailor-song-" VERSION "-full.wav", oL, oR, M); + // v12: her lead alone with a click on her (regularized) beats — to hear the vocal's smoothness on its own + { float *cl = zeros(), *cr = zeros(); + double env2 = 0, g2 = 1; const double aA2 = exp(-1 / (0.01 * SR)), aR2 = exp(-1 / (0.2 * SR)); + for (long i = 0; i < N; i++) { double x = vox.L[i], pw = x * x; env2 = pw > env2 ? aA2 * env2 + (1 - aA2) * pw : aR2 * env2 + (1 - aR2) * pw; + double lv = sqrt(env2), gt = lv > 0.06 ? pow(lv / 0.06, 1.0 / 4 - 1) : 1; g2 = gt < g2 ? gt : g2 + (gt - g2) * 0.0005; + cl[i] = cr[i] = (float)(x * g2 * 2.0); } + for (int k = 0; k < CHART_NBARS; k++) for (int j = 0; j < CHART_BARS[k].nb; j++) { long a2 = at(CHART_BARS[k].beats[j]); double f = j == 0 ? 1800 : 1200, g = j == 0 ? 0.35 : 0.22; + for (long i = 0; i < 0.02 * SR && a2 + i < N; i++) { double u = (double)i / SR, v = sin(2 * PI * f * u) * exp(-u * 260) * g; cl[a2 + i] += (float)v * 0.7f; cr[a2 + i] += (float)v; } } + double pk = 0; for (long i = start; i < endAt; i++) { pk = fmax(pk, fabs(cl[i])); pk = fmax(pk, fabs(cr[i])); } + for (long i = 0; i < N; i++) { cl[i] *= (float)(0.8 / pk); cr[i] *= (float)(0.8 / pk); } + write_wav_f32(LANE "/out/sailor-song-" VERSION "-vox-click.wav", cl + start, cr + start, M); free(cl); free(cr); } + if (EV) { fprintf(EV, "\n]}\n"); fclose(EV); } + printf("✓ %s/out/sailor-song-" VERSION "-full.wav %.1f s (from %.2f s of the take, floor at bar %d), %d bars, %d notes\n", LANE, (double)M / SR, (double)start / SR, FLOOR_BAR, CHART_NBARS, CHART_NNOTES); + return 0; +} diff --git a/pop/sailor-song/choir.json b/pop/sailor-song/choir.json new file mode 100644 index 0000000000..a0043a6022 --- /dev/null +++ b/pop/sailor-song/choir.json @@ -0,0 +1,273 @@ +{ + "frameMs": 5.0, + "tones": { + "low": [ + 44, + 44, + 44, + 42, + 42, + 44, + 44, + 42, + 42, + 44, + 42, + 42, + 42, + 44, + 44, + 42, + 42, + 44, + 42, + 44, + 44, + 42, + 42, + 42, + 42, + 42, + 42, + 44, + 44, + 44, + 44, + 42, + 42, + 42, + 44, + 44, + 44, + 42, + 42, + 42, + 44, + 42, + 44, + 44, + 44, + 42, + 42, + 44, + 44, + 44, + 44, + 42, + 42, + 42, + 44, + 44, + 44, + 42, + 42, + 42, + 42, + 44, + 44, + 42, + 42, + 42, + 44, + 44, + 44, + 44, + 42, + 42, + 44, + 42, + 44, + 44, + 42, + 42, + 42, + 44, + 44, + 44, + 44, + 42 + ], + "mid": [ + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47, + 47 + ], + "high": [ + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51 + ] + }, + "vowels": { + "G#m": "ah", + "Emaj7": "iy", + "B": "uw" + }, + "jeffrey": [ + "ah", + "iy", + "uw" + ] +} \ No newline at end of file diff --git a/pop/sailor-song/measures.cut.json b/pop/sailor-song/measures.cut.json new file mode 100644 index 0000000000..207558af0a --- /dev/null +++ b/pop/sailor-song/measures.cut.json @@ -0,0 +1,3909 @@ +{ + "strums": { + "count": 425, + "perBeat": 1.23, + "phaseHist8": [ + 4, + 4, + 10, + 14, + 132, + 46, + 1, + 2 + ] + }, + "beats": { + "snapped": [ + 3.5989999999999998, + 4.073, + 4.546, + 5.02, + 5.494, + 5.968, + 6.454, + 6.94, + 7.427, + 7.913, + 8.4, + 8.886, + 9.373, + 9.859, + 10.346, + 10.832, + 11.302, + 11.773, + 12.307, + 12.748, + 13.23, + 13.711, + 14.187, + 14.663, + 15.145, + 15.627, + 16.173, + 16.625, + 17.107, + 17.589, + 18.048, + 18.506, + 19.006, + 19.505, + 19.969, + 20.434, + 20.91, + 21.386, + 21.856, + 22.326, + 22.883, + 23.336, + 23.859, + 24.311, + 24.892, + 25.368, + 25.902, + 26.389, + 26.877, + 27.365, + 27.848, + 28.331, + 28.814, + 29.297, + 29.78, + 30.308, + 30.836, + 31.388, + 31.939, + 32.461, + 32.984, + 33.501, + 34.017, + 34.557, + 35.097, + 35.643, + 36.188, + 36.728, + 37.268, + 37.808, + 38.348, + 38.882, + 39.427, + 39.962, + 40.496, + 41.035, + 41.575, + 42.104, + 42.632, + 43.143, + 43.677, + 44.217, + 44.756, + 45.296, + 45.836, + 46.363126965355846, + 46.8905137054648, + 47.41821310483894, + 47.94715455486204, + 48.46752275144404, + 48.987675609169045, + 49.50697437994057, + 50.02542507870751, + 50.53652233479391, + 51.04812940154079, + 51.56008063076585, + 52.07241961955978, + 52.58129418498595, + 53.09008787847, + 53.59888157195405, + 54.10785525989205, + 54.616828947830044, + 55.125622641314095, + 55.634589060954305, + 56.14263359231784, + 56.64975956898965, + 57.15696410426977, + 57.6642631186425, + 58.17152788872035, + 58.678758551701094, + 60.14115658452161, + 60.61993265676294, + 61.09880521267671, + 61.57797442374718, + 62.0578229568209, + 62.53812442274511, + 63.019077168313004, + 63.500877181502375, + 63.983092369223954, + 64.46549386564183, + 64.94799331261576, + 65.43013763639117, + 65.9120070591779, + 66.3938922421885, + 66.8759469570406, + 67.35818710651267, + 67.8404114725341, + 68.32272754895645, + 68.80486999050622, + 69.28701243205599, + 69.76904054716424, + 70.25079754565755, + 70.73228373234343, + 71.21331888883113, + 71.69390385933116, + 72.17448882983119, + 72.6552667823867, + 73.13612410037048, + 73.61617342069471, + 74.09531940292464, + 74.5739709360621, + 75.94897815591739, + 76.45476332999223, + 76.95958282442237, + 77.46340641512613, + 77.94287069273145, + 78.42195259326468, + 78.90057758225655, + 79.37874653041092, + 79.85664241526587, + 80.33425006583705, + 80.81153170550499, + 81.28911187242336, + 81.76690512547424, + 82.24395275334717, + 82.72035962778378, + 83.19652068001211, + 83.67243713010524, + 84.1480924260713, + 84.62384756024585, + 85.09989218237698, + 85.57614424708869, + 86.0522962649411, + 86.52844828279352, + 87.0046167415612, + 87.48078520032887, + 87.95695365909654, + 88.43281482343616, + 88.9084876623725, + 89.38397118946763, + 89.85933285593954, + 90.33499387846182, + 90.81085227807216, + 91.28660836546176, + 91.76226259567166, + 92.23811419733153, + 92.714063372679, + 93.18991262837005, + 93.6657618840611, + 94.14142168748955, + 94.61677485244746, + 95.09239713179969, + 95.56828988168127, + 96.04408273559419, + 96.52009964020965, + 96.99581674080137, + 97.47162100636407, + 97.94773249284904, + 98.4237463390915, + 98.89957949384446, + 99.37551499397138, + 99.85173331851988, + 100.32795164306837, + 100.80397229189775, + 101.27978569965076, + 101.75561592174205, + 102.23163573190064, + 102.70767320971876, + 103.18311793330855, + 103.65819310045254, + 104.13346515867819, + 104.60935809938306, + 105.08599305727628, + 105.56310285422686, + 106.04050028468814, + 106.51817020896321, + 106.99621227789697, + 107.75478441518416, + 108.23223763761112, + 108.70931817586552, + 109.18554819291239, + 109.66118870467977, + 110.13643482015328, + 110.61141530458114, + 111.08629861205664, + 111.56108478233479, + 112.03567446382011, + 112.50987952410243, + 112.98337433945339, + 113.4559902937468, + 113.92824107434811, + 114.400984684298, + 114.87415673701955, + 115.3473287897411, + 115.82018348623257, + 116.2930547885984, + 116.76552522464631, + 117.23788751512019, + 117.71033788757087, + 118.18270231944166, + 118.65494648401526, + 119.12690576299862, + 119.59876679038977, + 120.07044152729533, + 120.54179296553681, + 121.0131444037783, + 121.48481914068385, + 121.95630773414052, + 122.4272875421209, + 122.89784286637278, + 123.3678786298861, + 123.83868854738607, + 124.30948200347335, + 124.78067348407146, + 125.25186496466956, + 125.72286131107703, + 126.19414138141423, + 126.665722856166, + 127.13768470627872, + 127.60984087482262, + 128.0820829081746, + 128.55475247522185, + 129.02764143124188, + 129.50040684952342, + 129.97317226780496, + 130.44626292801925, + 130.91966180434125, + 131.39350122891943, + 131.86741599381702, + 132.34178926533662, + 132.81597425782041, + 133.29075952060816, + 133.76584756397304, + 134.24075126900783, + 134.71534930933564, + 135.1895627149173, + 135.6635801053961, + 136.13729607801272, + 136.61109640693047, + 137.08478798067887, + 137.5588082491443, + 138.03301666583226, + 138.50731169544952, + 138.9817280393616, + 139.45612683574058, + 139.92987914065765, + 140.40277255329093, + 140.8756659659242, + 141.34816009383573, + 141.82056826524948, + 142.2929594578646, + 142.76547099268595, + 143.23776890621127, + 143.7100668197366, + 144.18236473326192, + 144.65502789340337, + 145.1277993369445, + 145.60026165450486, + 146.07232379856993, + 146.54408700546188, + 147.01585021235383, + 147.4877165005774, + 147.95976923065564, + 148.43222211826188, + 148.9046750058681, + 149.37714529734563, + 149.84959818495187, + 150.32196513106325, + 150.79431510133918, + 151.26698175048634, + 151.7394440680467, + 152.2123238446126, + 152.68443777849473, + 153.1564437299249, + 153.62853763047565, + 154.10084496776963, + 154.57391877584644, + 155.04688939592685, + 155.51966821427067, + 155.99215447583546, + 156.46443656171147, + 156.93671864758747, + 157.40898334345462, + 157.88164693620746, + 158.35420495585106, + 158.82657150818451, + 159.29881779214114, + 159.77108146475064, + 160.24270473059698, + 160.71432799644333, + 161.18574652531504, + 161.65628470811114, + 162.1262923401926, + 162.5952067609428, + 163.06359428432415, + 163.53154027649498, + 163.99938018417672, + 164.46702772319742, + 164.93426489493106, + 165.40109069463176, + 165.86733616930132, + 166.33347632901695, + 166.79981948638743, + 167.2662633698567, + 167.7323615339086, + 168.1985517480286, + 168.66458155437667, + 169.13068788460856, + 169.59755218838419, + 170.06475895172233, + 170.53241009800092, + 171.0005690352569, + 171.4885690352569 + ], + "hitFraction": 0.584, + "trackerOffsetMs": { + "median": -35, + "p90abs": 46.5 + } + }, + "meter": { + "gridShift": "motif-anchored, 2 beat-count flips at [27.85, 61.17]", + "beatsPerBar": 4, + "phase": 1, + "downbeatChangeFraction": 0.75 + }, + "bars": [ + { + "n": 1, + "t": 5.02, + "dur": 1.92, + "beats": [ + 5.02, + 5.494, + 5.968, + 6.454, + 6.94 + ], + "bpm": 125, + "chord": "G#m", + "agree": 1, + "strum": ".X.XXX.." + }, + { + "n": 2, + "t": 6.94, + "dur": 1.946, + "beats": [ + 6.94, + 7.427, + 7.913, + 8.4, + 8.886 + ], + "bpm": 123.3, + "chord": "G#m", + "agree": 0.75, + "strum": "X......." + }, + { + "n": 3, + "t": 8.886, + "dur": 1.946, + "beats": [ + 8.886, + 9.373, + 9.859, + 10.346, + 10.832 + ], + "bpm": 123.3, + "chord": "G#m", + "agree": 1, + "strum": "........" + }, + { + "n": 4, + "t": 10.832, + "dur": 1.916, + "beats": [ + 10.832, + 11.302, + 11.773, + 12.307, + 12.748 + ], + "bpm": 125.3, + "chord": "B", + "agree": 0.5, + "strum": "X.XXX.XX" + }, + { + "n": 5, + "t": 12.748, + "dur": 1.915, + "beats": [ + 12.748, + 13.23, + 13.711, + 14.187, + 14.663 + ], + "bpm": 125.3, + "chord": "B", + "agree": 0.5, + "strum": "X.XXX.XX" + }, + { + "n": 6, + "t": 14.663, + "dur": 1.962, + "beats": [ + 14.663, + 15.145, + 15.627, + 16.173, + 16.625 + ], + "bpm": 122.3, + "chord": "G#m", + "agree": 1, + "strum": "X..XX.XX" + }, + { + "n": 7, + "t": 16.625, + "dur": 1.881, + "beats": [ + 16.625, + 17.107, + 17.589, + 18.048, + 18.506 + ], + "bpm": 127.6, + "chord": "G#m", + "agree": 1, + "strum": "X..XX.XX" + }, + { + "n": 8, + "t": 18.506, + "dur": 1.928, + "beats": [ + 18.506, + 19.006, + 19.505, + 19.969, + 20.434 + ], + "bpm": 124.5, + "chord": "B", + "agree": 1, + "strum": "X..XX.XX" + }, + { + "n": 9, + "t": 20.434, + "dur": 1.892, + "beats": [ + 20.434, + 20.91, + 21.386, + 21.856, + 22.326 + ], + "bpm": 126.8, + "chord": "B", + "agree": 0.75, + "strum": "X..XX.XX" + }, + { + "n": 10, + "t": 22.326, + "dur": 1.985, + "beats": [ + 22.326, + 22.883, + 23.336, + 23.859, + 24.311 + ], + "bpm": 120.9, + "chord": "G#m", + "agree": 0.5, + "strum": "X.X.X.XX" + }, + { + "n": 11, + "t": 24.311, + "dur": 2.078, + "beats": [ + 24.311, + 24.892, + 25.368, + 25.902, + 26.389 + ], + "bpm": 115.5, + "chord": "B", + "agree": 0.75, + "strum": "X.XXX.XX" + }, + { + "n": 12, + "t": 26.389, + "dur": 2.425, + "beats": [ + 26.389, + 26.877, + 27.365, + 27.848, + 28.331, + 28.814 + ], + "bpm": 123.7, + "chord": "B", + "agree": 1, + "strum": ".X..XX...X" + }, + { + "n": 13, + "t": 28.814, + "dur": 2.022, + "beats": [ + 28.814, + 29.297, + 29.78, + 30.308, + 30.836 + ], + "bpm": 118.7, + "chord": "B", + "agree": 0.5, + "strum": "...XX..X" + }, + { + "n": 14, + "t": 30.836, + "dur": 2.148, + "beats": [ + 30.836, + 31.388, + 31.939, + 32.461, + 32.984 + ], + "bpm": 111.7, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 15, + "t": 32.984, + "dur": 2.113, + "beats": [ + 32.984, + 33.501, + 34.017, + 34.557, + 35.097 + ], + "bpm": 113.6, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 16, + "t": 35.097, + "dur": 2.171, + "beats": [ + 35.097, + 35.643, + 36.188, + 36.728, + 37.268 + ], + "bpm": 110.5, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 17, + "t": 37.268, + "dur": 2.159, + "beats": [ + 37.268, + 37.808, + 38.348, + 38.882, + 39.427 + ], + "bpm": 111.2, + "chord": "B", + "agree": 0.75, + "strum": "X..XX.X." + }, + { + "n": 18, + "t": 39.427, + "dur": 2.148, + "beats": [ + 39.427, + 39.962, + 40.496, + 41.035, + 41.575 + ], + "bpm": 111.7, + "chord": "Emaj7", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 19, + "t": 41.575, + "dur": 2.102, + "beats": [ + 41.575, + 42.104, + 42.632, + 43.143, + 43.677 + ], + "bpm": 114.2, + "chord": "B", + "agree": 0.5, + "strum": "X..XXXXX" + }, + { + "n": 20, + "t": 43.677, + "dur": 2.159, + "beats": [ + 43.677, + 44.217, + 44.756, + 45.296, + 45.836 + ], + "bpm": 111.2, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 21, + "t": 45.836, + "dur": 2.111, + "beats": [ + 45.836, + 46.363, + 46.891, + 47.418, + 47.947 + ], + "bpm": 113.7, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 22, + "t": 47.947, + "dur": 2.078, + "beats": [ + 47.947, + 48.468, + 48.988, + 49.507, + 50.025 + ], + "bpm": 115.5, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 23, + "t": 50.025, + "dur": 2.047, + "beats": [ + 50.025, + 50.537, + 51.048, + 51.56, + 52.072 + ], + "bpm": 117.2, + "chord": "B", + "agree": 0.75, + "strum": "X..XX..X" + }, + { + "n": 24, + "t": 52.072, + "dur": 2.035, + "beats": [ + 52.072, + 52.581, + 53.09, + 53.599, + 54.108 + ], + "bpm": 117.9, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 25, + "t": 54.108, + "dur": 2.035, + "beats": [ + 54.108, + 54.617, + 55.126, + 55.635, + 56.143 + ], + "bpm": 117.9, + "chord": "B", + "agree": 1, + "strum": "X..XX.X." + }, + { + "n": 26, + "t": 56.143, + "dur": 2.029, + "beats": [ + 56.143, + 56.65, + 57.157, + 57.664, + 58.172 + ], + "bpm": 118.3, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 27, + "t": 58.172, + "dur": 1.01, + "beats": [ + 58.172, + 58.679, + 59.182 + ], + "bpm": 118.7, + "chord": "B", + "agree": 0.8, + "strum": "X..X" + }, + { + "n": 26, + "t": 59.182, + "dur": 0.959, + "beats": [ + 59.182, + 59.662, + 60.141 + ], + "bpm": 118.3, + "chord": "B", + "agree": 1, + "strum": [] + }, + { + "n": 43, + "t": 60.141, + "dur": 0.479, + "beats": [ + 60.141, + 60.62 + ], + "bpm": 125.5, + "chord": "G#m", + "agree": 0.75, + "strum": "XX" + }, + { + "n": 44, + "t": 60.62, + "dur": 1.918, + "beats": [ + 60.62, + 61.099, + 61.578, + 62.058, + 62.538 + ], + "bpm": 125.1, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 45, + "t": 62.538, + "dur": 1.927, + "beats": [ + 62.538, + 63.019, + 63.501, + 63.983, + 64.465 + ], + "bpm": 124.5, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 46, + "t": 64.465, + "dur": 1.928, + "beats": [ + 64.465, + 64.948, + 65.43, + 65.912, + 66.394 + ], + "bpm": 124.5, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 47, + "t": 66.394, + "dur": 1.929, + "beats": [ + 66.394, + 66.876, + 67.358, + 67.84, + 68.323 + ], + "bpm": 124.4, + "chord": "B", + "agree": 0.75, + "strum": "X..XX..X" + }, + { + "n": 48, + "t": 68.323, + "dur": 1.928, + "beats": [ + 68.323, + 68.805, + 69.287, + 69.769, + 70.251 + ], + "bpm": 124.5, + "chord": "G#m", + "agree": 0.75, + "strum": "X..XX..X" + }, + { + "n": 49, + "t": 70.251, + "dur": 1.924, + "beats": [ + 70.251, + 70.732, + 71.213, + 71.694, + 72.174, + 72.175 + ], + "bpm": 124.8, + "chord": "Emaj7", + "agree": 0.75, + "strum": "X..XX.X." + }, + { + "n": 50, + "t": 72.174, + "dur": 1.921, + "beats": [ + 72.174, + 72.655, + 73.136, + 73.616, + 74.095 + ], + "bpm": 124.9, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 51, + "t": 74.095, + "dur": 0.841, + "beats": [ + 74.095, + 74.574, + 74.936 + ], + "bpm": 125.6, + "chord": "G#m", + "agree": 1, + "strum": "X..X" + }, + { + "n": 27, + "t": 74.936, + "dur": 2.528, + "beats": [ + 74.936, + 75.443, + 75.949, + 76.455, + 76.96, + 77.464 + ], + "bpm": 118.7, + "chord": "B", + "agree": 0.8, + "strum": "X..XX.X..." + }, + { + "n": 28, + "t": 77.464, + "dur": 1.915, + "beats": [ + 77.464, + 77.943, + 78.422, + 78.901, + 79.379 + ], + "bpm": 125.3, + "chord": "Emaj7", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 29, + "t": 79.379, + "dur": 1.91, + "beats": [ + 79.379, + 79.857, + 80.335, + 80.812, + 81.29 + ], + "bpm": 125.6, + "chord": "Emaj7", + "agree": 0.75, + "strum": "..X.X..X" + }, + { + "n": 30, + "t": 81.29, + "dur": 1.907, + "beats": [ + 81.29, + 81.767, + 82.244, + 82.721, + 83.197 + ], + "bpm": 125.8, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 31, + "t": 83.197, + "dur": 1.903, + "beats": [ + 83.197, + 83.673, + 84.149, + 84.624, + 85.1 + ], + "bpm": 126.1, + "chord": "G#m", + "agree": 1, + "strum": "X..XXXXX" + }, + { + "n": 32, + "t": 85.1, + "dur": 1.905, + "beats": [ + 85.1, + 85.577, + 86.053, + 86.529, + 87.005 + ], + "bpm": 126, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 33, + "t": 87.005, + "dur": 1.904, + "beats": [ + 87.005, + 87.481, + 87.957, + 88.433, + 88.909 + ], + "bpm": 126.1, + "chord": "B", + "agree": 1, + "strum": "X..XX..." + }, + { + "n": 34, + "t": 88.909, + "dur": 1.902, + "beats": [ + 88.909, + 89.384, + 89.86, + 90.335, + 90.811 + ], + "bpm": 126.2, + "chord": "B", + "agree": 0.5, + "strum": "X..XX..X" + }, + { + "n": 35, + "t": 90.811, + "dur": 1.903, + "beats": [ + 90.811, + 91.287, + 91.763, + 92.239, + 92.714 + ], + "bpm": 126.1, + "chord": "G#m", + "agree": 0.75, + "strum": "X..XX.XX" + }, + { + "n": 36, + "t": 92.714, + "dur": 1.903, + "beats": [ + 92.714, + 93.19, + 93.666, + 94.142, + 94.617 + ], + "bpm": 126.1, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 37, + "t": 94.617, + "dur": 1.903, + "beats": [ + 94.617, + 95.093, + 95.569, + 96.045, + 96.521 + ], + "bpm": 126.1, + "chord": "G#m", + "agree": 1, + "strum": "X..XXXXX" + }, + { + "n": 38, + "t": 96.521, + "dur": 1.904, + "beats": [ + 96.521, + 96.996, + 97.472, + 97.948, + 98.424, + 98.425 + ], + "bpm": 126.1, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 39, + "t": 98.424, + "dur": 1.904, + "beats": [ + 98.424, + 98.9, + 99.376, + 99.852, + 100.328 + ], + "bpm": 126, + "chord": "B", + "agree": 0.75, + "strum": "X..XX.XX" + }, + { + "n": 40, + "t": 100.328, + "dur": 1.904, + "beats": [ + 100.328, + 100.804, + 101.28, + 101.756, + 102.232 + ], + "bpm": 126.1, + "chord": "B", + "agree": 0.5, + "strum": "X..XX..X" + }, + { + "n": 41, + "t": 102.232, + "dur": 1.902, + "beats": [ + 102.232, + 102.708, + 103.184, + 103.659, + 104.134 + ], + "bpm": 126.2, + "chord": "Emaj7", + "agree": 0.75, + "strum": "X..XX..X" + }, + { + "n": 42, + "t": 104.134, + "dur": 1.907, + "beats": [ + 104.134, + 104.61, + 105.086, + 105.564, + 106.041 + ], + "bpm": 125.8, + "chord": "B", + "agree": 0.75, + "strum": "XXXXX.X." + }, + { + "n": 43, + "t": 106.041, + "dur": 1.597, + "beats": [ + 106.041, + 106.519, + 106.997, + 107.475, + 107.638 + ], + "bpm": 125.5, + "chord": "G#m", + "agree": 0.75, + "strum": "XXX.XXXX" + }, + { + "n": 51, + "t": 107.638, + "dur": 1.071, + "beats": [ + 107.638, + 107.755, + 108.232, + 108.71 + ], + "bpm": 125.6, + "chord": "G#m", + "agree": 1, + "strum": ".XX.XX" + }, + { + "n": 52, + "t": 108.71, + "dur": 1.902, + "beats": [ + 108.71, + 109.186, + 109.661, + 110.137, + 110.612 + ], + "bpm": 126.2, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 53, + "t": 110.612, + "dur": 1.898, + "beats": [ + 110.612, + 111.086, + 111.561, + 112.036, + 112.51 + ], + "bpm": 126.4, + "chord": "B", + "agree": 1, + "strum": "X..XXX.X" + }, + { + "n": 54, + "t": 112.51, + "dur": 1.891, + "beats": [ + 112.51, + 112.984, + 113.456, + 113.928, + 114.401 + ], + "bpm": 126.9, + "chord": "B", + "agree": 0.5, + "strum": "X..XX..." + }, + { + "n": 55, + "t": 114.401, + "dur": 1.892, + "beats": [ + 114.401, + 114.874, + 115.348, + 115.82, + 116.293 + ], + "bpm": 126.8, + "chord": "G#m", + "agree": 0.5, + "strum": "X..XXXXX" + }, + { + "n": 56, + "t": 116.293, + "dur": 1.89, + "beats": [ + 116.293, + 116.766, + 117.238, + 117.711, + 118.183 + ], + "bpm": 127, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 57, + "t": 118.183, + "dur": 1.888, + "beats": [ + 118.183, + 118.655, + 119.127, + 119.599, + 120.071 + ], + "bpm": 127.1, + "chord": "G#m", + "agree": 1, + "strum": "X..XXXXX" + }, + { + "n": 58, + "t": 120.071, + "dur": 1.886, + "beats": [ + 120.071, + 120.542, + 121.013, + 121.485, + 121.957 + ], + "bpm": 127.3, + "chord": "B", + "agree": 0.75, + "strum": "X..XX..X" + }, + { + "n": 59, + "t": 121.957, + "dur": 1.882, + "beats": [ + 121.957, + 122.427, + 122.898, + 123.368, + 123.839 + ], + "bpm": 127.5, + "chord": "B", + "agree": 0.5, + "strum": "X..XXXXX" + }, + { + "n": 60, + "t": 123.839, + "dur": 1.884, + "beats": [ + 123.839, + 124.31, + 124.781, + 125.252, + 125.723 + ], + "bpm": 127.4, + "chord": "B", + "agree": 0.5, + "strum": "X..XX..X" + }, + { + "n": 61, + "t": 125.723, + "dur": 1.887, + "beats": [ + 125.723, + 126.194, + 126.666, + 127.138, + 127.61 + ], + "bpm": 127.2, + "chord": "B", + "agree": 0.5, + "strum": "X..XXXXX" + }, + { + "n": 62, + "t": 127.61, + "dur": 1.891, + "beats": [ + 127.61, + 128.082, + 128.555, + 129.028, + 129.501 + ], + "bpm": 126.9, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 63, + "t": 129.501, + "dur": 1.893, + "beats": [ + 129.501, + 129.973, + 130.446, + 130.92, + 131.394 + ], + "bpm": 126.8, + "chord": "G#m", + "agree": 1, + "strum": "X..XX.XX" + }, + { + "n": 64, + "t": 131.394, + "dur": 1.897, + "beats": [ + 131.394, + 131.868, + 132.342, + 132.816, + 133.291 + ], + "bpm": 126.5, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 65, + "t": 133.291, + "dur": 1.899, + "beats": [ + 133.291, + 133.766, + 134.241, + 134.716, + 135.19 + ], + "bpm": 126.4, + "chord": "B", + "agree": 1, + "strum": "X.XXX..." + }, + { + "n": 66, + "t": 135.19, + "dur": 1.895, + "beats": [ + 135.19, + 135.664, + 136.137, + 136.611, + 137.085 + ], + "bpm": 126.6, + "chord": "B", + "agree": 0.5, + "strum": "X..XXXXX" + }, + { + "n": 67, + "t": 137.085, + "dur": 1.897, + "beats": [ + 137.085, + 137.559, + 138.033, + 138.508, + 138.982 + ], + "bpm": 126.5, + "chord": "G#m", + "agree": 0.75, + "strum": "X..XXX.X" + }, + { + "n": 68, + "t": 138.982, + "dur": 1.894, + "beats": [ + 138.982, + 139.456, + 139.93, + 140.403, + 140.876 + ], + "bpm": 126.7, + "chord": "G#m", + "agree": 1, + "strum": "XXXXXXX." + }, + { + "n": 69, + "t": 140.876, + "dur": 1.89, + "beats": [ + 140.876, + 141.348, + 141.821, + 142.293, + 142.766 + ], + "bpm": 127, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 70, + "t": 142.766, + "dur": 1.89, + "beats": [ + 142.766, + 143.238, + 143.71, + 144.183, + 144.655, + 144.656 + ], + "bpm": 127, + "chord": "G#m", + "agree": 1, + "strum": "X..XXXXX" + }, + { + "n": 71, + "t": 144.655, + "dur": 1.889, + "beats": [ + 144.655, + 145.128, + 145.6, + 146.073, + 146.544 + ], + "bpm": 127, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 72, + "t": 146.544, + "dur": 1.888, + "beats": [ + 146.544, + 147.016, + 147.488, + 147.96, + 148.432 + ], + "bpm": 127.1, + "chord": "B", + "agree": 0.5, + "strum": "X.XXX.XX" + }, + { + "n": 73, + "t": 148.432, + "dur": 1.89, + "beats": [ + 148.432, + 148.905, + 149.377, + 149.85, + 150.322 + ], + "bpm": 127, + "chord": "Emaj7", + "agree": 0.5, + "strum": "X..XX..X" + }, + { + "n": 74, + "t": 150.322, + "dur": 1.89, + "beats": [ + 150.322, + 150.795, + 151.267, + 151.74, + 152.213 + ], + "bpm": 127, + "chord": "B", + "agree": 0.5, + "strum": "X..XX.X." + }, + { + "n": 75, + "t": 152.213, + "dur": 1.889, + "beats": [ + 152.213, + 152.685, + 153.157, + 153.629, + 154.101, + 154.102 + ], + "bpm": 127.1, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 76, + "t": 154.101, + "dur": 1.891, + "beats": [ + 154.101, + 154.574, + 155.047, + 155.52, + 155.992 + ], + "bpm": 126.9, + "chord": "G#m", + "agree": 1, + "strum": "XX.XXXX." + }, + { + "n": 77, + "t": 155.992, + "dur": 1.889, + "beats": [ + 155.992, + 156.465, + 156.937, + 157.409, + 157.882 + ], + "bpm": 127, + "chord": "B", + "agree": 1, + "strum": "X..XXX.X" + }, + { + "n": 78, + "t": 157.882, + "dur": 1.889, + "beats": [ + 157.882, + 158.354, + 158.827, + 159.299, + 159.771 + ], + "bpm": 127, + "chord": "B", + "agree": 1, + "strum": "XXXXX..X" + }, + { + "n": 79, + "t": 159.771, + "dur": 1.885, + "beats": [ + 159.771, + 160.243, + 160.715, + 161.186, + 161.656 + ], + "bpm": 127.3, + "chord": "B", + "agree": 0.75, + "strum": "X..XX.XX" + }, + { + "n": 80, + "t": 161.656, + "dur": 1.875, + "beats": [ + 161.656, + 162.126, + 162.595, + 163.064, + 163.532 + ], + "bpm": 128, + "chord": "Emaj7", + "agree": 0.5, + "strum": "X..XX..X" + }, + { + "n": 81, + "t": 163.532, + "dur": 1.87, + "beats": [ + 163.532, + 164, + 164.467, + 164.934, + 165.401, + 165.402 + ], + "bpm": 128.4, + "chord": "G#m", + "agree": 1, + "strum": "XXXXXXXX" + }, + { + "n": 82, + "t": 165.401, + "dur": 1.865, + "beats": [ + 165.401, + 165.868, + 166.334, + 166.8, + 167.266 + ], + "bpm": 128.7, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 83, + "t": 167.266, + "dur": 1.864, + "beats": [ + 167.266, + 167.733, + 168.199, + 168.665, + 169.131 + ], + "bpm": 128.7, + "chord": "G#m", + "agree": 1, + "strum": "X..XXXXX" + }, + { + "n": 84, + "t": 169.131, + "dur": 1.87, + "beats": [ + 169.131, + 169.598, + 170.065, + 170.533, + 171.001 + ], + "bpm": 128.4, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + } + ], + "strumList": [ + { + "t": 1.707, + "amp": 0.3, + "dir": "D" + }, + { + "t": 2.159, + "amp": 0.48, + "dir": "U" + }, + { + "t": 2.798, + "amp": 0.1, + "dir": "D" + }, + { + "t": 3.274, + "amp": 0.26, + "dir": "D" + }, + { + "t": 3.599, + "amp": 1.06, + "dir": "D" + }, + { + "t": 3.68, + "amp": 0.31, + "dir": "D" + }, + { + "t": 5.317, + "amp": 0.23, + "dir": "D" + }, + { + "t": 5.677, + "amp": 0.3, + "dir": "D" + }, + { + "t": 5.968, + "amp": 0.46, + "dir": "D" + }, + { + "t": 6.223, + "amp": 0.26, + "dir": "D" + }, + { + "t": 6.931, + "amp": 0.24, + "dir": "D" + }, + { + "t": 10.832, + "amp": 1.16, + "dir": "U" + }, + { + "t": 11.285, + "amp": 0.34, + "dir": "D" + }, + { + "t": 11.459, + "amp": 0.42, + "dir": "D" + }, + { + "t": 11.598, + "amp": 0.15, + "dir": "D" + }, + { + "t": 11.773, + "amp": 0.55, + "dir": "U" + }, + { + "t": 12.307, + "amp": 0.51, + "dir": "U" + }, + { + "t": 12.562, + "amp": 0.41, + "dir": "U" + }, + { + "t": 12.748, + "amp": 0.63, + "dir": "U" + }, + { + "t": 13.247, + "amp": 0.24, + "dir": "D" + }, + { + "t": 13.41, + "amp": 0.36, + "dir": "D" + }, + { + "t": 13.537, + "amp": 0.29, + "dir": "U" + }, + { + "t": 13.711, + "amp": 0.61, + "dir": "U" + }, + { + "t": 14.176, + "amp": 0.26, + "dir": "D" + }, + { + "t": 14.35, + "amp": 0.41, + "dir": "U" + }, + { + "t": 14.512, + "amp": 0.12, + "dir": "D" + }, + { + "t": 14.663, + "amp": 0.74, + "dir": "U" + }, + { + "t": 15.163, + "amp": 0.15, + "dir": "D" + }, + { + "t": 15.453, + "amp": 0.33, + "dir": "U" + }, + { + "t": 15.627, + "amp": 0.66, + "dir": "U" + }, + { + "t": 16.173, + "amp": 0.5, + "dir": "D" + }, + { + "t": 16.335, + "amp": 0.41, + "dir": "D" + }, + { + "t": 16.625, + "amp": 0.5, + "dir": "U" + }, + { + "t": 17.16, + "amp": 0.12, + "dir": "D" + }, + { + "t": 17.415, + "amp": 0.6, + "dir": "U" + }, + { + "t": 17.589, + "amp": 0.7, + "dir": "U" + }, + { + "t": 18.019, + "amp": 0.24, + "dir": "D" + }, + { + "t": 18.181, + "amp": 0.35, + "dir": "D" + }, + { + "t": 18.332, + "amp": 0.24, + "dir": "D" + }, + { + "t": 18.506, + "amp": 0.38, + "dir": "D" + }, + { + "t": 19.203, + "amp": 0.64, + "dir": "D" + }, + { + "t": 19.365, + "amp": 0.19, + "dir": "U" + }, + { + "t": 19.505, + "amp": 0.37, + "dir": "U" + }, + { + "t": 20.004, + "amp": 0.28, + "dir": "D" + }, + { + "t": 20.132, + "amp": 0.3, + "dir": "D" + }, + { + "t": 20.283, + "amp": 0.18, + "dir": "U" + }, + { + "t": 20.434, + "amp": 0.46, + "dir": "U" + }, + { + "t": 21.072, + "amp": 0.43, + "dir": "D" + }, + { + "t": 21.211, + "amp": 0.48, + "dir": "U" + }, + { + "t": 21.386, + "amp": 0.55, + "dir": "U" + }, + { + "t": 21.966, + "amp": 0.62, + "dir": "D" + }, + { + "t": 22.163, + "amp": 0.21, + "dir": "D" + }, + { + "t": 22.326, + "amp": 0.79, + "dir": "U" + }, + { + "t": 22.883, + "amp": 0.46, + "dir": "D" + }, + { + "t": 23.023, + "amp": 0.2, + "dir": "D" + }, + { + "t": 23.174, + "amp": 0.2, + "dir": "D" + }, + { + "t": 23.336, + "amp": 0.86, + "dir": "U" + }, + { + "t": 23.859, + "amp": 0.61, + "dir": "D" + }, + { + "t": 24.009, + "amp": 0.32, + "dir": "D" + }, + { + "t": 24.311, + "amp": 0.73, + "dir": "U" + }, + { + "t": 24.892, + "amp": 0.46, + "dir": "D" + }, + { + "t": 25.031, + "amp": 0.43, + "dir": "D" + }, + { + "t": 25.217, + "amp": 0.14, + "dir": "D" + }, + { + "t": 25.368, + "amp": 0.59, + "dir": "U" + }, + { + "t": 25.902, + "amp": 0.43, + "dir": "D" + }, + { + "t": 26.204, + "amp": 0.59, + "dir": "D" + }, + { + "t": 26.529, + "amp": 0.62, + "dir": "U" + }, + { + "t": 27.365, + "amp": 0.45, + "dir": "D" + }, + { + "t": 27.655, + "amp": 0.84, + "dir": "U" + }, + { + "t": 28.456, + "amp": 0.49, + "dir": "D" + }, + { + "t": 28.735, + "amp": 0.35, + "dir": "D" + }, + { + "t": 29.513, + "amp": 0.59, + "dir": "D" + }, + { + "t": 29.78, + "amp": 0.78, + "dir": "D" + }, + { + "t": 30.557, + "amp": 0.38, + "dir": "D" + }, + { + "t": 30.836, + "amp": 0.79, + "dir": "U" + }, + { + "t": 31.684, + "amp": 0.48, + "dir": "D" + }, + { + "t": 31.939, + "amp": 0.92, + "dir": "U" + }, + { + "t": 32.74, + "amp": 0.24, + "dir": "D" + }, + { + "t": 32.984, + "amp": 0.91, + "dir": "U" + }, + { + "t": 33.75, + "amp": 0.52, + "dir": "D" + }, + { + "t": 34.017, + "amp": 0.66, + "dir": "U" + }, + { + "t": 34.795, + "amp": 0.38, + "dir": "D" + }, + { + "t": 35.097, + "amp": 0.83, + "dir": "U" + }, + { + "t": 35.944, + "amp": 0.45, + "dir": "D" + }, + { + "t": 36.188, + "amp": 0.74, + "dir": "U" + }, + { + "t": 36.989, + "amp": 0.49, + "dir": "D" + }, + { + "t": 37.268, + "amp": 0.53, + "dir": "D" + }, + { + "t": 38.081, + "amp": 0.54, + "dir": "U" + }, + { + "t": 38.348, + "amp": 0.66, + "dir": "U" + }, + { + "t": 38.882, + "amp": 0.62, + "dir": "U" + }, + { + "t": 39.219, + "amp": 0.1, + "dir": "D" + }, + { + "t": 39.427, + "amp": 0.81, + "dir": "U" + }, + { + "t": 40.252, + "amp": 0.53, + "dir": "D" + }, + { + "t": 40.496, + "amp": 0.7, + "dir": "U" + }, + { + "t": 41.355, + "amp": 0.44, + "dir": "D" + }, + { + "t": 41.575, + "amp": 0.92, + "dir": "U" + }, + { + "t": 42.4, + "amp": 0.46, + "dir": "D" + }, + { + "t": 42.632, + "amp": 0.74, + "dir": "D" + }, + { + "t": 42.887, + "amp": 0.46, + "dir": "D" + }, + { + "t": 43.143, + "amp": 0.71, + "dir": "U" + }, + { + "t": 43.41, + "amp": 0.49, + "dir": "D" + }, + { + "t": 43.677, + "amp": 0.65, + "dir": "D" + }, + { + "t": 44.501, + "amp": 0.25, + "dir": "D" + }, + { + "t": 44.756, + "amp": 0.94, + "dir": "D" + }, + { + "t": 45.569, + "amp": 0.73, + "dir": "D" + }, + { + "t": 45.836, + "amp": 0.67, + "dir": "U" + }, + { + "t": 46.624, + "amp": 0.48, + "dir": "D" + }, + { + "t": 46.891, + "amp": 0.72, + "dir": "U" + }, + { + "t": 47.645, + "amp": 0.61, + "dir": "D" + }, + { + "t": 47.947, + "amp": 0.79, + "dir": "U" + }, + { + "t": 48.739, + "amp": 0.48, + "dir": "D" + }, + { + "t": 48.988, + "amp": 0.93, + "dir": "U" + }, + { + "t": 49.76, + "amp": 0.47, + "dir": "D" + }, + { + "t": 50.025, + "amp": 0.82, + "dir": "U" + }, + { + "t": 50.815, + "amp": 0.49, + "dir": "D" + }, + { + "t": 51.048, + "amp": 0.59, + "dir": "U" + }, + { + "t": 51.784, + "amp": 0.64, + "dir": "D" + }, + { + "t": 52.072, + "amp": 0.75, + "dir": "U" + }, + { + "t": 52.836, + "amp": 0.64, + "dir": "D" + }, + { + "t": 53.09, + "amp": 0.9, + "dir": "U" + }, + { + "t": 53.833, + "amp": 0.61, + "dir": "D" + }, + { + "t": 54.108, + "amp": 0.8, + "dir": "U" + }, + { + "t": 54.836, + "amp": 0.55, + "dir": "D" + }, + { + "t": 55.126, + "amp": 0.82, + "dir": "U" + }, + { + "t": 55.635, + "amp": 0.74, + "dir": "D" + }, + { + "t": 56.143, + "amp": 1.16, + "dir": "U" + }, + { + "t": 56.912, + "amp": 0.36, + "dir": "D" + }, + { + "t": 57.157, + "amp": 0.69, + "dir": "U" + }, + { + "t": 57.928, + "amp": 0.68, + "dir": "U" + }, + { + "t": 58.172, + "amp": 1.08, + "dir": "U" + }, + { + "t": 58.949, + "amp": 0.59, + "dir": "D" + }, + { + "t": 60.141, + "amp": 0.78, + "dir": "U" + }, + { + "t": 60.435, + "amp": 0.39, + "dir": "D" + }, + { + "t": 60.62, + "amp": 0.98, + "dir": "U" + }, + { + "t": 61.341, + "amp": 0.45, + "dir": "D" + }, + { + "t": 61.578, + "amp": 0.75, + "dir": "D" + }, + { + "t": 62.325, + "amp": 0.53, + "dir": "D" + }, + { + "t": 62.538, + "amp": 0.7, + "dir": "D" + }, + { + "t": 63.255, + "amp": 0.49, + "dir": "D" + }, + { + "t": 63.501, + "amp": 0.56, + "dir": "D" + }, + { + "t": 64.233, + "amp": 0.6, + "dir": "D" + }, + { + "t": 64.465, + "amp": 0.79, + "dir": "U" + }, + { + "t": 65.213, + "amp": 0.46, + "dir": "D" + }, + { + "t": 65.43, + "amp": 0.7, + "dir": "U" + }, + { + "t": 66.15, + "amp": 0.61, + "dir": "D" + }, + { + "t": 66.394, + "amp": 0.69, + "dir": "U" + }, + { + "t": 67.172, + "amp": 0.35, + "dir": "D" + }, + { + "t": 67.358, + "amp": 0.78, + "dir": "U" + }, + { + "t": 68.06, + "amp": 0.57, + "dir": "U" + }, + { + "t": 68.323, + "amp": 1.08, + "dir": "U" + }, + { + "t": 69.048, + "amp": 0.59, + "dir": "D" + }, + { + "t": 69.287, + "amp": 0.9, + "dir": "U" + }, + { + "t": 70.026, + "amp": 0.63, + "dir": "D" + }, + { + "t": 70.251, + "amp": 0.96, + "dir": "U" + }, + { + "t": 70.962, + "amp": 0.56, + "dir": "D" + }, + { + "t": 71.213, + "amp": 0.81, + "dir": "U" + }, + { + "t": 71.694, + "amp": 0.72, + "dir": "U" + }, + { + "t": 72.174, + "amp": 0.92, + "dir": "U" + }, + { + "t": 72.909, + "amp": 0.23, + "dir": "D" + }, + { + "t": 73.136, + "amp": 0.82, + "dir": "U" + }, + { + "t": 73.856, + "amp": 0.62, + "dir": "U" + }, + { + "t": 74.095, + "amp": 0.92, + "dir": "U" + }, + { + "t": 74.824, + "amp": 0.48, + "dir": "D" + }, + { + "t": 75.949, + "amp": 0.82, + "dir": "U" + }, + { + "t": 76.455, + "amp": 0.7, + "dir": "D" + }, + { + "t": 77.464, + "amp": 0.97, + "dir": "U" + }, + { + "t": 78.123, + "amp": 0.56, + "dir": "D" + }, + { + "t": 78.422, + "amp": 0.76, + "dir": "U" + }, + { + "t": 79.049, + "amp": 0.42, + "dir": "D" + }, + { + "t": 79.857, + "amp": 1.08, + "dir": "U" + }, + { + "t": 80.335, + "amp": 0.77, + "dir": "D" + }, + { + "t": 81.062, + "amp": 0.58, + "dir": "D" + }, + { + "t": 81.29, + "amp": 0.94, + "dir": "U" + }, + { + "t": 82.091, + "amp": 0.32, + "dir": "D" + }, + { + "t": 82.244, + "amp": 0.72, + "dir": "U" + }, + { + "t": 82.99, + "amp": 0.66, + "dir": "U" + }, + { + "t": 83.22, + "amp": 0.31, + "dir": "D" + }, + { + "t": 83.81, + "amp": 0.49, + "dir": "D" + }, + { + "t": 84.149, + "amp": 0.95, + "dir": "U" + }, + { + "t": 84.392, + "amp": 0.27, + "dir": "D" + }, + { + "t": 84.624, + "amp": 0.54, + "dir": "D" + }, + { + "t": 84.824, + "amp": 0.33, + "dir": "D" + }, + { + "t": 85.1, + "amp": 0.78, + "dir": "U" + }, + { + "t": 85.851, + "amp": 0.66, + "dir": "D" + }, + { + "t": 86.053, + "amp": 0.8, + "dir": "U" + }, + { + "t": 86.795, + "amp": 0.65, + "dir": "D" + }, + { + "t": 87.005, + "amp": 0.88, + "dir": "U" + }, + { + "t": 87.725, + "amp": 0.6, + "dir": "U" + }, + { + "t": 87.957, + "amp": 0.9, + "dir": "U" + }, + { + "t": 88.909, + "amp": 1.38, + "dir": "U" + }, + { + "t": 89.639, + "amp": 0.56, + "dir": "D" + }, + { + "t": 89.86, + "amp": 0.63, + "dir": "U" + }, + { + "t": 90.429, + "amp": 0.12, + "dir": "D" + }, + { + "t": 90.625, + "amp": 0.28, + "dir": "D" + }, + { + "t": 90.811, + "amp": 0.7, + "dir": "U" + }, + { + "t": 91.543, + "amp": 0.49, + "dir": "D" + }, + { + "t": 91.763, + "amp": 0.7, + "dir": "U" + }, + { + "t": 92.034, + "amp": 0.2, + "dir": "D" + }, + { + "t": 92.239, + "amp": 1, + "dir": "U" + }, + { + "t": 92.509, + "amp": 0.3, + "dir": "D" + }, + { + "t": 92.714, + "amp": 1.24, + "dir": "U" + }, + { + "t": 93.44, + "amp": 0.59, + "dir": "D" + }, + { + "t": 93.666, + "amp": 0.84, + "dir": "U" + }, + { + "t": 94.39, + "amp": 0.64, + "dir": "D" + }, + { + "t": 94.617, + "amp": 1.03, + "dir": "U" + }, + { + "t": 95.345, + "amp": 0.53, + "dir": "D" + }, + { + "t": 95.569, + "amp": 0.6, + "dir": "D" + }, + { + "t": 95.817, + "amp": 0.22, + "dir": "D" + }, + { + "t": 96.045, + "amp": 0.81, + "dir": "U" + }, + { + "t": 96.293, + "amp": 0.32, + "dir": "D" + }, + { + "t": 96.521, + "amp": 0.95, + "dir": "U" + }, + { + "t": 97.229, + "amp": 0.67, + "dir": "D" + }, + { + "t": 97.472, + "amp": 0.91, + "dir": "U" + }, + { + "t": 98.186, + "amp": 0.54, + "dir": "U" + }, + { + "t": 98.424, + "amp": 0.89, + "dir": "U" + }, + { + "t": 99.133, + "amp": 0.5, + "dir": "D" + }, + { + "t": 99.376, + "amp": 0.69, + "dir": "U" + }, + { + "t": 99.852, + "amp": 0.66, + "dir": "D" + }, + { + "t": 100.118, + "amp": 0.35, + "dir": "D" + }, + { + "t": 100.328, + "amp": 0.77, + "dir": "U" + }, + { + "t": 101.034, + "amp": 0.57, + "dir": "D" + }, + { + "t": 101.28, + "amp": 0.69, + "dir": "U" + }, + { + "t": 102.046, + "amp": 0.48, + "dir": "D" + }, + { + "t": 102.232, + "amp": 0.98, + "dir": "U" + }, + { + "t": 102.951, + "amp": 0.67, + "dir": "D" + }, + { + "t": 103.184, + "amp": 0.76, + "dir": "U" + }, + { + "t": 103.896, + "amp": 0.52, + "dir": "D" + }, + { + "t": 104.134, + "amp": 0.76, + "dir": "U" + }, + { + "t": 104.41, + "amp": 0.27, + "dir": "D" + }, + { + "t": 104.61, + "amp": 0.5, + "dir": "D" + }, + { + "t": 104.837, + "amp": 0.36, + "dir": "D" + }, + { + "t": 105.086, + "amp": 0.61, + "dir": "U" + }, + { + "t": 105.353, + "amp": 0.11, + "dir": "D" + }, + { + "t": 105.564, + "amp": 0.39, + "dir": "D" + }, + { + "t": 106.041, + "amp": 0.42, + "dir": "D" + }, + { + "t": 106.314, + "amp": 0.26, + "dir": "D" + }, + { + "t": 106.519, + "amp": 0.71, + "dir": "D" + }, + { + "t": 106.769, + "amp": 0.16, + "dir": "D" + }, + { + "t": 106.997, + "amp": 0.47, + "dir": "D" + }, + { + "t": 107.231, + "amp": 0.3, + "dir": "D" + }, + { + "t": 107.755, + "amp": 0.82, + "dir": "U" + }, + { + "t": 108.232, + "amp": 1.12, + "dir": "U" + }, + { + "t": 108.503, + "amp": 0.51, + "dir": "U" + }, + { + "t": 108.71, + "amp": 0.7, + "dir": "U" + }, + { + "t": 109.418, + "amp": 0.44, + "dir": "D" + }, + { + "t": 109.661, + "amp": 0.8, + "dir": "U" + }, + { + "t": 110.388, + "amp": 0.63, + "dir": "D" + }, + { + "t": 110.612, + "amp": 0.54, + "dir": "U" + }, + { + "t": 111.315, + "amp": 0.7, + "dir": "U" + }, + { + "t": 111.561, + "amp": 0.67, + "dir": "U" + }, + { + "t": 111.815, + "amp": 0.26, + "dir": "D" + }, + { + "t": 112.268, + "amp": 0.58, + "dir": "D" + }, + { + "t": 112.51, + "amp": 0.7, + "dir": "U" + }, + { + "t": 113.229, + "amp": 0.55, + "dir": "D" + }, + { + "t": 113.456, + "amp": 0.77, + "dir": "U" + }, + { + "t": 114.401, + "amp": 0.92, + "dir": "U" + }, + { + "t": 115.108, + "amp": 0.64, + "dir": "D" + }, + { + "t": 115.348, + "amp": 0.91, + "dir": "U" + }, + { + "t": 115.601, + "amp": 0.27, + "dir": "D" + }, + { + "t": 115.82, + "amp": 0.99, + "dir": "U" + }, + { + "t": 116.057, + "amp": 0.45, + "dir": "D" + }, + { + "t": 116.293, + "amp": 0.8, + "dir": "U" + }, + { + "t": 117.016, + "amp": 0.5, + "dir": "D" + }, + { + "t": 117.238, + "amp": 0.8, + "dir": "U" + }, + { + "t": 117.985, + "amp": 0.51, + "dir": "D" + }, + { + "t": 118.183, + "amp": 1.25, + "dir": "U" + }, + { + "t": 118.894, + "amp": 0.63, + "dir": "D" + }, + { + "t": 119.127, + "amp": 0.99, + "dir": "U" + }, + { + "t": 119.381, + "amp": 0.24, + "dir": "D" + }, + { + "t": 119.599, + "amp": 0.45, + "dir": "D" + }, + { + "t": 119.84, + "amp": 0.4, + "dir": "D" + }, + { + "t": 120.071, + "amp": 0.87, + "dir": "U" + }, + { + "t": 120.772, + "amp": 0.48, + "dir": "U" + }, + { + "t": 121.013, + "amp": 0.64, + "dir": "U" + }, + { + "t": 121.743, + "amp": 0.62, + "dir": "D" + }, + { + "t": 121.957, + "amp": 0.58, + "dir": "U" + }, + { + "t": 122.529, + "amp": 0.11, + "dir": "D" + }, + { + "t": 122.674, + "amp": 0.47, + "dir": "D" + }, + { + "t": 122.898, + "amp": 1.01, + "dir": "U" + }, + { + "t": 123.138, + "amp": 0.3, + "dir": "D" + }, + { + "t": 123.368, + "amp": 0.58, + "dir": "U" + }, + { + "t": 123.615, + "amp": 0.37, + "dir": "U" + }, + { + "t": 123.839, + "amp": 0.88, + "dir": "U" + }, + { + "t": 124.571, + "amp": 0.49, + "dir": "D" + }, + { + "t": 124.781, + "amp": 0.9, + "dir": "U" + }, + { + "t": 125.488, + "amp": 0.81, + "dir": "D" + }, + { + "t": 125.723, + "amp": 0.78, + "dir": "U" + }, + { + "t": 126.468, + "amp": 0.45, + "dir": "D" + }, + { + "t": 126.666, + "amp": 0.92, + "dir": "U" + }, + { + "t": 126.914, + "amp": 0.62, + "dir": "U" + }, + { + "t": 127.138, + "amp": 0.64, + "dir": "U" + }, + { + "t": 127.401, + "amp": 0.43, + "dir": "D" + }, + { + "t": 127.61, + "amp": 1, + "dir": "U" + }, + { + "t": 128.335, + "amp": 0.49, + "dir": "D" + }, + { + "t": 128.555, + "amp": 1.04, + "dir": "U" + }, + { + "t": 129.305, + "amp": 0.71, + "dir": "D" + }, + { + "t": 129.501, + "amp": 0.93, + "dir": "U" + }, + { + "t": 130.229, + "amp": 0.61, + "dir": "D" + }, + { + "t": 130.446, + "amp": 1, + "dir": "U" + }, + { + "t": 130.716, + "amp": 0.16, + "dir": "D" + }, + { + "t": 130.92, + "amp": 0.55, + "dir": "D" + }, + { + "t": 131.189, + "amp": 0.3, + "dir": "D" + }, + { + "t": 131.394, + "amp": 1.14, + "dir": "U" + }, + { + "t": 132.119, + "amp": 0.61, + "dir": "U" + }, + { + "t": 132.342, + "amp": 0.43, + "dir": "U" + }, + { + "t": 132.639, + "amp": 0.18, + "dir": "D" + }, + { + "t": 133.097, + "amp": 0.37, + "dir": "D" + }, + { + "t": 133.291, + "amp": 0.82, + "dir": "U" + }, + { + "t": 133.766, + "amp": 0.65, + "dir": "D" + }, + { + "t": 134.048, + "amp": 0.26, + "dir": "D" + }, + { + "t": 134.241, + "amp": 0.82, + "dir": "U" + }, + { + "t": 135.19, + "amp": 0.81, + "dir": "U" + }, + { + "t": 135.482, + "amp": 0.14, + "dir": "D" + }, + { + "t": 135.701, + "amp": 0.18, + "dir": "D" + }, + { + "t": 135.929, + "amp": 0.54, + "dir": "D" + }, + { + "t": 136.137, + "amp": 0.98, + "dir": "U" + }, + { + "t": 136.397, + "amp": 0.3, + "dir": "D" + }, + { + "t": 136.611, + "amp": 0.71, + "dir": "U" + }, + { + "t": 136.849, + "amp": 0.24, + "dir": "D" + }, + { + "t": 137.085, + "amp": 1.02, + "dir": "U" + }, + { + "t": 137.353, + "amp": 0.17, + "dir": "D" + }, + { + "t": 137.811, + "amp": 0.66, + "dir": "U" + }, + { + "t": 138.033, + "amp": 0.79, + "dir": "U" + }, + { + "t": 138.304, + "amp": 0.28, + "dir": "D" + }, + { + "t": 138.745, + "amp": 0.74, + "dir": "U" + }, + { + "t": 138.982, + "amp": 0.83, + "dir": "U" + }, + { + "t": 139.224, + "amp": 0.24, + "dir": "D" + }, + { + "t": 139.456, + "amp": 0.8, + "dir": "U" + }, + { + "t": 139.687, + "amp": 0.38, + "dir": "D" + }, + { + "t": 139.93, + "amp": 0.81, + "dir": "U" + }, + { + "t": 140.173, + "amp": 0.51, + "dir": "U" + }, + { + "t": 140.403, + "amp": 0.77, + "dir": "U" + }, + { + "t": 140.645, + "amp": 0.18, + "dir": "D" + }, + { + "t": 140.876, + "amp": 0.77, + "dir": "U" + }, + { + "t": 141.59, + "amp": 0.56, + "dir": "D" + }, + { + "t": 141.821, + "amp": 0.69, + "dir": "U" + }, + { + "t": 142.521, + "amp": 0.59, + "dir": "U" + }, + { + "t": 142.766, + "amp": 0.9, + "dir": "U" + }, + { + "t": 143.474, + "amp": 0.85, + "dir": "D" + }, + { + "t": 143.71, + "amp": 0.93, + "dir": "U" + }, + { + "t": 143.952, + "amp": 0.4, + "dir": "U" + }, + { + "t": 144.183, + "amp": 0.64, + "dir": "D" + }, + { + "t": 144.407, + "amp": 0.22, + "dir": "D" + }, + { + "t": 144.655, + "amp": 0.66, + "dir": "U" + }, + { + "t": 145.342, + "amp": 0.59, + "dir": "U" + }, + { + "t": 145.6, + "amp": 0.64, + "dir": "U" + }, + { + "t": 146.289, + "amp": 0.72, + "dir": "U" + }, + { + "t": 146.544, + "amp": 0.89, + "dir": "U" + }, + { + "t": 147.016, + "amp": 1.02, + "dir": "U" + }, + { + "t": 147.235, + "amp": 0.35, + "dir": "U" + }, + { + "t": 147.488, + "amp": 0.78, + "dir": "U" + }, + { + "t": 147.718, + "amp": 0.19, + "dir": "U" + }, + { + "t": 147.96, + "amp": 0.72, + "dir": "U" + }, + { + "t": 148.233, + "amp": 0.29, + "dir": "D" + }, + { + "t": 148.432, + "amp": 0.99, + "dir": "U" + }, + { + "t": 149.161, + "amp": 0.53, + "dir": "D" + }, + { + "t": 149.377, + "amp": 0.87, + "dir": "U" + }, + { + "t": 150.08, + "amp": 0.38, + "dir": "D" + }, + { + "t": 150.322, + "amp": 0.82, + "dir": "U" + }, + { + "t": 151.055, + "amp": 0.68, + "dir": "D" + }, + { + "t": 151.267, + "amp": 1, + "dir": "U" + }, + { + "t": 151.553, + "amp": 0.12, + "dir": "D" + }, + { + "t": 151.74, + "amp": 0.73, + "dir": "U" + }, + { + "t": 152.213, + "amp": 1.23, + "dir": "U" + }, + { + "t": 152.943, + "amp": 0.73, + "dir": "D" + }, + { + "t": 153.145, + "amp": 0.27, + "dir": "D" + }, + { + "t": 153.865, + "amp": 0.4, + "dir": "D" + }, + { + "t": 154.101, + "amp": 0.93, + "dir": "U" + }, + { + "t": 154.369, + "amp": 0.23, + "dir": "D" + }, + { + "t": 154.835, + "amp": 0.34, + "dir": "D" + }, + { + "t": 155.047, + "amp": 0.87, + "dir": "D" + }, + { + "t": 155.317, + "amp": 0.25, + "dir": "D" + }, + { + "t": 155.52, + "amp": 0.36, + "dir": "D" + }, + { + "t": 155.805, + "amp": 0.15, + "dir": "D" + }, + { + "t": 155.992, + "amp": 1.18, + "dir": "U" + }, + { + "t": 156.73, + "amp": 0.51, + "dir": "D" + }, + { + "t": 156.937, + "amp": 0.73, + "dir": "U" + }, + { + "t": 157.207, + "amp": 0.3, + "dir": "D" + }, + { + "t": 157.657, + "amp": 0.51, + "dir": "U" + }, + { + "t": 157.882, + "amp": 0.81, + "dir": "U" + }, + { + "t": 158.144, + "amp": 0.29, + "dir": "D" + }, + { + "t": 158.354, + "amp": 0.45, + "dir": "D" + }, + { + "t": 158.591, + "amp": 0.44, + "dir": "U" + }, + { + "t": 158.827, + "amp": 0.61, + "dir": "U" + }, + { + "t": 159.532, + "amp": 0.48, + "dir": "U" + }, + { + "t": 159.771, + "amp": 0.89, + "dir": "U" + }, + { + "t": 160.479, + "amp": 0.36, + "dir": "D" + }, + { + "t": 160.715, + "amp": 0.91, + "dir": "U" + }, + { + "t": 161.186, + "amp": 0.76, + "dir": "U" + }, + { + "t": 161.432, + "amp": 0.22, + "dir": "D" + }, + { + "t": 161.656, + "amp": 1.12, + "dir": "U" + }, + { + "t": 162.346, + "amp": 0.89, + "dir": "U" + }, + { + "t": 162.595, + "amp": 0.95, + "dir": "U" + }, + { + "t": 163.32, + "amp": 0.71, + "dir": "D" + }, + { + "t": 163.532, + "amp": 0.73, + "dir": "U" + }, + { + "t": 163.778, + "amp": 0.42, + "dir": "U" + }, + { + "t": 164, + "amp": 0.75, + "dir": "U" + }, + { + "t": 164.239, + "amp": 0.36, + "dir": "D" + }, + { + "t": 164.467, + "amp": 0.91, + "dir": "U" + }, + { + "t": 164.701, + "amp": 0.33, + "dir": "U" + }, + { + "t": 164.934, + "amp": 0.78, + "dir": "U" + }, + { + "t": 165.167, + "amp": 0.36, + "dir": "D" + }, + { + "t": 165.401, + "amp": 0.91, + "dir": "U" + }, + { + "t": 166.098, + "amp": 0.54, + "dir": "D" + }, + { + "t": 166.334, + "amp": 0.78, + "dir": "U" + }, + { + "t": 167.03, + "amp": 0.64, + "dir": "U" + }, + { + "t": 167.266, + "amp": 0.8, + "dir": "U" + }, + { + "t": 167.972, + "amp": 0.57, + "dir": "D" + }, + { + "t": 168.199, + "amp": 0.73, + "dir": "U" + }, + { + "t": 168.443, + "amp": 0.51, + "dir": "D" + }, + { + "t": 168.676, + "amp": 0.23, + "dir": "D" + }, + { + "t": 168.886, + "amp": 0.54, + "dir": "D" + }, + { + "t": 169.131, + "amp": 0.92, + "dir": "U" + }, + { + "t": 169.825, + "amp": 0.56, + "dir": "U" + }, + { + "t": 170.065, + "amp": 0.71, + "dir": "U" + }, + { + "t": 170.769, + "amp": 0.62, + "dir": "U" + }, + { + "t": 171.001, + "amp": 0.8, + "dir": "U" + } + ], + "regularized": { + "keep": 0.3, + "floorBar": 24, + "medianBpm": 120.2, + "totalSec": 173.545 + }, + "cut": { + "segments": [ + { + "name": "A intro+verse1", + "from": 0, + "to": 59.182, + "offset": 0 + }, + { + "name": "S screw", + "from": 56.143, + "to": 57.102, + "offset": 59.182 + }, + { + "name": "B verse2", + "from": 90.711, + "to": 105.506, + "offset": 60.141 + }, + { + "name": "C chorus1", + "from": 58.172, + "to": 90.875, + "offset": 74.936 + }, + { + "name": "D chorus2..end", + "from": 105.506, + "to": 169.368, + "offset": 107.638 + } + ], + "totalSec": 171.501 + } +} \ No newline at end of file diff --git a/pop/sailor-song/measures.cut.json.off b/pop/sailor-song/measures.cut.json.off new file mode 100644 index 0000000000..4f6706726f --- /dev/null +++ b/pop/sailor-song/measures.cut.json.off @@ -0,0 +1,3146 @@ +{ + "strums": { + "count": 425, + "perBeat": 1.23, + "phaseHist8": [ + 4, + 4, + 10, + 14, + 132, + 46, + 1, + 2 + ] + }, + "beats": { + "snapped": [ + 3.5989999999999998, + 4.073, + 4.546, + 5.02, + 5.494, + 5.968, + 6.454, + 6.94, + 7.427, + 7.913, + 8.4, + 8.886, + 9.373, + 9.859, + 10.346, + 10.832, + 11.302, + 11.773, + 12.307, + 12.748, + 13.23, + 13.711, + 14.187, + 14.663, + 15.145, + 15.627, + 16.173, + 16.625, + 17.107, + 17.589, + 18.048, + 18.506, + 19.006, + 19.505, + 19.969, + 20.434, + 20.91, + 21.386, + 21.856, + 22.326, + 22.883, + 23.336, + 23.859, + 24.311, + 24.892, + 25.368, + 25.902, + 26.389, + 26.877, + 27.365, + 27.848, + 28.331, + 28.814, + 29.297, + 29.78, + 30.308, + 30.836, + 31.388, + 31.939, + 32.461, + 32.984, + 33.501, + 34.017, + 34.557, + 35.097, + 35.643, + 36.188, + 36.728, + 37.268, + 37.808, + 38.348, + 38.882, + 39.427, + 39.962, + 40.496, + 41.035, + 41.575, + 42.104, + 42.632, + 43.143, + 43.677, + 44.217, + 44.756, + 45.296, + 45.836, + 46.363126965355846, + 46.8905137054648, + 47.41821310483894, + 47.94715455486204, + 48.46752275144404, + 48.987675609169045, + 49.50697437994057, + 50.02542507870751, + 50.53652233479391, + 51.04812940154079, + 51.56008063076585, + 52.07241961955978, + 52.58129418498595, + 53.09008787847, + 53.59888157195405, + 54.10785525989205, + 54.616828947830044, + 55.125622641314095, + 55.634589060954305, + 56.14263359231784, + 56.64975956898965, + 57.15696410426977, + 57.6642631186425, + 58.17152788872035, + 58.52767473575531, + 59.03049091946477, + 59.53361859108877, + 60.03745955081618, + 60.541776090036606, + 61.0467764728829, + 61.55266648673174, + 62.05899243383939, + 62.56551400507817, + 63.072138424400805, + 63.57838996436497, + 64.08435285829104, + 64.59033230045218, + 65.09648975104687, + 65.60284190799256, + 66.10917749231506, + 66.61560937255852, + 67.12185893618577, + 67.62810849981302, + 68.13423802067669, + 68.64008286909466, + 69.14564336511484, + 69.65073027942691, + 70.15534449845194, + 70.65995871747697, + 71.16477556766026, + 71.66967575154322, + 72.17372753788365, + 72.67683081922509, + 73.17941492901942, + 73.68132404785098, + 74.18264993139928, + 74.6835844965664, + 75.18362601446562, + 75.68304855182136, + 76.18205697306855, + 76.6807864817178, + 77.17941395456708, + 77.67793943335914, + 78.17625859891874, + 78.67417391221517, + 79.17134346833369, + 79.66759022034175, + 80.16345353997313, + 80.65983433042051, + 81.15666498577814, + 81.65349564113576, + 82.1499930724518, + 82.64650793993593, + 83.14260189778625, + 83.63858230278383, + 84.13465519385706, + 84.6306378473214, + 85.12649422012367, + 85.62205146305621, + 86.11750554181693, + 86.61276401556776, + 87.10768302572131, + 87.60260203587487, + 88.0978605096257, + 88.59292353275521, + 89.0874523311346, + 89.58153542159909, + 90.07507297328807, + 90.56942338666303, + 91.06375651555467, + 91.55850757018268, + 92.0532586248107, + 92.54780478853853, + 93.0426488623926, + 93.53780941088196, + 94.0333693535003, + 94.5291333304714, + 95.02498746549098, + 95.52129051089055, + 96.01782391471163, + 96.51422760390723, + 97.01063129310283, + 97.50737648632786, + 98.00444530646594, + 98.50197670227304, + 98.9995872054155, + 99.49767914051107, + 99.99557338261904, + 100.49409790854618, + 100.9929403540793, + 101.49158924436584, + 101.98991718671003, + 102.48784126257077, + 102.98555952257352, + 103.48296129382096, + 103.98045163918461, + 104.47782779162041, + 104.97554907350913, + 105.4734679110315, + 105.97147769212962, + 106.4696148532373, + 106.96773358943524, + 107.46517350959816, + 107.96171159286313, + 108.45824967612809, + 108.95436851043517, + 109.45039709041963, + 109.94640784266552, + 110.44254495422793, + 110.9384577634295, + 111.43437057263107, + 111.93028338183264, + 112.42657969998116, + 112.92298971569933, + 113.41907514913773, + 113.91474040040605, + 114.41009176764258, + 114.90544313487912, + 115.40090273751387, + 115.89655810409603, + 116.39263363608258, + 116.88870916806913, + 117.38480297412052, + 117.88087850610707, + 118.37686379952403, + 118.87283126831376, + 119.36913124991828, + 119.86521668335668, + 120.36174044875087, + 120.85746007932708, + 121.35306632832875, + 121.84876492390704, + 122.34468762806573, + 122.84141512654638, + 123.33803427763083, + 123.83445203689182, + 124.33056261153486, + 124.82645880170465, + 125.32235499187443, + 125.81823292253493, + 126.31452969492541, + 126.81071561555119, + 127.30670049550132, + 127.80255909365574, + 128.29843594989575, + 128.7936403790344, + 129.2888448081731, + 129.78383426348836, + 130.27789935542427, + 130.77140736910985, + 131.26376751089754, + 131.755574410448, + 132.24691770222734, + 132.7381496052932, + 133.22917952126494, + 133.71977855158525, + 134.20994564127096, + 134.69950338967402, + 135.18895055737545, + 135.67861087261446, + 136.16837695025714, + 136.65778002251164, + 137.14727974733768, + 137.63661104400313, + 138.12602269074665, + 138.61623020971103, + 139.1067973112161, + 139.5978310148086, + 140.08939789892736, + 140.57739789892736, + 141.06439789892738, + 141.54039789892738, + 142.01639789892738 + ], + "hitFraction": 0.584, + "trackerOffsetMs": { + "median": -35, + "p90abs": 46.5 + } + }, + "meter": { + "gridShift": "motif-anchored, 2 beat-count flips at [27.85, 61.17]", + "beatsPerBar": 4, + "phase": 1, + "downbeatChangeFraction": 0.75 + }, + "bars": [ + { + "n": 1, + "t": 5.02, + "dur": 1.92, + "beats": [ + 5.02, + 5.494, + 5.968, + 6.454, + 6.94 + ], + "bpm": 125, + "chord": "G#m", + "agree": 1, + "strum": ".X.XXX.." + }, + { + "n": 2, + "t": 6.94, + "dur": 1.946, + "beats": [ + 6.94, + 7.427, + 7.913, + 8.4, + 8.886 + ], + "bpm": 123.3, + "chord": "G#m", + "agree": 0.75, + "strum": "X......." + }, + { + "n": 3, + "t": 8.886, + "dur": 1.946, + "beats": [ + 8.886, + 9.373, + 9.859, + 10.346, + 10.832 + ], + "bpm": 123.3, + "chord": "G#m", + "agree": 1, + "strum": "........" + }, + { + "n": 4, + "t": 10.832, + "dur": 1.916, + "beats": [ + 10.832, + 11.302, + 11.773, + 12.307, + 12.748 + ], + "bpm": 125.3, + "chord": "B", + "agree": 0.5, + "strum": "X.XXX.XX" + }, + { + "n": 5, + "t": 12.748, + "dur": 1.915, + "beats": [ + 12.748, + 13.23, + 13.711, + 14.187, + 14.663 + ], + "bpm": 125.3, + "chord": "B", + "agree": 0.5, + "strum": "X.XXX.XX" + }, + { + "n": 6, + "t": 14.663, + "dur": 1.962, + "beats": [ + 14.663, + 15.145, + 15.627, + 16.173, + 16.625 + ], + "bpm": 122.3, + "chord": "G#m", + "agree": 1, + "strum": "X..XX.XX" + }, + { + "n": 7, + "t": 16.625, + "dur": 1.881, + "beats": [ + 16.625, + 17.107, + 17.589, + 18.048, + 18.506 + ], + "bpm": 127.6, + "chord": "G#m", + "agree": 1, + "strum": "X..XX.XX" + }, + { + "n": 8, + "t": 18.506, + "dur": 1.928, + "beats": [ + 18.506, + 19.006, + 19.505, + 19.969, + 20.434 + ], + "bpm": 124.5, + "chord": "B", + "agree": 1, + "strum": "X..XX.XX" + }, + { + "n": 9, + "t": 20.434, + "dur": 1.892, + "beats": [ + 20.434, + 20.91, + 21.386, + 21.856, + 22.326 + ], + "bpm": 126.8, + "chord": "B", + "agree": 0.75, + "strum": "X..XX.XX" + }, + { + "n": 10, + "t": 22.326, + "dur": 1.985, + "beats": [ + 22.326, + 22.883, + 23.336, + 23.859, + 24.311 + ], + "bpm": 120.9, + "chord": "G#m", + "agree": 0.5, + "strum": "X.X.X.XX" + }, + { + "n": 11, + "t": 24.311, + "dur": 2.078, + "beats": [ + 24.311, + 24.892, + 25.368, + 25.902, + 26.389 + ], + "bpm": 115.5, + "chord": "B", + "agree": 0.75, + "strum": "X.XXX.XX" + }, + { + "n": 12, + "t": 26.389, + "dur": 2.425, + "beats": [ + 26.389, + 26.877, + 27.365, + 27.848, + 28.331, + 28.814 + ], + "bpm": 123.7, + "chord": "B", + "agree": 1, + "strum": ".X..XX...X" + }, + { + "n": 13, + "t": 28.814, + "dur": 2.022, + "beats": [ + 28.814, + 29.297, + 29.78, + 30.308, + 30.836 + ], + "bpm": 118.7, + "chord": "B", + "agree": 0.5, + "strum": "...XX..X" + }, + { + "n": 14, + "t": 30.836, + "dur": 2.148, + "beats": [ + 30.836, + 31.388, + 31.939, + 32.461, + 32.984 + ], + "bpm": 111.7, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 15, + "t": 32.984, + "dur": 2.113, + "beats": [ + 32.984, + 33.501, + 34.017, + 34.557, + 35.097 + ], + "bpm": 113.6, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 16, + "t": 35.097, + "dur": 2.171, + "beats": [ + 35.097, + 35.643, + 36.188, + 36.728, + 37.268 + ], + "bpm": 110.5, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 17, + "t": 37.268, + "dur": 2.159, + "beats": [ + 37.268, + 37.808, + 38.348, + 38.882, + 39.427 + ], + "bpm": 111.2, + "chord": "B", + "agree": 0.75, + "strum": "X..XX.X." + }, + { + "n": 18, + "t": 39.427, + "dur": 2.148, + "beats": [ + 39.427, + 39.962, + 40.496, + 41.035, + 41.575 + ], + "bpm": 111.7, + "chord": "Emaj7", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 19, + "t": 41.575, + "dur": 2.102, + "beats": [ + 41.575, + 42.104, + 42.632, + 43.143, + 43.677 + ], + "bpm": 114.2, + "chord": "B", + "agree": 0.5, + "strum": "X..XXXXX" + }, + { + "n": 20, + "t": 43.677, + "dur": 2.159, + "beats": [ + 43.677, + 44.217, + 44.756, + 45.296, + 45.836 + ], + "bpm": 111.2, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 21, + "t": 45.836, + "dur": 2.111, + "beats": [ + 45.836, + 46.363, + 46.891, + 47.418, + 47.947 + ], + "bpm": 113.7, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 22, + "t": 47.947, + "dur": 2.078, + "beats": [ + 47.947, + 48.468, + 48.988, + 49.507, + 50.025 + ], + "bpm": 115.5, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 23, + "t": 50.025, + "dur": 2.047, + "beats": [ + 50.025, + 50.537, + 51.048, + 51.56, + 52.072 + ], + "bpm": 117.2, + "chord": "B", + "agree": 0.75, + "strum": "X..XX..X" + }, + { + "n": 24, + "t": 52.072, + "dur": 2.035, + "beats": [ + 52.072, + 52.581, + 53.09, + 53.599, + 54.108 + ], + "bpm": 117.9, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 25, + "t": 54.108, + "dur": 2.035, + "beats": [ + 54.108, + 54.617, + 55.126, + 55.635, + 56.143 + ], + "bpm": 117.9, + "chord": "B", + "agree": 1, + "strum": "X..XX.X." + }, + { + "n": 26, + "t": 56.143, + "dur": 2.029, + "beats": [ + 56.143, + 56.65, + 57.157, + 57.664, + 58.172 + ], + "bpm": 118.3, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 43, + "t": 58.172, + "dur": 0.355, + "beats": [ + 58.172, + 58.527 + ], + "bpm": 119.5, + "chord": "G#m", + "agree": 0.75, + "strum": "XX" + }, + { + "n": 44, + "t": 58.527, + "dur": 2.014, + "beats": [ + 58.527, + 59.03, + 59.533, + 60.037, + 60.542 + ], + "bpm": 119.2, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 45, + "t": 60.542, + "dur": 2.024, + "beats": [ + 60.542, + 61.047, + 61.552, + 62.059, + 62.565 + ], + "bpm": 118.6, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 46, + "t": 62.565, + "dur": 2.025, + "beats": [ + 62.565, + 63.072, + 63.578, + 64.084, + 64.59 + ], + "bpm": 118.5, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 47, + "t": 64.59, + "dur": 2.025, + "beats": [ + 64.59, + 65.096, + 65.603, + 66.109, + 66.615 + ], + "bpm": 118.5, + "chord": "B", + "agree": 0.75, + "strum": "X..XX..X" + }, + { + "n": 48, + "t": 66.615, + "dur": 2.024, + "beats": [ + 66.615, + 67.122, + 67.628, + 68.134, + 68.64 + ], + "bpm": 118.5, + "chord": "G#m", + "agree": 0.75, + "strum": "X..XX..X" + }, + { + "n": 49, + "t": 68.64, + "dur": 2.02, + "beats": [ + 68.64, + 69.145, + 69.65, + 70.155, + 70.66 + ], + "bpm": 118.8, + "chord": "Emaj7", + "agree": 0.75, + "strum": "X..XX.X." + }, + { + "n": 50, + "t": 70.66, + "dur": 2.017, + "beats": [ + 70.66, + 71.165, + 71.669, + 72.173, + 72.677 + ], + "bpm": 119, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 51, + "t": 72.677, + "dur": 2.007, + "beats": [ + 72.677, + 73.179, + 73.681, + 74.182, + 74.683 + ], + "bpm": 119.6, + "chord": "G#m", + "agree": 1, + "strum": "X..XX.XX" + }, + { + "n": 52, + "t": 74.683, + "dur": 1.997, + "beats": [ + 74.683, + 75.183, + 75.683, + 76.182, + 76.681 + ], + "bpm": 120.2, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 53, + "t": 76.681, + "dur": 1.993, + "beats": [ + 76.681, + 77.179, + 77.678, + 78.176, + 78.674 + ], + "bpm": 120.4, + "chord": "B", + "agree": 1, + "strum": "X..XXX.X" + }, + { + "n": 54, + "t": 78.674, + "dur": 1.986, + "beats": [ + 78.674, + 79.171, + 79.667, + 80.163, + 80.66 + ], + "bpm": 120.9, + "chord": "B", + "agree": 0.5, + "strum": "X..XX..." + }, + { + "n": 55, + "t": 80.66, + "dur": 1.987, + "beats": [ + 80.66, + 81.156, + 81.653, + 82.15, + 82.646 + ], + "bpm": 120.8, + "chord": "G#m", + "agree": 0.5, + "strum": "X..XXXXX" + }, + { + "n": 56, + "t": 82.646, + "dur": 1.984, + "beats": [ + 82.646, + 83.142, + 83.638, + 84.134, + 84.63 + ], + "bpm": 121, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 57, + "t": 84.63, + "dur": 1.982, + "beats": [ + 84.63, + 85.126, + 85.622, + 86.117, + 86.613 + ], + "bpm": 121.1, + "chord": "G#m", + "agree": 1, + "strum": "X..XXXXX" + }, + { + "n": 58, + "t": 86.613, + "dur": 1.98, + "beats": [ + 86.613, + 87.107, + 87.602, + 88.098, + 88.593 + ], + "bpm": 121.2, + "chord": "B", + "agree": 0.75, + "strum": "X..XX..X" + }, + { + "n": 59, + "t": 88.593, + "dur": 1.976, + "beats": [ + 88.593, + 89.087, + 89.581, + 90.075, + 90.569 + ], + "bpm": 121.4, + "chord": "B", + "agree": 0.5, + "strum": "X..XXXXX" + }, + { + "n": 60, + "t": 90.569, + "dur": 1.978, + "beats": [ + 90.569, + 91.064, + 91.558, + 92.053, + 92.548 + ], + "bpm": 121.3, + "chord": "B", + "agree": 0.5, + "strum": "X..XX..X" + }, + { + "n": 61, + "t": 92.548, + "dur": 1.981, + "beats": [ + 92.548, + 93.042, + 93.538, + 94.033, + 94.529 + ], + "bpm": 121.1, + "chord": "B", + "agree": 0.5, + "strum": "X..XXXXX" + }, + { + "n": 62, + "t": 94.529, + "dur": 1.985, + "beats": [ + 94.529, + 95.025, + 95.521, + 96.018, + 96.514 + ], + "bpm": 120.9, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 63, + "t": 96.514, + "dur": 1.988, + "beats": [ + 96.514, + 97.01, + 97.507, + 98.004, + 98.502 + ], + "bpm": 120.7, + "chord": "G#m", + "agree": 1, + "strum": "X..XX.XX" + }, + { + "n": 64, + "t": 98.502, + "dur": 1.992, + "beats": [ + 98.502, + 98.999, + 99.497, + 99.995, + 100.494 + ], + "bpm": 120.5, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 65, + "t": 100.494, + "dur": 1.994, + "beats": [ + 100.494, + 100.993, + 101.491, + 101.99, + 102.488 + ], + "bpm": 120.4, + "chord": "B", + "agree": 1, + "strum": "X.XXX..." + }, + { + "n": 66, + "t": 102.488, + "dur": 1.99, + "beats": [ + 102.488, + 102.985, + 103.483, + 103.98, + 104.478 + ], + "bpm": 120.6, + "chord": "B", + "agree": 0.5, + "strum": "X..XXXXX" + }, + { + "n": 67, + "t": 104.478, + "dur": 1.992, + "beats": [ + 104.478, + 104.975, + 105.473, + 105.971, + 106.469 + ], + "bpm": 120.5, + "chord": "G#m", + "agree": 0.75, + "strum": "X..XXX.X" + }, + { + "n": 68, + "t": 106.469, + "dur": 1.989, + "beats": [ + 106.469, + 106.967, + 107.465, + 107.961, + 108.458 + ], + "bpm": 120.7, + "chord": "G#m", + "agree": 1, + "strum": "XXXXXXX." + }, + { + "n": 69, + "t": 108.458, + "dur": 1.984, + "beats": [ + 108.458, + 108.954, + 109.45, + 109.946, + 110.442 + ], + "bpm": 120.9, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 70, + "t": 110.442, + "dur": 1.984, + "beats": [ + 110.442, + 110.938, + 111.434, + 111.93, + 112.426 + ], + "bpm": 121, + "chord": "G#m", + "agree": 1, + "strum": "X..XXXXX" + }, + { + "n": 71, + "t": 112.426, + "dur": 1.984, + "beats": [ + 112.426, + 112.923, + 113.419, + 113.915, + 114.41 + ], + "bpm": 121, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 72, + "t": 114.41, + "dur": 1.983, + "beats": [ + 114.41, + 114.905, + 115.401, + 115.896, + 116.392 + ], + "bpm": 121.1, + "chord": "B", + "agree": 0.5, + "strum": "X.XXX.XX" + }, + { + "n": 73, + "t": 116.392, + "dur": 1.984, + "beats": [ + 116.392, + 116.888, + 117.385, + 117.881, + 118.377 + ], + "bpm": 121, + "chord": "Emaj7", + "agree": 0.5, + "strum": "X..XX..X" + }, + { + "n": 74, + "t": 118.377, + "dur": 1.985, + "beats": [ + 118.377, + 118.873, + 119.369, + 119.865, + 120.362 + ], + "bpm": 120.9, + "chord": "B", + "agree": 0.5, + "strum": "X..XX.X." + }, + { + "n": 75, + "t": 120.362, + "dur": 1.983, + "beats": [ + 120.362, + 120.857, + 121.353, + 121.849, + 122.344 + ], + "bpm": 121, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 76, + "t": 122.344, + "dur": 1.986, + "beats": [ + 122.344, + 122.841, + 123.338, + 123.834, + 124.33 + ], + "bpm": 120.9, + "chord": "G#m", + "agree": 1, + "strum": "XX.XXXX." + }, + { + "n": 77, + "t": 124.33, + "dur": 1.984, + "beats": [ + 124.33, + 124.826, + 125.322, + 125.818, + 126.314 + ], + "bpm": 121, + "chord": "B", + "agree": 1, + "strum": "X..XXX.X" + }, + { + "n": 78, + "t": 126.314, + "dur": 1.984, + "beats": [ + 126.314, + 126.81, + 127.306, + 127.802, + 128.298 + ], + "bpm": 121, + "chord": "B", + "agree": 1, + "strum": "XXXXX..X" + }, + { + "n": 79, + "t": 128.298, + "dur": 1.979, + "beats": [ + 128.298, + 128.793, + 129.289, + 129.784, + 130.278 + ], + "bpm": 121.2, + "chord": "B", + "agree": 0.75, + "strum": "X..XX.XX" + }, + { + "n": 80, + "t": 130.278, + "dur": 1.969, + "beats": [ + 130.278, + 130.771, + 131.264, + 131.755, + 132.247 + ], + "bpm": 121.9, + "chord": "Emaj7", + "agree": 0.5, + "strum": "X..XX..X" + }, + { + "n": 81, + "t": 132.247, + "dur": 1.963, + "beats": [ + 132.247, + 132.738, + 133.229, + 133.72, + 134.21 + ], + "bpm": 122.3, + "chord": "G#m", + "agree": 1, + "strum": "XXXXXXXX" + }, + { + "n": 82, + "t": 134.21, + "dur": 1.958, + "beats": [ + 134.21, + 134.699, + 135.189, + 135.678, + 136.168 + ], + "bpm": 122.5, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 83, + "t": 136.168, + "dur": 1.958, + "beats": [ + 136.168, + 136.658, + 137.147, + 137.636, + 138.126 + ], + "bpm": 122.6, + "chord": "G#m", + "agree": 1, + "strum": "X..XXXXX" + }, + { + "n": 84, + "t": 138.126, + "dur": 1.963, + "beats": [ + 138.126, + 138.616, + 139.107, + 139.598, + 140.089 + ], + "bpm": 122.2, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + } + ], + "strumList": [ + { + "t": 1.707, + "amp": 0.3, + "dir": "D" + }, + { + "t": 2.159, + "amp": 0.48, + "dir": "U" + }, + { + "t": 2.798, + "amp": 0.1, + "dir": "D" + }, + { + "t": 3.274, + "amp": 0.26, + "dir": "D" + }, + { + "t": 3.599, + "amp": 1.06, + "dir": "D" + }, + { + "t": 3.68, + "amp": 0.31, + "dir": "D" + }, + { + "t": 5.317, + "amp": 0.23, + "dir": "D" + }, + { + "t": 5.677, + "amp": 0.3, + "dir": "D" + }, + { + "t": 5.968, + "amp": 0.46, + "dir": "D" + }, + { + "t": 6.223, + "amp": 0.26, + "dir": "D" + }, + { + "t": 6.931, + "amp": 0.24, + "dir": "D" + }, + { + "t": 10.832, + "amp": 1.16, + "dir": "U" + }, + { + "t": 11.285, + "amp": 0.34, + "dir": "D" + }, + { + "t": 11.459, + "amp": 0.42, + "dir": "D" + }, + { + "t": 11.598, + "amp": 0.15, + "dir": "D" + }, + { + "t": 11.773, + "amp": 0.55, + "dir": "U" + }, + { + "t": 12.307, + "amp": 0.51, + "dir": "U" + }, + { + "t": 12.562, + "amp": 0.41, + "dir": "U" + }, + { + "t": 12.748, + "amp": 0.63, + "dir": "U" + }, + { + "t": 13.247, + "amp": 0.24, + "dir": "D" + }, + { + "t": 13.41, + "amp": 0.36, + "dir": "D" + }, + { + "t": 13.537, + "amp": 0.29, + "dir": "U" + }, + { + "t": 13.711, + "amp": 0.61, + "dir": "U" + }, + { + "t": 14.176, + "amp": 0.26, + "dir": "D" + }, + { + "t": 14.35, + "amp": 0.41, + "dir": "U" + }, + { + "t": 14.512, + "amp": 0.12, + "dir": "D" + }, + { + "t": 14.663, + "amp": 0.74, + "dir": "U" + }, + { + "t": 15.163, + "amp": 0.15, + "dir": "D" + }, + { + "t": 15.453, + "amp": 0.33, + "dir": "U" + }, + { + "t": 15.627, + "amp": 0.66, + "dir": "U" + }, + { + "t": 16.173, + "amp": 0.5, + "dir": "D" + }, + { + "t": 16.335, + "amp": 0.41, + "dir": "D" + }, + { + "t": 16.625, + "amp": 0.5, + "dir": "U" + }, + { + "t": 17.16, + "amp": 0.12, + "dir": "D" + }, + { + "t": 17.415, + "amp": 0.6, + "dir": "U" + }, + { + "t": 17.589, + "amp": 0.7, + "dir": "U" + }, + { + "t": 18.019, + "amp": 0.24, + "dir": "D" + }, + { + "t": 18.181, + "amp": 0.35, + "dir": "D" + }, + { + "t": 18.332, + "amp": 0.24, + "dir": "D" + }, + { + "t": 18.506, + "amp": 0.38, + "dir": "D" + }, + { + "t": 19.203, + "amp": 0.64, + "dir": "D" + }, + { + "t": 19.365, + "amp": 0.19, + "dir": "U" + }, + { + "t": 19.505, + "amp": 0.37, + "dir": "U" + }, + { + "t": 20.004, + "amp": 0.28, + "dir": "D" + }, + { + "t": 20.132, + "amp": 0.3, + "dir": "D" + }, + { + "t": 20.283, + "amp": 0.18, + "dir": "U" + }, + { + "t": 20.434, + "amp": 0.46, + "dir": "U" + }, + { + "t": 21.072, + "amp": 0.43, + "dir": "D" + }, + { + "t": 21.211, + "amp": 0.48, + "dir": "U" + }, + { + "t": 21.386, + "amp": 0.55, + "dir": "U" + }, + { + "t": 21.966, + "amp": 0.62, + "dir": "D" + }, + { + "t": 22.163, + "amp": 0.21, + "dir": "D" + }, + { + "t": 22.326, + "amp": 0.79, + "dir": "U" + }, + { + "t": 22.883, + "amp": 0.46, + "dir": "D" + }, + { + "t": 23.023, + "amp": 0.2, + "dir": "D" + }, + { + "t": 23.174, + "amp": 0.2, + "dir": "D" + }, + { + "t": 23.336, + "amp": 0.86, + "dir": "U" + }, + { + "t": 23.859, + "amp": 0.61, + "dir": "D" + }, + { + "t": 24.009, + "amp": 0.32, + "dir": "D" + }, + { + "t": 24.311, + "amp": 0.73, + "dir": "U" + }, + { + "t": 24.892, + "amp": 0.46, + "dir": "D" + }, + { + "t": 25.031, + "amp": 0.43, + "dir": "D" + }, + { + "t": 25.217, + "amp": 0.14, + "dir": "D" + }, + { + "t": 25.368, + "amp": 0.59, + "dir": "U" + }, + { + "t": 25.902, + "amp": 0.43, + "dir": "D" + }, + { + "t": 26.204, + "amp": 0.59, + "dir": "D" + }, + { + "t": 26.529, + "amp": 0.62, + "dir": "U" + }, + { + "t": 27.365, + "amp": 0.45, + "dir": "D" + }, + { + "t": 27.655, + "amp": 0.84, + "dir": "U" + }, + { + "t": 28.456, + "amp": 0.49, + "dir": "D" + }, + { + "t": 28.735, + "amp": 0.35, + "dir": "D" + }, + { + "t": 29.513, + "amp": 0.59, + "dir": "D" + }, + { + "t": 29.78, + "amp": 0.78, + "dir": "D" + }, + { + "t": 30.557, + "amp": 0.38, + "dir": "D" + }, + { + "t": 30.836, + "amp": 0.79, + "dir": "U" + }, + { + "t": 31.684, + "amp": 0.48, + "dir": "D" + }, + { + "t": 31.939, + "amp": 0.92, + "dir": "U" + }, + { + "t": 32.74, + "amp": 0.24, + "dir": "D" + }, + { + "t": 32.984, + "amp": 0.91, + "dir": "U" + }, + { + "t": 33.75, + "amp": 0.52, + "dir": "D" + }, + { + "t": 34.017, + "amp": 0.66, + "dir": "U" + }, + { + "t": 34.795, + "amp": 0.38, + "dir": "D" + }, + { + "t": 35.097, + "amp": 0.83, + "dir": "U" + }, + { + "t": 35.944, + "amp": 0.45, + "dir": "D" + }, + { + "t": 36.188, + "amp": 0.74, + "dir": "U" + }, + { + "t": 36.989, + "amp": 0.49, + "dir": "D" + }, + { + "t": 37.268, + "amp": 0.53, + "dir": "D" + }, + { + "t": 38.081, + "amp": 0.54, + "dir": "U" + }, + { + "t": 38.348, + "amp": 0.66, + "dir": "U" + }, + { + "t": 38.882, + "amp": 0.62, + "dir": "U" + }, + { + "t": 39.219, + "amp": 0.1, + "dir": "D" + }, + { + "t": 39.427, + "amp": 0.81, + "dir": "U" + }, + { + "t": 40.252, + "amp": 0.53, + "dir": "D" + }, + { + "t": 40.496, + "amp": 0.7, + "dir": "U" + }, + { + "t": 41.355, + "amp": 0.44, + "dir": "D" + }, + { + "t": 41.575, + "amp": 0.92, + "dir": "U" + }, + { + "t": 42.4, + "amp": 0.46, + "dir": "D" + }, + { + "t": 42.632, + "amp": 0.74, + "dir": "D" + }, + { + "t": 42.887, + "amp": 0.46, + "dir": "D" + }, + { + "t": 43.143, + "amp": 0.71, + "dir": "U" + }, + { + "t": 43.41, + "amp": 0.49, + "dir": "D" + }, + { + "t": 43.677, + "amp": 0.65, + "dir": "D" + }, + { + "t": 44.501, + "amp": 0.25, + "dir": "D" + }, + { + "t": 44.756, + "amp": 0.94, + "dir": "D" + }, + { + "t": 45.569, + "amp": 0.73, + "dir": "D" + }, + { + "t": 45.836, + "amp": 0.67, + "dir": "U" + }, + { + "t": 46.624, + "amp": 0.48, + "dir": "D" + }, + { + "t": 46.891, + "amp": 0.72, + "dir": "U" + }, + { + "t": 47.645, + "amp": 0.61, + "dir": "D" + }, + { + "t": 47.947, + "amp": 0.79, + "dir": "U" + }, + { + "t": 48.739, + "amp": 0.48, + "dir": "D" + }, + { + "t": 48.988, + "amp": 0.93, + "dir": "U" + }, + { + "t": 49.76, + "amp": 0.47, + "dir": "D" + }, + { + "t": 50.025, + "amp": 0.82, + "dir": "U" + }, + { + "t": 50.815, + "amp": 0.49, + "dir": "D" + }, + { + "t": 51.048, + "amp": 0.59, + "dir": "U" + }, + { + "t": 51.784, + "amp": 0.64, + "dir": "D" + }, + { + "t": 52.072, + "amp": 0.75, + "dir": "U" + }, + { + "t": 52.836, + "amp": 0.64, + "dir": "D" + }, + { + "t": 53.09, + "amp": 0.9, + "dir": "U" + }, + { + "t": 53.833, + "amp": 0.61, + "dir": "D" + }, + { + "t": 54.108, + "amp": 0.8, + "dir": "U" + }, + { + "t": 54.836, + "amp": 0.55, + "dir": "D" + }, + { + "t": 55.126, + "amp": 0.82, + "dir": "U" + }, + { + "t": 55.635, + "amp": 0.74, + "dir": "D" + }, + { + "t": 56.143, + "amp": 1.16, + "dir": "U" + }, + { + "t": 56.912, + "amp": 0.36, + "dir": "D" + }, + { + "t": 57.157, + "amp": 0.69, + "dir": "U" + }, + { + "t": 57.928, + "amp": 0.68, + "dir": "U" + }, + { + "t": 58.172, + "amp": 1.08, + "dir": "U" + }, + { + "t": 58.334, + "amp": 0.39, + "dir": "D" + }, + { + "t": 58.527, + "amp": 0.98, + "dir": "U" + }, + { + "t": 59.285, + "amp": 0.45, + "dir": "D" + }, + { + "t": 59.533, + "amp": 0.75, + "dir": "D" + }, + { + "t": 60.318, + "amp": 0.53, + "dir": "D" + }, + { + "t": 60.542, + "amp": 0.7, + "dir": "D" + }, + { + "t": 61.294, + "amp": 0.49, + "dir": "D" + }, + { + "t": 61.552, + "amp": 0.56, + "dir": "D" + }, + { + "t": 62.321, + "amp": 0.6, + "dir": "D" + }, + { + "t": 62.565, + "amp": 0.79, + "dir": "U" + }, + { + "t": 63.35, + "amp": 0.46, + "dir": "D" + }, + { + "t": 63.578, + "amp": 0.7, + "dir": "U" + }, + { + "t": 64.334, + "amp": 0.61, + "dir": "D" + }, + { + "t": 64.59, + "amp": 0.69, + "dir": "U" + }, + { + "t": 65.407, + "amp": 0.35, + "dir": "D" + }, + { + "t": 65.603, + "amp": 0.78, + "dir": "U" + }, + { + "t": 66.339, + "amp": 0.57, + "dir": "U" + }, + { + "t": 66.615, + "amp": 1.08, + "dir": "U" + }, + { + "t": 67.377, + "amp": 0.59, + "dir": "D" + }, + { + "t": 67.628, + "amp": 0.9, + "dir": "U" + }, + { + "t": 68.404, + "amp": 0.63, + "dir": "D" + }, + { + "t": 68.64, + "amp": 0.96, + "dir": "U" + }, + { + "t": 69.387, + "amp": 0.56, + "dir": "D" + }, + { + "t": 69.65, + "amp": 0.81, + "dir": "U" + }, + { + "t": 70.155, + "amp": 0.72, + "dir": "U" + }, + { + "t": 70.66, + "amp": 0.92, + "dir": "U" + }, + { + "t": 71.431, + "amp": 0.23, + "dir": "D" + }, + { + "t": 71.669, + "amp": 0.82, + "dir": "U" + }, + { + "t": 72.426, + "amp": 0.62, + "dir": "U" + }, + { + "t": 72.677, + "amp": 0.92, + "dir": "U" + }, + { + "t": 73.441, + "amp": 0.48, + "dir": "D" + }, + { + "t": 73.681, + "amp": 0.82, + "dir": "U" + }, + { + "t": 74.182, + "amp": 1.12, + "dir": "U" + }, + { + "t": 74.467, + "amp": 0.51, + "dir": "U" + }, + { + "t": 74.683, + "amp": 0.7, + "dir": "U" + }, + { + "t": 75.427, + "amp": 0.44, + "dir": "D" + }, + { + "t": 75.683, + "amp": 0.8, + "dir": "U" + }, + { + "t": 76.446, + "amp": 0.63, + "dir": "D" + }, + { + "t": 76.681, + "amp": 0.54, + "dir": "U" + }, + { + "t": 77.419, + "amp": 0.7, + "dir": "U" + }, + { + "t": 77.678, + "amp": 0.67, + "dir": "U" + }, + { + "t": 77.944, + "amp": 0.26, + "dir": "D" + }, + { + "t": 78.419, + "amp": 0.58, + "dir": "D" + }, + { + "t": 78.674, + "amp": 0.7, + "dir": "U" + }, + { + "t": 79.428, + "amp": 0.55, + "dir": "D" + }, + { + "t": 79.667, + "amp": 0.77, + "dir": "U" + }, + { + "t": 80.66, + "amp": 0.92, + "dir": "U" + }, + { + "t": 81.401, + "amp": 0.64, + "dir": "D" + }, + { + "t": 81.653, + "amp": 0.91, + "dir": "U" + }, + { + "t": 81.919, + "amp": 0.27, + "dir": "D" + }, + { + "t": 82.15, + "amp": 0.99, + "dir": "U" + }, + { + "t": 82.398, + "amp": 0.45, + "dir": "D" + }, + { + "t": 82.646, + "amp": 0.8, + "dir": "U" + }, + { + "t": 83.405, + "amp": 0.5, + "dir": "D" + }, + { + "t": 83.638, + "amp": 0.8, + "dir": "U" + }, + { + "t": 84.423, + "amp": 0.51, + "dir": "D" + }, + { + "t": 84.63, + "amp": 1.25, + "dir": "U" + }, + { + "t": 85.377, + "amp": 0.63, + "dir": "D" + }, + { + "t": 85.622, + "amp": 0.99, + "dir": "U" + }, + { + "t": 85.888, + "amp": 0.24, + "dir": "D" + }, + { + "t": 86.117, + "amp": 0.45, + "dir": "D" + }, + { + "t": 86.371, + "amp": 0.4, + "dir": "D" + }, + { + "t": 86.613, + "amp": 0.87, + "dir": "U" + }, + { + "t": 87.349, + "amp": 0.48, + "dir": "U" + }, + { + "t": 87.602, + "amp": 0.64, + "dir": "U" + }, + { + "t": 88.368, + "amp": 0.62, + "dir": "D" + }, + { + "t": 88.593, + "amp": 0.58, + "dir": "U" + }, + { + "t": 89.194, + "amp": 0.11, + "dir": "D" + }, + { + "t": 89.346, + "amp": 0.47, + "dir": "D" + }, + { + "t": 89.581, + "amp": 1.01, + "dir": "U" + }, + { + "t": 89.834, + "amp": 0.3, + "dir": "D" + }, + { + "t": 90.075, + "amp": 0.58, + "dir": "U" + }, + { + "t": 90.334, + "amp": 0.37, + "dir": "U" + }, + { + "t": 90.569, + "amp": 0.88, + "dir": "U" + }, + { + "t": 91.338, + "amp": 0.49, + "dir": "D" + }, + { + "t": 91.558, + "amp": 0.9, + "dir": "U" + }, + { + "t": 92.3, + "amp": 0.81, + "dir": "D" + }, + { + "t": 92.548, + "amp": 0.78, + "dir": "U" + }, + { + "t": 93.33, + "amp": 0.45, + "dir": "D" + }, + { + "t": 93.538, + "amp": 0.92, + "dir": "U" + }, + { + "t": 93.798, + "amp": 0.62, + "dir": "U" + }, + { + "t": 94.033, + "amp": 0.64, + "dir": "U" + }, + { + "t": 94.309, + "amp": 0.43, + "dir": "D" + }, + { + "t": 94.529, + "amp": 1, + "dir": "U" + }, + { + "t": 95.29, + "amp": 0.49, + "dir": "D" + }, + { + "t": 95.521, + "amp": 1.04, + "dir": "U" + }, + { + "t": 96.309, + "amp": 0.71, + "dir": "D" + }, + { + "t": 96.514, + "amp": 0.93, + "dir": "U" + }, + { + "t": 97.279, + "amp": 0.61, + "dir": "D" + }, + { + "t": 97.507, + "amp": 1, + "dir": "U" + }, + { + "t": 97.79, + "amp": 0.16, + "dir": "D" + }, + { + "t": 98.004, + "amp": 0.55, + "dir": "D" + }, + { + "t": 98.287, + "amp": 0.3, + "dir": "D" + }, + { + "t": 98.502, + "amp": 1.14, + "dir": "U" + }, + { + "t": 99.264, + "amp": 0.61, + "dir": "U" + }, + { + "t": 99.497, + "amp": 0.43, + "dir": "U" + }, + { + "t": 99.809, + "amp": 0.18, + "dir": "D" + }, + { + "t": 100.29, + "amp": 0.37, + "dir": "D" + }, + { + "t": 100.494, + "amp": 0.82, + "dir": "U" + }, + { + "t": 100.993, + "amp": 0.65, + "dir": "D" + }, + { + "t": 101.289, + "amp": 0.26, + "dir": "D" + }, + { + "t": 101.491, + "amp": 0.82, + "dir": "U" + }, + { + "t": 102.488, + "amp": 0.81, + "dir": "U" + }, + { + "t": 102.794, + "amp": 0.14, + "dir": "D" + }, + { + "t": 103.024, + "amp": 0.18, + "dir": "D" + }, + { + "t": 103.264, + "amp": 0.54, + "dir": "D" + }, + { + "t": 103.483, + "amp": 0.98, + "dir": "U" + }, + { + "t": 103.755, + "amp": 0.3, + "dir": "D" + }, + { + "t": 103.98, + "amp": 0.71, + "dir": "U" + }, + { + "t": 104.229, + "amp": 0.24, + "dir": "D" + }, + { + "t": 104.478, + "amp": 1.02, + "dir": "U" + }, + { + "t": 104.759, + "amp": 0.17, + "dir": "D" + }, + { + "t": 105.239, + "amp": 0.66, + "dir": "U" + }, + { + "t": 105.473, + "amp": 0.79, + "dir": "U" + }, + { + "t": 105.758, + "amp": 0.28, + "dir": "D" + }, + { + "t": 106.22, + "amp": 0.74, + "dir": "U" + }, + { + "t": 106.469, + "amp": 0.83, + "dir": "U" + }, + { + "t": 106.724, + "amp": 0.24, + "dir": "D" + }, + { + "t": 106.967, + "amp": 0.8, + "dir": "U" + }, + { + "t": 107.21, + "amp": 0.38, + "dir": "D" + }, + { + "t": 107.465, + "amp": 0.81, + "dir": "U" + }, + { + "t": 107.72, + "amp": 0.51, + "dir": "U" + }, + { + "t": 107.961, + "amp": 0.77, + "dir": "U" + }, + { + "t": 108.216, + "amp": 0.18, + "dir": "D" + }, + { + "t": 108.458, + "amp": 0.77, + "dir": "U" + }, + { + "t": 109.208, + "amp": 0.56, + "dir": "D" + }, + { + "t": 109.45, + "amp": 0.69, + "dir": "U" + }, + { + "t": 110.185, + "amp": 0.59, + "dir": "U" + }, + { + "t": 110.442, + "amp": 0.9, + "dir": "U" + }, + { + "t": 111.186, + "amp": 0.85, + "dir": "D" + }, + { + "t": 111.434, + "amp": 0.93, + "dir": "U" + }, + { + "t": 111.688, + "amp": 0.4, + "dir": "U" + }, + { + "t": 111.93, + "amp": 0.64, + "dir": "D" + }, + { + "t": 112.166, + "amp": 0.22, + "dir": "D" + }, + { + "t": 112.426, + "amp": 0.66, + "dir": "U" + }, + { + "t": 113.147, + "amp": 0.59, + "dir": "U" + }, + { + "t": 113.419, + "amp": 0.64, + "dir": "U" + }, + { + "t": 114.142, + "amp": 0.72, + "dir": "U" + }, + { + "t": 114.41, + "amp": 0.89, + "dir": "U" + }, + { + "t": 114.905, + "amp": 1.02, + "dir": "U" + }, + { + "t": 115.136, + "amp": 0.35, + "dir": "U" + }, + { + "t": 115.401, + "amp": 0.78, + "dir": "U" + }, + { + "t": 115.642, + "amp": 0.19, + "dir": "U" + }, + { + "t": 115.896, + "amp": 0.72, + "dir": "U" + }, + { + "t": 116.183, + "amp": 0.29, + "dir": "D" + }, + { + "t": 116.392, + "amp": 0.99, + "dir": "U" + }, + { + "t": 117.157, + "amp": 0.53, + "dir": "D" + }, + { + "t": 117.385, + "amp": 0.87, + "dir": "U" + }, + { + "t": 118.122, + "amp": 0.38, + "dir": "D" + }, + { + "t": 118.377, + "amp": 0.82, + "dir": "U" + }, + { + "t": 119.146, + "amp": 0.68, + "dir": "D" + }, + { + "t": 119.369, + "amp": 1, + "dir": "U" + }, + { + "t": 119.669, + "amp": 0.12, + "dir": "D" + }, + { + "t": 119.865, + "amp": 0.73, + "dir": "U" + }, + { + "t": 120.362, + "amp": 1.23, + "dir": "U" + }, + { + "t": 121.128, + "amp": 0.73, + "dir": "D" + }, + { + "t": 121.341, + "amp": 0.27, + "dir": "D" + }, + { + "t": 122.096, + "amp": 0.4, + "dir": "D" + }, + { + "t": 122.344, + "amp": 0.93, + "dir": "U" + }, + { + "t": 122.626, + "amp": 0.23, + "dir": "D" + }, + { + "t": 123.116, + "amp": 0.34, + "dir": "D" + }, + { + "t": 123.338, + "amp": 0.87, + "dir": "D" + }, + { + "t": 123.621, + "amp": 0.25, + "dir": "D" + }, + { + "t": 123.834, + "amp": 0.36, + "dir": "D" + }, + { + "t": 124.134, + "amp": 0.15, + "dir": "D" + }, + { + "t": 124.33, + "amp": 1.18, + "dir": "U" + }, + { + "t": 125.104, + "amp": 0.51, + "dir": "D" + }, + { + "t": 125.322, + "amp": 0.73, + "dir": "U" + }, + { + "t": 125.605, + "amp": 0.3, + "dir": "D" + }, + { + "t": 126.078, + "amp": 0.51, + "dir": "U" + }, + { + "t": 126.314, + "amp": 0.81, + "dir": "U" + }, + { + "t": 126.59, + "amp": 0.29, + "dir": "D" + }, + { + "t": 126.81, + "amp": 0.45, + "dir": "D" + }, + { + "t": 127.058, + "amp": 0.44, + "dir": "U" + }, + { + "t": 127.306, + "amp": 0.61, + "dir": "U" + }, + { + "t": 128.047, + "amp": 0.48, + "dir": "U" + }, + { + "t": 128.298, + "amp": 0.89, + "dir": "U" + }, + { + "t": 129.041, + "amp": 0.36, + "dir": "D" + }, + { + "t": 129.289, + "amp": 0.91, + "dir": "U" + }, + { + "t": 129.784, + "amp": 0.76, + "dir": "U" + }, + { + "t": 130.042, + "amp": 0.22, + "dir": "D" + }, + { + "t": 130.278, + "amp": 1.12, + "dir": "U" + }, + { + "t": 131.002, + "amp": 0.89, + "dir": "U" + }, + { + "t": 131.264, + "amp": 0.95, + "dir": "U" + }, + { + "t": 132.024, + "amp": 0.71, + "dir": "D" + }, + { + "t": 132.247, + "amp": 0.73, + "dir": "U" + }, + { + "t": 132.505, + "amp": 0.42, + "dir": "U" + }, + { + "t": 132.738, + "amp": 0.75, + "dir": "U" + }, + { + "t": 132.99, + "amp": 0.36, + "dir": "D" + }, + { + "t": 133.229, + "amp": 0.91, + "dir": "U" + }, + { + "t": 133.475, + "amp": 0.33, + "dir": "U" + }, + { + "t": 133.72, + "amp": 0.78, + "dir": "U" + }, + { + "t": 133.964, + "amp": 0.36, + "dir": "D" + }, + { + "t": 134.21, + "amp": 0.91, + "dir": "U" + }, + { + "t": 134.941, + "amp": 0.54, + "dir": "D" + }, + { + "t": 135.189, + "amp": 0.78, + "dir": "U" + }, + { + "t": 135.92, + "amp": 0.64, + "dir": "U" + }, + { + "t": 136.168, + "amp": 0.8, + "dir": "U" + }, + { + "t": 136.908, + "amp": 0.57, + "dir": "D" + }, + { + "t": 137.147, + "amp": 0.73, + "dir": "U" + }, + { + "t": 137.404, + "amp": 0.51, + "dir": "D" + }, + { + "t": 137.648, + "amp": 0.23, + "dir": "D" + }, + { + "t": 137.868, + "amp": 0.54, + "dir": "D" + }, + { + "t": 138.126, + "amp": 0.92, + "dir": "U" + }, + { + "t": 138.855, + "amp": 0.56, + "dir": "U" + }, + { + "t": 139.107, + "amp": 0.71, + "dir": "U" + }, + { + "t": 139.846, + "amp": 0.62, + "dir": "U" + }, + { + "t": 140.089, + "amp": 0.8, + "dir": "U" + }, + { + "t": 140.832, + "amp": 0.64, + "dir": "U" + }, + { + "t": 141.064, + "amp": 0.74, + "dir": "U" + }, + { + "t": 141.285, + "amp": 0.28, + "dir": "U" + }, + { + "t": 141.54, + "amp": 0.49, + "dir": "U" + }, + { + "t": 141.749, + "amp": 0.33, + "dir": "U" + }, + { + "t": 142.016, + "amp": 0.95, + "dir": "U" + }, + { + "t": 144.396, + "amp": 0.3, + "dir": "D" + }, + { + "t": 144.675, + "amp": 0.25, + "dir": "D" + } + ], + "regularized": { + "keep": 0.3, + "floorBar": 24, + "medianBpm": 120.2, + "totalSec": 178.953 + }, + "cut": { + "spans": [ + { + "a": 58.172, + "b": 92.359, + "fromBar": 27, + "word": "And lately" + } + ], + "removedSec": 34.187 + } +} \ No newline at end of file diff --git a/pop/sailor-song/measures.json b/pop/sailor-song/measures.json new file mode 100644 index 0000000000..38c42f8439 --- /dev/null +++ b/pop/sailor-song/measures.json @@ -0,0 +1,3852 @@ +{ + "strums": { + "count": 425, + "perBeat": 1.23, + "phaseHist8": [ + 4, + 4, + 10, + 14, + 132, + 46, + 1, + 2 + ] + }, + "beats": { + "snapped": [ + 3.599, + 4.073, + 4.546, + 5.02, + 5.494, + 5.968, + 6.454, + 6.94, + 7.427, + 7.913, + 8.4, + 8.886, + 9.373, + 9.859, + 10.346, + 10.832, + 11.302, + 11.773, + 12.307, + 12.748, + 13.23, + 13.711, + 14.187, + 14.663, + 15.145, + 15.627, + 16.173, + 16.625, + 17.107, + 17.589, + 18.048, + 18.506, + 19.006, + 19.505, + 19.969, + 20.434, + 20.91, + 21.386, + 21.856, + 22.326, + 22.883, + 23.336, + 23.859, + 24.311, + 24.892, + 25.368, + 25.902, + 26.389, + 26.877, + 27.365, + 27.848, + 28.331, + 28.814, + 29.297, + 29.78, + 30.308, + 30.836, + 31.388, + 31.939, + 32.461, + 32.984, + 33.501, + 34.017, + 34.557, + 35.097, + 35.643, + 36.188, + 36.728, + 37.268, + 37.808, + 38.348, + 38.882, + 39.427, + 39.962, + 40.496, + 41.035, + 41.575, + 42.104, + 42.632, + 43.143, + 43.677, + 44.217, + 44.756, + 45.296, + 45.836, + 46.364, + 46.893, + 47.421, + 47.949, + 48.483, + 49.017, + 49.54, + 50.062, + 50.596, + 51.13, + 51.664, + 52.198, + 52.733, + 53.267, + 53.783, + 54.3, + 54.851, + 55.403, + 55.948, + 56.494, + 57.022, + 57.551, + 58.085, + 58.619, + 59.141, + 59.664, + 60.209, + 60.691, + 61.173, + 61.713, + 62.253, + 62.767, + 63.282, + 63.797, + 64.296, + 64.784, + 65.271, + 65.776, + 66.281, + 66.786, + 67.291, + 67.796, + 68.301, + 68.824, + 69.323, + 69.817, + 70.31, + 70.809, + 71.308, + 71.808, + 72.307, + 72.812, + 73.317, + 73.816, + 74.315, + 74.82, + 75.326, + 75.831, + 76.336, + 76.823, + 77.334, + 77.822, + 78.309, + 78.82, + 79.331, + 79.824, + 80.318, + 80.829, + 81.34, + 81.839, + 82.338, + 82.826, + 83.313, + 83.836, + 84.358, + 84.857, + 85.357, + 85.85, + 86.343, + 86.848, + 87.353, + 87.853, + 88.352, + 88.863, + 89.374, + 89.873, + 90.36, + 90.86, + 91.359, + 91.847, + 92.334, + 92.833, + 93.344, + 93.884, + 94.424, + 94.946, + 95.469, + 95.991, + 96.514, + 97.042, + 97.57, + 98.087, + 98.604, + 99.132, + 99.66, + 100.2, + 100.74, + 101.251, + 101.761, + 102.278, + 102.795, + 103.317, + 103.84, + 104.374, + 104.908, + 105.43, + 105.964, + 106.481, + 106.998, + 107.508, + 108.019, + 108.53, + 109.041, + 109.552, + 110.063, + 110.585, + 111.107, + 111.578, + 112.048, + 112.541, + 113.035, + 113.534, + 114.033, + 114.515, + 114.997, + 115.49, + 115.984, + 116.489, + 116.994, + 117.493, + 117.981, + 118.451, + 118.921, + 119.42, + 119.919, + 120.413, + 120.906, + 121.382, + 121.882, + 122.358, + 122.834, + 123.321, + 123.809, + 124.296, + 124.784, + 125.283, + 125.771, + 126.241, + 126.711, + 127.199, + 127.687, + 128.157, + 128.627, + 129.091, + 129.614, + 130.113, + 130.612, + 131.088, + 131.564, + 132.069, + 132.574, + 133.085, + 133.596, + 134.089, + 134.583, + 135.065, + 135.546, + 136.034, + 136.522, + 137.009, + 137.497, + 138.025, + 138.553, + 139.041, + 139.552, + 140.045, + 140.539, + 141.026, + 141.514, + 142.013, + 142.489, + 142.988, + 143.464, + 143.964, + 144.463, + 144.956, + 145.45, + 145.937, + 146.425, + 146.901, + 147.389, + 147.876, + 148.364, + 148.857, + 149.351, + 149.827, + 150.326, + 150.802, + 151.324, + 151.806, + 152.288, + 152.764, + 153.24, + 153.734, + 154.227, + 154.726, + 155.225, + 155.713, + 156.201, + 156.688, + 157.176, + 157.669, + 158.163, + 158.65, + 159.15, + 159.626, + 160.102, + 160.589, + 161.077, + 161.599, + 162.087, + 162.569, + 163.051, + 163.538, + 164.026, + 164.513, + 165.024, + 165.506, + 165.988, + 166.475, + 166.963, + 167.427, + 167.903, + 168.368, + 168.809, + 169.268, + 169.726, + 170.185, + 170.643, + 171.119, + 171.595, + 172.06, + 172.524, + 173.0, + 173.476, + 173.97, + 174.463, + 174.951, + 175.438, + 175.914, + 176.39 + ], + "hitFraction": 0.584, + "trackerOffsetMs": { + "median": -35.0, + "p90abs": 46.5 + } + }, + "meter": { + "gridShift": "motif-anchored, 2 beat-count flips at [27.85, 61.17]", + "beatsPerBar": 4, + "phase": 1, + "downbeatChangeFraction": 0.75 + }, + "bars": [ + { + "n": 1, + "t": 5.02, + "dur": 1.92, + "beats": [ + 5.02, + 5.494, + 5.968, + 6.454, + 6.94 + ], + "bpm": 125.0, + "chord": "G#m", + "agree": 1.0, + "strum": ".X.XXX.." + }, + { + "n": 2, + "t": 6.94, + "dur": 1.946, + "beats": [ + 6.94, + 7.427, + 7.913, + 8.4, + 8.886 + ], + "bpm": 123.3, + "chord": "G#m", + "agree": 0.75, + "strum": "X......." + }, + { + "n": 3, + "t": 8.886, + "dur": 1.946, + "beats": [ + 8.886, + 9.373, + 9.859, + 10.346, + 10.832 + ], + "bpm": 123.3, + "chord": "G#m", + "agree": 1.0, + "strum": "........" + }, + { + "n": 4, + "t": 10.832, + "dur": 1.916, + "beats": [ + 10.832, + 11.302, + 11.773, + 12.307, + 12.748 + ], + "bpm": 125.3, + "chord": "B", + "agree": 0.5, + "strum": "X.XXX.XX" + }, + { + "n": 5, + "t": 12.748, + "dur": 1.916, + "beats": [ + 12.748, + 13.23, + 13.711, + 14.187, + 14.663 + ], + "bpm": 125.3, + "chord": "B", + "agree": 0.5, + "strum": "X.XXX.XX" + }, + { + "n": 6, + "t": 14.663, + "dur": 1.962, + "beats": [ + 14.663, + 15.145, + 15.627, + 16.173, + 16.625 + ], + "bpm": 122.3, + "chord": "G#m", + "agree": 1.0, + "strum": "X..XX.XX" + }, + { + "n": 7, + "t": 16.625, + "dur": 1.881, + "beats": [ + 16.625, + 17.107, + 17.589, + 18.048, + 18.506 + ], + "bpm": 127.6, + "chord": "G#m", + "agree": 1.0, + "strum": "X..XX.XX" + }, + { + "n": 8, + "t": 18.506, + "dur": 1.927, + "beats": [ + 18.506, + 19.006, + 19.505, + 19.969, + 20.434 + ], + "bpm": 124.5, + "chord": "B", + "agree": 1.0, + "strum": "X..XX.XX" + }, + { + "n": 9, + "t": 20.434, + "dur": 1.892, + "beats": [ + 20.434, + 20.91, + 21.386, + 21.856, + 22.326 + ], + "bpm": 126.8, + "chord": "B", + "agree": 0.75, + "strum": "X..XX.XX" + }, + { + "n": 10, + "t": 22.326, + "dur": 1.985, + "beats": [ + 22.326, + 22.883, + 23.336, + 23.859, + 24.311 + ], + "bpm": 120.9, + "chord": "G#m", + "agree": 0.5, + "strum": "X.X.X.XX" + }, + { + "n": 11, + "t": 24.311, + "dur": 2.078, + "beats": [ + 24.311, + 24.892, + 25.368, + 25.902, + 26.389 + ], + "bpm": 115.5, + "chord": "B", + "agree": 0.75, + "strum": "X.XXX.XX" + }, + { + "n": 12, + "t": 26.389, + "dur": 2.424, + "beats": [ + 26.389, + 26.877, + 27.365, + 27.848, + 28.331, + 28.814 + ], + "bpm": 123.8, + "chord": "B", + "agree": 1.0, + "strum": ".X..XX...X" + }, + { + "n": 13, + "t": 28.814, + "dur": 2.022, + "beats": [ + 28.814, + 29.297, + 29.78, + 30.308, + 30.836 + ], + "bpm": 118.7, + "chord": "B", + "agree": 0.5, + "strum": "...XX..X" + }, + { + "n": 14, + "t": 30.836, + "dur": 2.148, + "beats": [ + 30.836, + 31.388, + 31.939, + 32.461, + 32.984 + ], + "bpm": 111.7, + "chord": "G#m", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 15, + "t": 32.984, + "dur": 2.113, + "beats": [ + 32.984, + 33.501, + 34.017, + 34.557, + 35.097 + ], + "bpm": 113.6, + "chord": "G#m", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 16, + "t": 35.097, + "dur": 2.171, + "beats": [ + 35.097, + 35.643, + 36.188, + 36.728, + 37.268 + ], + "bpm": 110.5, + "chord": "B", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 17, + "t": 37.268, + "dur": 2.159, + "beats": [ + 37.268, + 37.808, + 38.348, + 38.882, + 39.427 + ], + "bpm": 111.1, + "chord": "B", + "agree": 0.75, + "strum": "X..XX.X." + }, + { + "n": 18, + "t": 39.427, + "dur": 2.148, + "beats": [ + 39.427, + 39.962, + 40.496, + 41.035, + 41.575 + ], + "bpm": 111.7, + "chord": "Emaj7", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 19, + "t": 41.575, + "dur": 2.101, + "beats": [ + 41.575, + 42.104, + 42.632, + 43.143, + 43.677 + ], + "bpm": 114.2, + "chord": "B", + "agree": 0.5, + "strum": "X..XXXXX" + }, + { + "n": 20, + "t": 43.677, + "dur": 2.159, + "beats": [ + 43.677, + 44.217, + 44.756, + 45.296, + 45.836 + ], + "bpm": 111.1, + "chord": "G#m", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 21, + "t": 45.836, + "dur": 2.113, + "beats": [ + 45.836, + 46.364, + 46.893, + 47.421, + 47.949 + ], + "bpm": 113.6, + "chord": "G#m", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 22, + "t": 47.949, + "dur": 2.113, + "beats": [ + 47.949, + 48.483, + 49.017, + 49.54, + 50.062 + ], + "bpm": 113.6, + "chord": "B", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 23, + "t": 50.062, + "dur": 2.136, + "beats": [ + 50.062, + 50.596, + 51.13, + 51.664, + 52.198 + ], + "bpm": 112.3, + "chord": "B", + "agree": 0.75, + "strum": "X..XX..X" + }, + { + "n": 24, + "t": 52.198, + "dur": 2.101, + "beats": [ + 52.198, + 52.733, + 53.267, + 53.783, + 54.3 + ], + "bpm": 114.2, + "chord": "B", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 25, + "t": 54.3, + "dur": 2.194, + "beats": [ + 54.3, + 54.851, + 55.403, + 55.948, + 56.494 + ], + "bpm": 109.4, + "chord": "B", + "agree": 1.0, + "strum": "X..XX.X." + }, + { + "n": 26, + "t": 56.494, + "dur": 2.125, + "beats": [ + 56.494, + 57.022, + 57.551, + 58.085, + 58.619 + ], + "bpm": 113.0, + "chord": "B", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 27, + "t": 58.619, + "dur": 2.554, + "beats": [ + 58.619, + 59.141, + 59.664, + 60.209, + 60.691, + 61.173 + ], + "bpm": 117.5, + "chord": "B", + "agree": 0.8, + "strum": "X..XX.X..." + }, + { + "n": 28, + "t": 61.173, + "dur": 2.109, + "beats": [ + 61.173, + 61.713, + 62.253, + 62.767, + 63.282 + ], + "bpm": 113.8, + "chord": "Emaj7", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 29, + "t": 63.282, + "dur": 1.989, + "beats": [ + 63.282, + 63.797, + 64.296, + 64.784, + 65.271 + ], + "bpm": 120.7, + "chord": "Emaj7", + "agree": 0.75, + "strum": "..X.X..X" + }, + { + "n": 30, + "t": 65.271, + "dur": 2.02, + "beats": [ + 65.271, + 65.776, + 66.281, + 66.786, + 67.291 + ], + "bpm": 118.8, + "chord": "G#m", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 31, + "t": 67.291, + "dur": 2.032, + "beats": [ + 67.291, + 67.796, + 68.301, + 68.824, + 69.323 + ], + "bpm": 118.1, + "chord": "G#m", + "agree": 1.0, + "strum": "X..XXXXX" + }, + { + "n": 32, + "t": 69.323, + "dur": 1.985, + "beats": [ + 69.323, + 69.817, + 70.31, + 70.809, + 71.308 + ], + "bpm": 120.9, + "chord": "B", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 33, + "t": 71.308, + "dur": 2.009, + "beats": [ + 71.308, + 71.808, + 72.307, + 72.812, + 73.317 + ], + "bpm": 119.5, + "chord": "B", + "agree": 1.0, + "strum": "X..XX..." + }, + { + "n": 34, + "t": 73.317, + "dur": 2.009, + "beats": [ + 73.317, + 73.816, + 74.315, + 74.82, + 75.326 + ], + "bpm": 119.5, + "chord": "B", + "agree": 0.5, + "strum": "X..XX..X" + }, + { + "n": 35, + "t": 75.326, + "dur": 2.009, + "beats": [ + 75.326, + 75.831, + 76.336, + 76.823, + 77.334 + ], + "bpm": 119.5, + "chord": "G#m", + "agree": 0.75, + "strum": "X..XX.XX" + }, + { + "n": 36, + "t": 77.334, + "dur": 1.997, + "beats": [ + 77.334, + 77.822, + 78.309, + 78.82, + 79.331 + ], + "bpm": 120.2, + "chord": "G#m", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 37, + "t": 79.331, + "dur": 2.009, + "beats": [ + 79.331, + 79.824, + 80.318, + 80.829, + 81.34 + ], + "bpm": 119.5, + "chord": "G#m", + "agree": 1.0, + "strum": "X..XXXXX" + }, + { + "n": 38, + "t": 81.34, + "dur": 1.974, + "beats": [ + 81.34, + 81.839, + 82.338, + 82.826, + 83.313 + ], + "bpm": 121.6, + "chord": "B", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 39, + "t": 83.313, + "dur": 2.043, + "beats": [ + 83.313, + 83.836, + 84.358, + 84.857, + 85.357 + ], + "bpm": 117.5, + "chord": "B", + "agree": 0.75, + "strum": "X..XX.XX" + }, + { + "n": 40, + "t": 85.357, + "dur": 1.997, + "beats": [ + 85.357, + 85.85, + 86.343, + 86.848, + 87.353 + ], + "bpm": 120.2, + "chord": "B", + "agree": 0.5, + "strum": "X..XX..X" + }, + { + "n": 41, + "t": 87.353, + "dur": 2.02, + "beats": [ + 87.353, + 87.853, + 88.352, + 88.863, + 89.374 + ], + "bpm": 118.8, + "chord": "Emaj7", + "agree": 0.75, + "strum": "X..XX..X" + }, + { + "n": 42, + "t": 89.374, + "dur": 1.985, + "beats": [ + 89.374, + 89.873, + 90.36, + 90.86, + 91.359 + ], + "bpm": 120.9, + "chord": "B", + "agree": 0.75, + "strum": "XXXXX.X." + }, + { + "n": 43, + "t": 91.359, + "dur": 1.985, + "beats": [ + 91.359, + 91.847, + 92.334, + 92.833, + 93.344 + ], + "bpm": 120.9, + "chord": "G#m", + "agree": 0.75, + "strum": "XXX.XXXX" + }, + { + "n": 44, + "t": 93.344, + "dur": 2.125, + "beats": [ + 93.344, + 93.884, + 94.424, + 94.946, + 95.469 + ], + "bpm": 113.0, + "chord": "G#m", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 45, + "t": 95.469, + "dur": 2.101, + "beats": [ + 95.469, + 95.991, + 96.514, + 97.042, + 97.57 + ], + "bpm": 114.2, + "chord": "G#m", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 46, + "t": 97.57, + "dur": 2.09, + "beats": [ + 97.57, + 98.087, + 98.604, + 99.132, + 99.66 + ], + "bpm": 114.8, + "chord": "B", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 47, + "t": 99.66, + "dur": 2.101, + "beats": [ + 99.66, + 100.2, + 100.74, + 101.251, + 101.761 + ], + "bpm": 114.2, + "chord": "B", + "agree": 0.75, + "strum": "X..XX..X" + }, + { + "n": 48, + "t": 101.761, + "dur": 2.078, + "beats": [ + 101.761, + 102.278, + 102.795, + 103.317, + 103.84 + ], + "bpm": 115.5, + "chord": "G#m", + "agree": 0.75, + "strum": "X..XX..X" + }, + { + "n": 49, + "t": 103.84, + "dur": 2.125, + "beats": [ + 103.84, + 104.374, + 104.908, + 105.43, + 105.964 + ], + "bpm": 113.0, + "chord": "Emaj7", + "agree": 0.75, + "strum": "X..XX.X." + }, + { + "n": 50, + "t": 105.964, + "dur": 2.055, + "beats": [ + 105.964, + 106.481, + 106.998, + 107.508, + 108.019 + ], + "bpm": 116.8, + "chord": "G#m", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 51, + "t": 108.019, + "dur": 2.043, + "beats": [ + 108.019, + 108.53, + 109.041, + 109.552, + 110.063 + ], + "bpm": 117.5, + "chord": "G#m", + "agree": 1.0, + "strum": "X..XX.XX" + }, + { + "n": 52, + "t": 110.063, + "dur": 1.985, + "beats": [ + 110.063, + 110.585, + 111.107, + 111.578, + 112.048 + ], + "bpm": 120.9, + "chord": "B", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 53, + "t": 112.048, + "dur": 1.985, + "beats": [ + 112.048, + 112.541, + 113.035, + 113.534, + 114.033 + ], + "bpm": 120.9, + "chord": "B", + "agree": 1.0, + "strum": "X..XXX.X" + }, + { + "n": 54, + "t": 114.033, + "dur": 1.95, + "beats": [ + 114.033, + 114.515, + 114.997, + 115.49, + 115.984 + ], + "bpm": 123.0, + "chord": "B", + "agree": 0.5, + "strum": "X..XX..." + }, + { + "n": 55, + "t": 115.984, + "dur": 1.997, + "beats": [ + 115.984, + 116.489, + 116.994, + 117.493, + 117.981 + ], + "bpm": 120.2, + "chord": "G#m", + "agree": 0.5, + "strum": "X..XXXXX" + }, + { + "n": 56, + "t": 117.981, + "dur": 1.939, + "beats": [ + 117.981, + 118.451, + 118.921, + 119.42, + 119.919 + ], + "bpm": 123.8, + "chord": "G#m", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 57, + "t": 119.919, + "dur": 1.962, + "beats": [ + 119.919, + 120.413, + 120.906, + 121.382, + 121.882 + ], + "bpm": 122.3, + "chord": "G#m", + "agree": 1.0, + "strum": "X..XXXXX" + }, + { + "n": 58, + "t": 121.882, + "dur": 1.927, + "beats": [ + 121.882, + 122.358, + 122.834, + 123.321, + 123.809 + ], + "bpm": 124.5, + "chord": "B", + "agree": 0.75, + "strum": "X..XX..X" + }, + { + "n": 59, + "t": 123.809, + "dur": 1.962, + "beats": [ + 123.809, + 124.296, + 124.784, + 125.283, + 125.771 + ], + "bpm": 122.3, + "chord": "B", + "agree": 0.5, + "strum": "X..XXXXX" + }, + { + "n": 60, + "t": 125.771, + "dur": 1.916, + "beats": [ + 125.771, + 126.241, + 126.711, + 127.199, + 127.687 + ], + "bpm": 125.3, + "chord": "B", + "agree": 0.5, + "strum": "X..XX..X" + }, + { + "n": 61, + "t": 127.687, + "dur": 1.927, + "beats": [ + 127.687, + 128.157, + 128.627, + 129.091, + 129.614 + ], + "bpm": 124.5, + "chord": "B", + "agree": 0.5, + "strum": "X..XXXXX" + }, + { + "n": 62, + "t": 129.614, + "dur": 1.95, + "beats": [ + 129.614, + 130.113, + 130.612, + 131.088, + 131.564 + ], + "bpm": 123.0, + "chord": "G#m", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 63, + "t": 131.564, + "dur": 2.032, + "beats": [ + 131.564, + 132.069, + 132.574, + 133.085, + 133.596 + ], + "bpm": 118.1, + "chord": "G#m", + "agree": 1.0, + "strum": "X..XX.XX" + }, + { + "n": 64, + "t": 133.596, + "dur": 1.95, + "beats": [ + 133.596, + 134.089, + 134.583, + 135.065, + 135.546 + ], + "bpm": 123.0, + "chord": "B", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 65, + "t": 135.546, + "dur": 1.95, + "beats": [ + 135.546, + 136.034, + 136.522, + 137.009, + 137.497 + ], + "bpm": 123.0, + "chord": "B", + "agree": 1.0, + "strum": "X.XXX..." + }, + { + "n": 66, + "t": 137.497, + "dur": 2.055, + "beats": [ + 137.497, + 138.025, + 138.553, + 139.041, + 139.552 + ], + "bpm": 116.8, + "chord": "B", + "agree": 0.5, + "strum": "X..XXXXX" + }, + { + "n": 67, + "t": 139.552, + "dur": 1.962, + "beats": [ + 139.552, + 140.045, + 140.539, + 141.026, + 141.514 + ], + "bpm": 122.3, + "chord": "G#m", + "agree": 0.75, + "strum": "X..XXX.X" + }, + { + "n": 68, + "t": 141.514, + "dur": 1.95, + "beats": [ + 141.514, + 142.013, + 142.489, + 142.988, + 143.464 + ], + "bpm": 123.0, + "chord": "G#m", + "agree": 1.0, + "strum": "XXXXXXX." + }, + { + "n": 69, + "t": 143.464, + "dur": 1.985, + "beats": [ + 143.464, + 143.964, + 144.463, + 144.956, + 145.45 + ], + "bpm": 120.9, + "chord": "G#m", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 70, + "t": 145.45, + "dur": 1.939, + "beats": [ + 145.45, + 145.937, + 146.425, + 146.901, + 147.389 + ], + "bpm": 123.8, + "chord": "G#m", + "agree": 1.0, + "strum": "X..XXXXX" + }, + { + "n": 71, + "t": 147.389, + "dur": 1.962, + "beats": [ + 147.389, + 147.876, + 148.364, + 148.857, + 149.351 + ], + "bpm": 122.3, + "chord": "B", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 72, + "t": 149.351, + "dur": 1.974, + "beats": [ + 149.351, + 149.827, + 150.326, + 150.802, + 151.324 + ], + "bpm": 121.6, + "chord": "B", + "agree": 0.5, + "strum": "X.XXX.XX" + }, + { + "n": 73, + "t": 151.324, + "dur": 1.916, + "beats": [ + 151.324, + 151.806, + 152.288, + 152.764, + 153.24 + ], + "bpm": 125.3, + "chord": "Emaj7", + "agree": 0.5, + "strum": "X..XX..X" + }, + { + "n": 74, + "t": 153.24, + "dur": 1.985, + "beats": [ + 153.24, + 153.734, + 154.227, + 154.726, + 155.225 + ], + "bpm": 120.9, + "chord": "B", + "agree": 0.5, + "strum": "X..XX.X." + }, + { + "n": 75, + "t": 155.225, + "dur": 1.95, + "beats": [ + 155.225, + 155.713, + 156.201, + 156.688, + 157.176 + ], + "bpm": 123.0, + "chord": "G#m", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 76, + "t": 157.176, + "dur": 1.974, + "beats": [ + 157.176, + 157.669, + 158.163, + 158.65, + 159.15 + ], + "bpm": 121.6, + "chord": "G#m", + "agree": 1.0, + "strum": "XX.XXXX." + }, + { + "n": 77, + "t": 159.15, + "dur": 1.927, + "beats": [ + 159.15, + 159.626, + 160.102, + 160.589, + 161.077 + ], + "bpm": 124.5, + "chord": "B", + "agree": 1.0, + "strum": "X..XXX.X" + }, + { + "n": 78, + "t": 161.077, + "dur": 1.974, + "beats": [ + 161.077, + 161.599, + 162.087, + 162.569, + 163.051 + ], + "bpm": 121.6, + "chord": "B", + "agree": 1.0, + "strum": "XXXXX..X" + }, + { + "n": 79, + "t": 163.051, + "dur": 1.974, + "beats": [ + 163.051, + 163.538, + 164.026, + 164.513, + 165.024 + ], + "bpm": 121.6, + "chord": "B", + "agree": 0.75, + "strum": "X..XX.XX" + }, + { + "n": 80, + "t": 165.024, + "dur": 1.939, + "beats": [ + 165.024, + 165.506, + 165.988, + 166.475, + 166.963 + ], + "bpm": 123.8, + "chord": "Emaj7", + "agree": 0.5, + "strum": "X..XX..X" + }, + { + "n": 81, + "t": 166.963, + "dur": 1.846, + "beats": [ + 166.963, + 167.427, + 167.903, + 168.368, + 168.809 + ], + "bpm": 130.0, + "chord": "G#m", + "agree": 1.0, + "strum": "XXXXXXXX" + }, + { + "n": 82, + "t": 168.809, + "dur": 1.834, + "beats": [ + 168.809, + 169.268, + 169.726, + 170.185, + 170.643 + ], + "bpm": 130.8, + "chord": "G#m", + "agree": 1.0, + "strum": "X..XX..X" + }, + { + "n": 83, + "t": 170.643, + "dur": 1.881, + "beats": [ + 170.643, + 171.119, + 171.595, + 172.06, + 172.524 + ], + "bpm": 127.6, + "chord": "G#m", + "agree": 1.0, + "strum": "X..XXXXX" + }, + { + "n": 84, + "t": 172.524, + "dur": 1.939, + "beats": [ + 172.524, + 173.0, + 173.476, + 173.97, + 174.463 + ], + "bpm": 123.8, + "chord": "B", + "agree": 1.0, + "strum": "X..XX..X" + } + ], + "strumList": [ + { + "t": 1.707, + "amp": 0.3, + "dir": "D" + }, + { + "t": 2.159, + "amp": 0.48, + "dir": "U" + }, + { + "t": 2.798, + "amp": 0.1, + "dir": "D" + }, + { + "t": 3.274, + "amp": 0.26, + "dir": "D" + }, + { + "t": 3.599, + "amp": 1.06, + "dir": "D" + }, + { + "t": 3.68, + "amp": 0.31, + "dir": "D" + }, + { + "t": 5.317, + "amp": 0.23, + "dir": "D" + }, + { + "t": 5.677, + "amp": 0.3, + "dir": "D" + }, + { + "t": 5.968, + "amp": 0.46, + "dir": "D" + }, + { + "t": 6.223, + "amp": 0.26, + "dir": "D" + }, + { + "t": 6.931, + "amp": 0.24, + "dir": "D" + }, + { + "t": 10.832, + "amp": 1.16, + "dir": "U" + }, + { + "t": 11.285, + "amp": 0.34, + "dir": "D" + }, + { + "t": 11.459, + "amp": 0.42, + "dir": "D" + }, + { + "t": 11.598, + "amp": 0.15, + "dir": "D" + }, + { + "t": 11.773, + "amp": 0.55, + "dir": "U" + }, + { + "t": 12.307, + "amp": 0.51, + "dir": "U" + }, + { + "t": 12.562, + "amp": 0.41, + "dir": "U" + }, + { + "t": 12.748, + "amp": 0.63, + "dir": "U" + }, + { + "t": 13.247, + "amp": 0.24, + "dir": "D" + }, + { + "t": 13.41, + "amp": 0.36, + "dir": "D" + }, + { + "t": 13.537, + "amp": 0.29, + "dir": "U" + }, + { + "t": 13.711, + "amp": 0.61, + "dir": "U" + }, + { + "t": 14.176, + "amp": 0.26, + "dir": "D" + }, + { + "t": 14.35, + "amp": 0.41, + "dir": "U" + }, + { + "t": 14.512, + "amp": 0.12, + "dir": "D" + }, + { + "t": 14.663, + "amp": 0.74, + "dir": "U" + }, + { + "t": 15.163, + "amp": 0.15, + "dir": "D" + }, + { + "t": 15.453, + "amp": 0.33, + "dir": "U" + }, + { + "t": 15.627, + "amp": 0.66, + "dir": "U" + }, + { + "t": 16.173, + "amp": 0.5, + "dir": "D" + }, + { + "t": 16.335, + "amp": 0.41, + "dir": "D" + }, + { + "t": 16.625, + "amp": 0.5, + "dir": "U" + }, + { + "t": 17.16, + "amp": 0.12, + "dir": "D" + }, + { + "t": 17.415, + "amp": 0.6, + "dir": "U" + }, + { + "t": 17.589, + "amp": 0.7, + "dir": "U" + }, + { + "t": 18.019, + "amp": 0.24, + "dir": "D" + }, + { + "t": 18.181, + "amp": 0.35, + "dir": "D" + }, + { + "t": 18.332, + "amp": 0.24, + "dir": "D" + }, + { + "t": 18.506, + "amp": 0.38, + "dir": "D" + }, + { + "t": 19.203, + "amp": 0.64, + "dir": "D" + }, + { + "t": 19.365, + "amp": 0.19, + "dir": "U" + }, + { + "t": 19.505, + "amp": 0.37, + "dir": "U" + }, + { + "t": 20.004, + "amp": 0.28, + "dir": "D" + }, + { + "t": 20.132, + "amp": 0.3, + "dir": "D" + }, + { + "t": 20.283, + "amp": 0.18, + "dir": "U" + }, + { + "t": 20.434, + "amp": 0.46, + "dir": "U" + }, + { + "t": 21.072, + "amp": 0.43, + "dir": "D" + }, + { + "t": 21.211, + "amp": 0.48, + "dir": "U" + }, + { + "t": 21.386, + "amp": 0.55, + "dir": "U" + }, + { + "t": 21.966, + "amp": 0.62, + "dir": "D" + }, + { + "t": 22.163, + "amp": 0.21, + "dir": "D" + }, + { + "t": 22.326, + "amp": 0.79, + "dir": "U" + }, + { + "t": 22.883, + "amp": 0.46, + "dir": "D" + }, + { + "t": 23.023, + "amp": 0.2, + "dir": "D" + }, + { + "t": 23.174, + "amp": 0.2, + "dir": "D" + }, + { + "t": 23.336, + "amp": 0.86, + "dir": "U" + }, + { + "t": 23.859, + "amp": 0.61, + "dir": "D" + }, + { + "t": 24.009, + "amp": 0.32, + "dir": "D" + }, + { + "t": 24.311, + "amp": 0.73, + "dir": "U" + }, + { + "t": 24.892, + "amp": 0.46, + "dir": "D" + }, + { + "t": 25.031, + "amp": 0.43, + "dir": "D" + }, + { + "t": 25.217, + "amp": 0.14, + "dir": "D" + }, + { + "t": 25.368, + "amp": 0.59, + "dir": "U" + }, + { + "t": 25.902, + "amp": 0.43, + "dir": "D" + }, + { + "t": 26.204, + "amp": 0.59, + "dir": "D" + }, + { + "t": 26.529, + "amp": 0.62, + "dir": "U" + }, + { + "t": 27.365, + "amp": 0.45, + "dir": "D" + }, + { + "t": 27.655, + "amp": 0.84, + "dir": "U" + }, + { + "t": 28.456, + "amp": 0.49, + "dir": "D" + }, + { + "t": 28.735, + "amp": 0.35, + "dir": "D" + }, + { + "t": 29.513, + "amp": 0.59, + "dir": "D" + }, + { + "t": 29.78, + "amp": 0.78, + "dir": "D" + }, + { + "t": 30.557, + "amp": 0.38, + "dir": "D" + }, + { + "t": 30.836, + "amp": 0.79, + "dir": "U" + }, + { + "t": 31.684, + "amp": 0.48, + "dir": "D" + }, + { + "t": 31.939, + "amp": 0.92, + "dir": "U" + }, + { + "t": 32.74, + "amp": 0.24, + "dir": "D" + }, + { + "t": 32.984, + "amp": 0.91, + "dir": "U" + }, + { + "t": 33.75, + "amp": 0.52, + "dir": "D" + }, + { + "t": 34.017, + "amp": 0.66, + "dir": "U" + }, + { + "t": 34.795, + "amp": 0.38, + "dir": "D" + }, + { + "t": 35.097, + "amp": 0.83, + "dir": "U" + }, + { + "t": 35.944, + "amp": 0.45, + "dir": "D" + }, + { + "t": 36.188, + "amp": 0.74, + "dir": "U" + }, + { + "t": 36.989, + "amp": 0.49, + "dir": "D" + }, + { + "t": 37.268, + "amp": 0.53, + "dir": "D" + }, + { + "t": 38.081, + "amp": 0.54, + "dir": "U" + }, + { + "t": 38.348, + "amp": 0.66, + "dir": "U" + }, + { + "t": 38.882, + "amp": 0.62, + "dir": "U" + }, + { + "t": 39.219, + "amp": 0.1, + "dir": "D" + }, + { + "t": 39.427, + "amp": 0.81, + "dir": "U" + }, + { + "t": 40.252, + "amp": 0.53, + "dir": "D" + }, + { + "t": 40.496, + "amp": 0.7, + "dir": "U" + }, + { + "t": 41.355, + "amp": 0.44, + "dir": "D" + }, + { + "t": 41.575, + "amp": 0.92, + "dir": "U" + }, + { + "t": 42.4, + "amp": 0.46, + "dir": "D" + }, + { + "t": 42.632, + "amp": 0.74, + "dir": "D" + }, + { + "t": 42.887, + "amp": 0.46, + "dir": "D" + }, + { + "t": 43.143, + "amp": 0.71, + "dir": "U" + }, + { + "t": 43.41, + "amp": 0.49, + "dir": "D" + }, + { + "t": 43.677, + "amp": 0.65, + "dir": "D" + }, + { + "t": 44.501, + "amp": 0.25, + "dir": "D" + }, + { + "t": 44.756, + "amp": 0.94, + "dir": "D" + }, + { + "t": 45.569, + "amp": 0.73, + "dir": "D" + }, + { + "t": 45.836, + "amp": 0.67, + "dir": "U" + }, + { + "t": 46.626, + "amp": 0.48, + "dir": "D" + }, + { + "t": 46.893, + "amp": 0.72, + "dir": "U" + }, + { + "t": 47.647, + "amp": 0.61, + "dir": "D" + }, + { + "t": 47.949, + "amp": 0.79, + "dir": "U" + }, + { + "t": 48.762, + "amp": 0.48, + "dir": "D" + }, + { + "t": 49.017, + "amp": 0.93, + "dir": "U" + }, + { + "t": 49.795, + "amp": 0.47, + "dir": "D" + }, + { + "t": 50.062, + "amp": 0.82, + "dir": "U" + }, + { + "t": 50.887, + "amp": 0.49, + "dir": "D" + }, + { + "t": 51.13, + "amp": 0.59, + "dir": "U" + }, + { + "t": 51.897, + "amp": 0.64, + "dir": "D" + }, + { + "t": 52.198, + "amp": 0.75, + "dir": "U" + }, + { + "t": 53.0, + "amp": 0.64, + "dir": "D" + }, + { + "t": 53.267, + "amp": 0.9, + "dir": "U" + }, + { + "t": 54.021, + "amp": 0.61, + "dir": "D" + }, + { + "t": 54.3, + "amp": 0.8, + "dir": "U" + }, + { + "t": 55.089, + "amp": 0.55, + "dir": "D" + }, + { + "t": 55.403, + "amp": 0.82, + "dir": "U" + }, + { + "t": 55.948, + "amp": 0.74, + "dir": "D" + }, + { + "t": 56.494, + "amp": 1.16, + "dir": "U" + }, + { + "t": 57.295, + "amp": 0.36, + "dir": "D" + }, + { + "t": 57.551, + "amp": 0.69, + "dir": "U" + }, + { + "t": 58.363, + "amp": 0.68, + "dir": "U" + }, + { + "t": 58.619, + "amp": 1.08, + "dir": "U" + }, + { + "t": 59.42, + "amp": 0.59, + "dir": "D" + }, + { + "t": 59.664, + "amp": 0.82, + "dir": "U" + }, + { + "t": 60.209, + "amp": 0.7, + "dir": "D" + }, + { + "t": 61.173, + "amp": 0.97, + "dir": "U" + }, + { + "t": 61.916, + "amp": 0.56, + "dir": "D" + }, + { + "t": 62.253, + "amp": 0.76, + "dir": "U" + }, + { + "t": 62.926, + "amp": 0.42, + "dir": "D" + }, + { + "t": 63.797, + "amp": 1.08, + "dir": "U" + }, + { + "t": 64.296, + "amp": 0.77, + "dir": "D" + }, + { + "t": 65.039, + "amp": 0.58, + "dir": "D" + }, + { + "t": 65.271, + "amp": 0.94, + "dir": "U" + }, + { + "t": 66.119, + "amp": 0.32, + "dir": "D" + }, + { + "t": 66.281, + "amp": 0.72, + "dir": "U" + }, + { + "t": 67.071, + "amp": 0.66, + "dir": "U" + }, + { + "t": 67.315, + "amp": 0.31, + "dir": "D" + }, + { + "t": 67.942, + "amp": 0.49, + "dir": "D" + }, + { + "t": 68.301, + "amp": 0.95, + "dir": "U" + }, + { + "t": 68.569, + "amp": 0.27, + "dir": "D" + }, + { + "t": 68.824, + "amp": 0.54, + "dir": "D" + }, + { + "t": 69.033, + "amp": 0.33, + "dir": "D" + }, + { + "t": 69.323, + "amp": 0.78, + "dir": "U" + }, + { + "t": 70.101, + "amp": 0.66, + "dir": "D" + }, + { + "t": 70.31, + "amp": 0.8, + "dir": "U" + }, + { + "t": 71.088, + "amp": 0.65, + "dir": "D" + }, + { + "t": 71.308, + "amp": 0.88, + "dir": "U" + }, + { + "t": 72.063, + "amp": 0.6, + "dir": "U" + }, + { + "t": 72.307, + "amp": 0.9, + "dir": "U" + }, + { + "t": 73.317, + "amp": 1.38, + "dir": "U" + }, + { + "t": 74.083, + "amp": 0.56, + "dir": "D" + }, + { + "t": 74.315, + "amp": 0.63, + "dir": "U" + }, + { + "t": 74.919, + "amp": 0.12, + "dir": "D" + }, + { + "t": 75.128, + "amp": 0.28, + "dir": "D" + }, + { + "t": 75.326, + "amp": 0.7, + "dir": "U" + }, + { + "t": 76.103, + "amp": 0.49, + "dir": "D" + }, + { + "t": 76.336, + "amp": 0.7, + "dir": "U" + }, + { + "t": 76.614, + "amp": 0.2, + "dir": "D" + }, + { + "t": 76.823, + "amp": 1.0, + "dir": "U" + }, + { + "t": 77.113, + "amp": 0.3, + "dir": "D" + }, + { + "t": 77.334, + "amp": 1.24, + "dir": "U" + }, + { + "t": 78.077, + "amp": 0.59, + "dir": "D" + }, + { + "t": 78.309, + "amp": 0.84, + "dir": "U" + }, + { + "t": 79.087, + "amp": 0.64, + "dir": "D" + }, + { + "t": 79.331, + "amp": 1.03, + "dir": "U" + }, + { + "t": 80.086, + "amp": 0.53, + "dir": "D" + }, + { + "t": 80.318, + "amp": 0.6, + "dir": "D" + }, + { + "t": 80.585, + "amp": 0.22, + "dir": "D" + }, + { + "t": 80.829, + "amp": 0.81, + "dir": "U" + }, + { + "t": 81.096, + "amp": 0.32, + "dir": "D" + }, + { + "t": 81.34, + "amp": 0.95, + "dir": "U" + }, + { + "t": 82.083, + "amp": 0.67, + "dir": "D" + }, + { + "t": 82.338, + "amp": 0.91, + "dir": "U" + }, + { + "t": 83.069, + "amp": 0.54, + "dir": "U" + }, + { + "t": 83.313, + "amp": 0.89, + "dir": "U" + }, + { + "t": 84.091, + "amp": 0.5, + "dir": "D" + }, + { + "t": 84.358, + "amp": 0.69, + "dir": "U" + }, + { + "t": 84.857, + "amp": 0.66, + "dir": "D" + }, + { + "t": 85.136, + "amp": 0.35, + "dir": "D" + }, + { + "t": 85.357, + "amp": 0.77, + "dir": "U" + }, + { + "t": 86.088, + "amp": 0.57, + "dir": "D" + }, + { + "t": 86.343, + "amp": 0.69, + "dir": "U" + }, + { + "t": 87.156, + "amp": 0.48, + "dir": "D" + }, + { + "t": 87.353, + "amp": 0.98, + "dir": "U" + }, + { + "t": 88.108, + "amp": 0.67, + "dir": "D" + }, + { + "t": 88.352, + "amp": 0.76, + "dir": "U" + }, + { + "t": 89.118, + "amp": 0.52, + "dir": "D" + }, + { + "t": 89.374, + "amp": 0.76, + "dir": "U" + }, + { + "t": 89.664, + "amp": 0.27, + "dir": "D" + }, + { + "t": 89.873, + "amp": 0.5, + "dir": "D" + }, + { + "t": 90.105, + "amp": 0.36, + "dir": "D" + }, + { + "t": 90.36, + "amp": 0.61, + "dir": "U" + }, + { + "t": 90.639, + "amp": 0.11, + "dir": "D" + }, + { + "t": 90.86, + "amp": 0.39, + "dir": "D" + }, + { + "t": 91.359, + "amp": 0.42, + "dir": "D" + }, + { + "t": 91.638, + "amp": 0.26, + "dir": "D" + }, + { + "t": 91.847, + "amp": 0.71, + "dir": "D" + }, + { + "t": 92.102, + "amp": 0.16, + "dir": "D" + }, + { + "t": 92.334, + "amp": 0.47, + "dir": "D" + }, + { + "t": 92.578, + "amp": 0.3, + "dir": "D" + }, + { + "t": 92.833, + "amp": 0.78, + "dir": "U" + }, + { + "t": 93.147, + "amp": 0.39, + "dir": "D" + }, + { + "t": 93.344, + "amp": 0.98, + "dir": "U" + }, + { + "t": 94.157, + "amp": 0.45, + "dir": "D" + }, + { + "t": 94.424, + "amp": 0.75, + "dir": "D" + }, + { + "t": 95.237, + "amp": 0.53, + "dir": "D" + }, + { + "t": 95.469, + "amp": 0.7, + "dir": "D" + }, + { + "t": 96.247, + "amp": 0.49, + "dir": "D" + }, + { + "t": 96.514, + "amp": 0.56, + "dir": "D" + }, + { + "t": 97.315, + "amp": 0.6, + "dir": "D" + }, + { + "t": 97.57, + "amp": 0.79, + "dir": "U" + }, + { + "t": 98.371, + "amp": 0.46, + "dir": "D" + }, + { + "t": 98.604, + "amp": 0.7, + "dir": "U" + }, + { + "t": 99.393, + "amp": 0.61, + "dir": "D" + }, + { + "t": 99.66, + "amp": 0.69, + "dir": "U" + }, + { + "t": 100.531, + "amp": 0.35, + "dir": "D" + }, + { + "t": 100.74, + "amp": 0.78, + "dir": "U" + }, + { + "t": 101.483, + "amp": 0.57, + "dir": "U" + }, + { + "t": 101.761, + "amp": 1.08, + "dir": "U" + }, + { + "t": 102.539, + "amp": 0.59, + "dir": "D" + }, + { + "t": 102.795, + "amp": 0.9, + "dir": "U" + }, + { + "t": 103.596, + "amp": 0.63, + "dir": "D" + }, + { + "t": 103.84, + "amp": 0.96, + "dir": "U" + }, + { + "t": 104.629, + "amp": 0.56, + "dir": "D" + }, + { + "t": 104.908, + "amp": 0.81, + "dir": "U" + }, + { + "t": 105.43, + "amp": 0.72, + "dir": "U" + }, + { + "t": 105.964, + "amp": 0.92, + "dir": "U" + }, + { + "t": 106.754, + "amp": 0.23, + "dir": "D" + }, + { + "t": 106.998, + "amp": 0.82, + "dir": "U" + }, + { + "t": 107.764, + "amp": 0.62, + "dir": "U" + }, + { + "t": 108.019, + "amp": 0.92, + "dir": "U" + }, + { + "t": 108.797, + "amp": 0.48, + "dir": "D" + }, + { + "t": 109.041, + "amp": 0.82, + "dir": "U" + }, + { + "t": 109.552, + "amp": 1.12, + "dir": "U" + }, + { + "t": 109.842, + "amp": 0.51, + "dir": "U" + }, + { + "t": 110.063, + "amp": 0.7, + "dir": "U" + }, + { + "t": 110.84, + "amp": 0.44, + "dir": "D" + }, + { + "t": 111.107, + "amp": 0.8, + "dir": "U" + }, + { + "t": 111.827, + "amp": 0.63, + "dir": "D" + }, + { + "t": 112.048, + "amp": 0.54, + "dir": "U" + }, + { + "t": 112.779, + "amp": 0.7, + "dir": "U" + }, + { + "t": 113.035, + "amp": 0.67, + "dir": "U" + }, + { + "t": 113.302, + "amp": 0.26, + "dir": "D" + }, + { + "t": 113.778, + "amp": 0.58, + "dir": "D" + }, + { + "t": 114.033, + "amp": 0.7, + "dir": "U" + }, + { + "t": 114.765, + "amp": 0.55, + "dir": "D" + }, + { + "t": 114.997, + "amp": 0.77, + "dir": "U" + }, + { + "t": 115.984, + "amp": 0.92, + "dir": "U" + }, + { + "t": 116.738, + "amp": 0.64, + "dir": "D" + }, + { + "t": 116.994, + "amp": 0.91, + "dir": "U" + }, + { + "t": 117.261, + "amp": 0.27, + "dir": "D" + }, + { + "t": 117.493, + "amp": 0.99, + "dir": "U" + }, + { + "t": 117.737, + "amp": 0.45, + "dir": "D" + }, + { + "t": 117.981, + "amp": 0.8, + "dir": "U" + }, + { + "t": 118.7, + "amp": 0.5, + "dir": "D" + }, + { + "t": 118.921, + "amp": 0.8, + "dir": "U" + }, + { + "t": 119.71, + "amp": 0.51, + "dir": "D" + }, + { + "t": 119.919, + "amp": 1.25, + "dir": "U" + }, + { + "t": 120.662, + "amp": 0.63, + "dir": "D" + }, + { + "t": 120.906, + "amp": 0.99, + "dir": "U" + }, + { + "t": 121.162, + "amp": 0.24, + "dir": "D" + }, + { + "t": 121.382, + "amp": 0.45, + "dir": "D" + }, + { + "t": 121.638, + "amp": 0.4, + "dir": "D" + }, + { + "t": 121.882, + "amp": 0.87, + "dir": "U" + }, + { + "t": 122.59, + "amp": 0.48, + "dir": "U" + }, + { + "t": 122.834, + "amp": 0.64, + "dir": "U" + }, + { + "t": 123.588, + "amp": 0.62, + "dir": "D" + }, + { + "t": 123.809, + "amp": 0.58, + "dir": "U" + }, + { + "t": 124.401, + "amp": 0.11, + "dir": "D" + }, + { + "t": 124.552, + "amp": 0.47, + "dir": "D" + }, + { + "t": 124.784, + "amp": 1.01, + "dir": "U" + }, + { + "t": 125.039, + "amp": 0.3, + "dir": "D" + }, + { + "t": 125.283, + "amp": 0.58, + "dir": "U" + }, + { + "t": 125.539, + "amp": 0.37, + "dir": "U" + }, + { + "t": 125.771, + "amp": 0.88, + "dir": "U" + }, + { + "t": 126.502, + "amp": 0.49, + "dir": "D" + }, + { + "t": 126.711, + "amp": 0.9, + "dir": "U" + }, + { + "t": 127.443, + "amp": 0.81, + "dir": "D" + }, + { + "t": 127.687, + "amp": 0.78, + "dir": "U" + }, + { + "t": 128.43, + "amp": 0.45, + "dir": "D" + }, + { + "t": 128.627, + "amp": 0.92, + "dir": "U" + }, + { + "t": 128.871, + "amp": 0.62, + "dir": "U" + }, + { + "t": 129.091, + "amp": 0.64, + "dir": "U" + }, + { + "t": 129.382, + "amp": 0.43, + "dir": "D" + }, + { + "t": 129.614, + "amp": 1.0, + "dir": "U" + }, + { + "t": 130.38, + "amp": 0.49, + "dir": "D" + }, + { + "t": 130.612, + "amp": 1.04, + "dir": "U" + }, + { + "t": 131.367, + "amp": 0.71, + "dir": "D" + }, + { + "t": 131.564, + "amp": 0.93, + "dir": "U" + }, + { + "t": 132.342, + "amp": 0.61, + "dir": "D" + }, + { + "t": 132.574, + "amp": 1.0, + "dir": "U" + }, + { + "t": 132.865, + "amp": 0.16, + "dir": "D" + }, + { + "t": 133.085, + "amp": 0.55, + "dir": "D" + }, + { + "t": 133.375, + "amp": 0.3, + "dir": "D" + }, + { + "t": 133.596, + "amp": 1.14, + "dir": "U" + }, + { + "t": 134.351, + "amp": 0.61, + "dir": "U" + }, + { + "t": 134.583, + "amp": 0.43, + "dir": "U" + }, + { + "t": 134.885, + "amp": 0.18, + "dir": "D" + }, + { + "t": 135.349, + "amp": 0.37, + "dir": "D" + }, + { + "t": 135.546, + "amp": 0.82, + "dir": "U" + }, + { + "t": 136.034, + "amp": 0.65, + "dir": "D" + }, + { + "t": 136.324, + "amp": 0.26, + "dir": "D" + }, + { + "t": 136.522, + "amp": 0.82, + "dir": "U" + }, + { + "t": 137.497, + "amp": 0.81, + "dir": "U" + }, + { + "t": 137.822, + "amp": 0.14, + "dir": "D" + }, + { + "t": 138.066, + "amp": 0.18, + "dir": "D" + }, + { + "t": 138.321, + "amp": 0.54, + "dir": "D" + }, + { + "t": 138.553, + "amp": 0.98, + "dir": "U" + }, + { + "t": 138.82, + "amp": 0.3, + "dir": "D" + }, + { + "t": 139.041, + "amp": 0.71, + "dir": "U" + }, + { + "t": 139.297, + "amp": 0.24, + "dir": "D" + }, + { + "t": 139.552, + "amp": 1.02, + "dir": "U" + }, + { + "t": 139.831, + "amp": 0.17, + "dir": "D" + }, + { + "t": 140.307, + "amp": 0.66, + "dir": "U" + }, + { + "t": 140.539, + "amp": 0.79, + "dir": "U" + }, + { + "t": 140.817, + "amp": 0.28, + "dir": "D" + }, + { + "t": 141.27, + "amp": 0.74, + "dir": "U" + }, + { + "t": 141.514, + "amp": 0.83, + "dir": "U" + }, + { + "t": 141.769, + "amp": 0.24, + "dir": "D" + }, + { + "t": 142.013, + "amp": 0.8, + "dir": "U" + }, + { + "t": 142.245, + "amp": 0.38, + "dir": "D" + }, + { + "t": 142.489, + "amp": 0.81, + "dir": "U" + }, + { + "t": 142.745, + "amp": 0.51, + "dir": "U" + }, + { + "t": 142.988, + "amp": 0.77, + "dir": "U" + }, + { + "t": 143.232, + "amp": 0.18, + "dir": "D" + }, + { + "t": 143.464, + "amp": 0.77, + "dir": "U" + }, + { + "t": 144.219, + "amp": 0.56, + "dir": "D" + }, + { + "t": 144.463, + "amp": 0.69, + "dir": "U" + }, + { + "t": 145.194, + "amp": 0.59, + "dir": "U" + }, + { + "t": 145.45, + "amp": 0.9, + "dir": "U" + }, + { + "t": 146.181, + "amp": 0.85, + "dir": "D" + }, + { + "t": 146.425, + "amp": 0.93, + "dir": "U" + }, + { + "t": 146.669, + "amp": 0.4, + "dir": "U" + }, + { + "t": 146.901, + "amp": 0.64, + "dir": "D" + }, + { + "t": 147.133, + "amp": 0.22, + "dir": "D" + }, + { + "t": 147.389, + "amp": 0.66, + "dir": "U" + }, + { + "t": 148.097, + "amp": 0.59, + "dir": "U" + }, + { + "t": 148.364, + "amp": 0.64, + "dir": "U" + }, + { + "t": 149.084, + "amp": 0.72, + "dir": "U" + }, + { + "t": 149.351, + "amp": 0.89, + "dir": "U" + }, + { + "t": 149.827, + "amp": 1.02, + "dir": "U" + }, + { + "t": 150.059, + "amp": 0.35, + "dir": "U" + }, + { + "t": 150.326, + "amp": 0.78, + "dir": "U" + }, + { + "t": 150.558, + "amp": 0.19, + "dir": "U" + }, + { + "t": 150.802, + "amp": 0.72, + "dir": "U" + }, + { + "t": 151.104, + "amp": 0.29, + "dir": "D" + }, + { + "t": 151.324, + "amp": 0.99, + "dir": "U" + }, + { + "t": 152.067, + "amp": 0.53, + "dir": "D" + }, + { + "t": 152.288, + "amp": 0.87, + "dir": "U" + }, + { + "t": 152.996, + "amp": 0.38, + "dir": "D" + }, + { + "t": 153.24, + "amp": 0.82, + "dir": "U" + }, + { + "t": 154.006, + "amp": 0.68, + "dir": "D" + }, + { + "t": 154.227, + "amp": 1.0, + "dir": "U" + }, + { + "t": 154.529, + "amp": 0.12, + "dir": "D" + }, + { + "t": 154.726, + "amp": 0.73, + "dir": "U" + }, + { + "t": 155.225, + "amp": 1.23, + "dir": "U" + }, + { + "t": 155.98, + "amp": 0.73, + "dir": "D" + }, + { + "t": 156.189, + "amp": 0.27, + "dir": "D" + }, + { + "t": 156.932, + "amp": 0.4, + "dir": "D" + }, + { + "t": 157.176, + "amp": 0.93, + "dir": "U" + }, + { + "t": 157.455, + "amp": 0.23, + "dir": "D" + }, + { + "t": 157.942, + "amp": 0.34, + "dir": "D" + }, + { + "t": 158.163, + "amp": 0.87, + "dir": "D" + }, + { + "t": 158.441, + "amp": 0.25, + "dir": "D" + }, + { + "t": 158.65, + "amp": 0.36, + "dir": "D" + }, + { + "t": 158.952, + "amp": 0.15, + "dir": "D" + }, + { + "t": 159.15, + "amp": 1.18, + "dir": "U" + }, + { + "t": 159.893, + "amp": 0.51, + "dir": "D" + }, + { + "t": 160.102, + "amp": 0.73, + "dir": "U" + }, + { + "t": 160.38, + "amp": 0.3, + "dir": "D" + }, + { + "t": 160.845, + "amp": 0.51, + "dir": "U" + }, + { + "t": 161.077, + "amp": 0.81, + "dir": "U" + }, + { + "t": 161.367, + "amp": 0.29, + "dir": "D" + }, + { + "t": 161.599, + "amp": 0.45, + "dir": "D" + }, + { + "t": 161.843, + "amp": 0.44, + "dir": "U" + }, + { + "t": 162.087, + "amp": 0.61, + "dir": "U" + }, + { + "t": 162.807, + "amp": 0.48, + "dir": "U" + }, + { + "t": 163.051, + "amp": 0.89, + "dir": "U" + }, + { + "t": 163.782, + "amp": 0.36, + "dir": "D" + }, + { + "t": 164.026, + "amp": 0.91, + "dir": "U" + }, + { + "t": 164.513, + "amp": 0.76, + "dir": "U" + }, + { + "t": 164.78, + "amp": 0.22, + "dir": "D" + }, + { + "t": 165.024, + "amp": 1.12, + "dir": "U" + }, + { + "t": 165.732, + "amp": 0.89, + "dir": "U" + }, + { + "t": 165.988, + "amp": 0.95, + "dir": "U" + }, + { + "t": 166.742, + "amp": 0.71, + "dir": "D" + }, + { + "t": 166.963, + "amp": 0.73, + "dir": "U" + }, + { + "t": 167.207, + "amp": 0.42, + "dir": "U" + }, + { + "t": 167.427, + "amp": 0.75, + "dir": "U" + }, + { + "t": 167.671, + "amp": 0.36, + "dir": "D" + }, + { + "t": 167.903, + "amp": 0.91, + "dir": "U" + }, + { + "t": 168.136, + "amp": 0.33, + "dir": "U" + }, + { + "t": 168.368, + "amp": 0.78, + "dir": "U" + }, + { + "t": 168.588, + "amp": 0.36, + "dir": "D" + }, + { + "t": 168.809, + "amp": 0.91, + "dir": "U" + }, + { + "t": 169.494, + "amp": 0.54, + "dir": "D" + }, + { + "t": 169.726, + "amp": 0.78, + "dir": "U" + }, + { + "t": 170.411, + "amp": 0.64, + "dir": "U" + }, + { + "t": 170.643, + "amp": 0.8, + "dir": "U" + }, + { + "t": 171.363, + "amp": 0.57, + "dir": "D" + }, + { + "t": 171.595, + "amp": 0.73, + "dir": "U" + }, + { + "t": 171.839, + "amp": 0.51, + "dir": "D" + }, + { + "t": 172.071, + "amp": 0.23, + "dir": "D" + }, + { + "t": 172.28, + "amp": 0.54, + "dir": "D" + }, + { + "t": 172.524, + "amp": 0.92, + "dir": "U" + }, + { + "t": 173.232, + "amp": 0.56, + "dir": "U" + }, + { + "t": 173.476, + "amp": 0.71, + "dir": "U" + }, + { + "t": 174.219, + "amp": 0.62, + "dir": "U" + }, + { + "t": 174.463, + "amp": 0.8, + "dir": "U" + }, + { + "t": 175.206, + "amp": 0.64, + "dir": "U" + }, + { + "t": 175.438, + "amp": 0.74, + "dir": "U" + }, + { + "t": 175.659, + "amp": 0.28, + "dir": "U" + }, + { + "t": 175.914, + "amp": 0.49, + "dir": "U" + }, + { + "t": 176.123, + "amp": 0.33, + "dir": "U" + }, + { + "t": 176.39, + "amp": 0.95, + "dir": "U" + }, + { + "t": 178.77, + "amp": 0.3, + "dir": "D" + }, + { + "t": 179.049, + "amp": 0.25, + "dir": "D" + } + ] +} \ No newline at end of file diff --git a/pop/sailor-song/measures.reg.json b/pop/sailor-song/measures.reg.json new file mode 100644 index 0000000000..a70c4bfe6b --- /dev/null +++ b/pop/sailor-song/measures.reg.json @@ -0,0 +1,3858 @@ +{ + "strums": { + "count": 425, + "perBeat": 1.23, + "phaseHist8": [ + 4, + 4, + 10, + 14, + 132, + 46, + 1, + 2 + ] + }, + "beats": { + "snapped": [ + 3.5989999999999998, + 4.073, + 4.546, + 5.02, + 5.494, + 5.968, + 6.454, + 6.94, + 7.427, + 7.913, + 8.4, + 8.886, + 9.373, + 9.859, + 10.346, + 10.832, + 11.302, + 11.773, + 12.307, + 12.748, + 13.23, + 13.711, + 14.187, + 14.663, + 15.145, + 15.627, + 16.173, + 16.625, + 17.107, + 17.589, + 18.048, + 18.506, + 19.006, + 19.505, + 19.969, + 20.434, + 20.91, + 21.386, + 21.856, + 22.326, + 22.883, + 23.336, + 23.859, + 24.311, + 24.892, + 25.368, + 25.902, + 26.389, + 26.877, + 27.365, + 27.848, + 28.331, + 28.814, + 29.297, + 29.78, + 30.308, + 30.836, + 31.388, + 31.939, + 32.461, + 32.984, + 33.501, + 34.017, + 34.557, + 35.097, + 35.643, + 36.188, + 36.728, + 37.268, + 37.808, + 38.348, + 38.882, + 39.427, + 39.962, + 40.496, + 41.035, + 41.575, + 42.104, + 42.632, + 43.143, + 43.677, + 44.217, + 44.756, + 45.296, + 45.836, + 46.363126965355846, + 46.8905137054648, + 47.41821310483894, + 47.94715455486204, + 48.46752275144404, + 48.987675609169045, + 49.50697437994057, + 50.02542507870751, + 50.53652233479391, + 51.04812940154079, + 51.56008063076585, + 52.07241961955978, + 52.58129418498595, + 53.09008787847, + 53.59888157195405, + 54.10785525989205, + 54.616828947830044, + 55.125622641314095, + 55.634589060954305, + 56.14263359231784, + 56.64975956898965, + 57.15696410426977, + 57.6642631186425, + 58.17152788872035, + 58.678758551701094, + 59.18541386204914, + 59.691199036124, + 60.19601853055413, + 60.69984212125789, + 61.1793063988632, + 61.65838829939644, + 62.1370132883883, + 62.61518223654269, + 63.09307812139763, + 63.57068577196881, + 64.04796741163675, + 64.52554757855512, + 65.003340831606, + 65.48038845947893, + 65.95679533391554, + 66.43295638614387, + 66.908872836237, + 67.38452813220306, + 67.8602832663776, + 68.33632788850873, + 68.81257995322045, + 69.28873197107286, + 69.76488398892528, + 70.24105244769295, + 70.71722090646062, + 71.1933893652283, + 71.66925052956792, + 72.14492336850425, + 72.62040689559939, + 73.0957685620713, + 73.57142958459357, + 74.04728798420392, + 74.52304407159352, + 74.99869830180342, + 75.47454990346328, + 75.95049907881076, + 76.42634833450181, + 76.90219759019286, + 77.37785739362131, + 77.85321055857922, + 78.32883283793144, + 78.80472558781302, + 79.28051844172595, + 79.75653534634141, + 80.23225244693313, + 80.70805671249583, + 81.1841681989808, + 81.66018204522325, + 82.13601519997621, + 82.61195070010314, + 83.08816902465163, + 83.56438734920013, + 84.04040799802951, + 84.51622140578252, + 84.9920516278738, + 85.4680714380324, + 85.94410891585052, + 86.41955363944031, + 86.8946288065843, + 87.36990086480995, + 87.84579380551482, + 88.32242876340804, + 88.79953856035861, + 89.2769359908199, + 89.75460591509497, + 90.23264798402873, + 90.7111565845216, + 91.18993265676293, + 91.6688052126767, + 92.14797442374717, + 92.62782295682089, + 93.1081244227451, + 93.589077168313, + 94.07087718150237, + 94.55309236922395, + 95.03549386564183, + 95.51799331261576, + 96.00013763639116, + 96.48200705917789, + 96.96389224218849, + 97.44594695704059, + 97.92818710651267, + 98.4104114725341, + 98.89272754895644, + 99.37486999050621, + 99.85701243205598, + 100.33904054716423, + 100.82079754565754, + 101.30228373234343, + 101.78331888883112, + 102.26390385933115, + 102.74448882983118, + 103.2252667823867, + 103.70612410037047, + 104.1861734206947, + 104.66531940292464, + 105.14397093606209, + 105.62197962066358, + 106.09943284309054, + 106.57651338134494, + 107.05274339839181, + 107.52838391015919, + 108.0036300256327, + 108.47861051006056, + 108.95349381753606, + 109.42827998781421, + 109.90286966929953, + 110.37707472958185, + 110.85056954493281, + 111.32318549922621, + 111.79543627982753, + 112.26817988977741, + 112.74135194249897, + 113.21452399522052, + 113.68737869171198, + 114.16024999407782, + 114.63272043012573, + 115.10508272059961, + 115.57753309305029, + 116.04989752492108, + 116.52214168949467, + 116.99410096847804, + 117.4659619958692, + 117.93763673277475, + 118.40898817101623, + 118.88033960925772, + 119.35201434616327, + 119.82350293961994, + 120.29448274760031, + 120.7650380718522, + 121.23507383536553, + 121.70588375286549, + 122.17667720895277, + 122.64786868955088, + 123.11906017014898, + 123.59005651655644, + 124.06133658689365, + 124.53291806164542, + 125.00487991175814, + 125.47703608030204, + 125.94927811365403, + 126.42194768070125, + 126.89483663672131, + 127.36760205500283, + 127.84036747328436, + 128.31345813349867, + 128.78685700982066, + 129.26069643439885, + 129.73461119929644, + 130.20898447081603, + 130.68316946329983, + 131.15795472608758, + 131.63304276945246, + 132.10794647448725, + 132.58254451481505, + 133.0567579203967, + 133.53077531087553, + 134.00449128349214, + 134.4782916124099, + 134.9519831861583, + 135.42600345462373, + 135.90021187131168, + 136.37450690092894, + 136.84892324484102, + 137.32332204122, + 137.79707434613707, + 138.26996775877035, + 138.74286117140363, + 139.21535529931515, + 139.6877634707289, + 140.16015466334403, + 140.63266619816537, + 141.1049641116907, + 141.57726202521602, + 142.04955993874134, + 142.5222230988828, + 142.9949945424239, + 143.46745685998428, + 143.93951900404934, + 144.4112822109413, + 144.88304541783324, + 145.3549117060568, + 145.82696443613506, + 146.2994173237413, + 146.77187021134753, + 147.24434050282505, + 147.7167933904313, + 148.18916033654267, + 148.6615103068186, + 149.13417695596576, + 149.60663927352613, + 150.07951905009202, + 150.55163298397414, + 151.0236389354043, + 151.49573283595507, + 151.96804017324905, + 152.44111398132586, + 152.91408460140627, + 153.38686341975009, + 153.85934968131488, + 154.33163176719088, + 154.8039138530669, + 155.27617854893404, + 155.74884214168688, + 156.22140016133048, + 156.69376671366393, + 157.16601299762056, + 157.63827667023006, + 158.1098999360764, + 158.58152320192275, + 159.05294173079446, + 159.52347991359056, + 159.99348754567202, + 160.46240196642222, + 160.93078948980357, + 161.3987354819744, + 161.86657538965613, + 162.33422292867684, + 162.80146010041048, + 163.26828590011118, + 163.73453137478074, + 164.20067153449637, + 164.66701469186685, + 165.1334585753361, + 165.59955673938802, + 166.065746953508, + 166.5317767598561, + 166.99788309008798, + 167.4647473938636, + 167.93195415720174, + 168.39960530348034, + 168.8677642407363, + 169.3557642407363, + 169.8427642407363, + 170.3187642407363, + 170.7947642407363 + ], + "hitFraction": 0.584, + "trackerOffsetMs": { + "median": -35, + "p90abs": 46.5 + } + }, + "meter": { + "gridShift": "motif-anchored, 2 beat-count flips at [27.85, 61.17]", + "beatsPerBar": 4, + "phase": 1, + "downbeatChangeFraction": 0.75 + }, + "bars": [ + { + "n": 1, + "t": 5.02, + "dur": 1.92, + "beats": [ + 5.02, + 5.494, + 5.968, + 6.454, + 6.94 + ], + "bpm": 125, + "chord": "G#m", + "agree": 1, + "strum": ".X.XXX.." + }, + { + "n": 2, + "t": 6.94, + "dur": 1.946, + "beats": [ + 6.94, + 7.427, + 7.913, + 8.4, + 8.886 + ], + "bpm": 123.3, + "chord": "G#m", + "agree": 0.75, + "strum": "X......." + }, + { + "n": 3, + "t": 8.886, + "dur": 1.946, + "beats": [ + 8.886, + 9.373, + 9.859, + 10.346, + 10.832 + ], + "bpm": 123.3, + "chord": "G#m", + "agree": 1, + "strum": "........" + }, + { + "n": 4, + "t": 10.832, + "dur": 1.916, + "beats": [ + 10.832, + 11.302, + 11.773, + 12.307, + 12.748 + ], + "bpm": 125.3, + "chord": "B", + "agree": 0.5, + "strum": "X.XXX.XX" + }, + { + "n": 5, + "t": 12.748, + "dur": 1.915, + "beats": [ + 12.748, + 13.23, + 13.711, + 14.187, + 14.663 + ], + "bpm": 125.3, + "chord": "B", + "agree": 0.5, + "strum": "X.XXX.XX" + }, + { + "n": 6, + "t": 14.663, + "dur": 1.962, + "beats": [ + 14.663, + 15.145, + 15.627, + 16.173, + 16.625 + ], + "bpm": 122.3, + "chord": "G#m", + "agree": 1, + "strum": "X..XX.XX" + }, + { + "n": 7, + "t": 16.625, + "dur": 1.881, + "beats": [ + 16.625, + 17.107, + 17.589, + 18.048, + 18.506 + ], + "bpm": 127.6, + "chord": "G#m", + "agree": 1, + "strum": "X..XX.XX" + }, + { + "n": 8, + "t": 18.506, + "dur": 1.928, + "beats": [ + 18.506, + 19.006, + 19.505, + 19.969, + 20.434 + ], + "bpm": 124.5, + "chord": "B", + "agree": 1, + "strum": "X..XX.XX" + }, + { + "n": 9, + "t": 20.434, + "dur": 1.892, + "beats": [ + 20.434, + 20.91, + 21.386, + 21.856, + 22.326 + ], + "bpm": 126.8, + "chord": "B", + "agree": 0.75, + "strum": "X..XX.XX" + }, + { + "n": 10, + "t": 22.326, + "dur": 1.985, + "beats": [ + 22.326, + 22.883, + 23.336, + 23.859, + 24.311 + ], + "bpm": 120.9, + "chord": "G#m", + "agree": 0.5, + "strum": "X.X.X.XX" + }, + { + "n": 11, + "t": 24.311, + "dur": 2.078, + "beats": [ + 24.311, + 24.892, + 25.368, + 25.902, + 26.389 + ], + "bpm": 115.5, + "chord": "B", + "agree": 0.75, + "strum": "X.XXX.XX" + }, + { + "n": 12, + "t": 26.389, + "dur": 2.425, + "beats": [ + 26.389, + 26.877, + 27.365, + 27.848, + 28.331, + 28.814 + ], + "bpm": 123.7, + "chord": "B", + "agree": 1, + "strum": ".X..XX...X" + }, + { + "n": 13, + "t": 28.814, + "dur": 2.022, + "beats": [ + 28.814, + 29.297, + 29.78, + 30.308, + 30.836 + ], + "bpm": 118.7, + "chord": "B", + "agree": 0.5, + "strum": "...XX..X" + }, + { + "n": 14, + "t": 30.836, + "dur": 2.148, + "beats": [ + 30.836, + 31.388, + 31.939, + 32.461, + 32.984 + ], + "bpm": 111.7, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 15, + "t": 32.984, + "dur": 2.113, + "beats": [ + 32.984, + 33.501, + 34.017, + 34.557, + 35.097 + ], + "bpm": 113.6, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 16, + "t": 35.097, + "dur": 2.171, + "beats": [ + 35.097, + 35.643, + 36.188, + 36.728, + 37.268 + ], + "bpm": 110.5, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 17, + "t": 37.268, + "dur": 2.159, + "beats": [ + 37.268, + 37.808, + 38.348, + 38.882, + 39.427 + ], + "bpm": 111.2, + "chord": "B", + "agree": 0.75, + "strum": "X..XX.X." + }, + { + "n": 18, + "t": 39.427, + "dur": 2.148, + "beats": [ + 39.427, + 39.962, + 40.496, + 41.035, + 41.575 + ], + "bpm": 111.7, + "chord": "Emaj7", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 19, + "t": 41.575, + "dur": 2.102, + "beats": [ + 41.575, + 42.104, + 42.632, + 43.143, + 43.677 + ], + "bpm": 114.2, + "chord": "B", + "agree": 0.5, + "strum": "X..XXXXX" + }, + { + "n": 20, + "t": 43.677, + "dur": 2.159, + "beats": [ + 43.677, + 44.217, + 44.756, + 45.296, + 45.836 + ], + "bpm": 111.2, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 21, + "t": 45.836, + "dur": 2.111, + "beats": [ + 45.836, + 46.363, + 46.891, + 47.418, + 47.947 + ], + "bpm": 113.7, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 22, + "t": 47.947, + "dur": 2.078, + "beats": [ + 47.947, + 48.468, + 48.988, + 49.507, + 50.025 + ], + "bpm": 115.5, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 23, + "t": 50.025, + "dur": 2.047, + "beats": [ + 50.025, + 50.537, + 51.048, + 51.56, + 52.072 + ], + "bpm": 117.2, + "chord": "B", + "agree": 0.75, + "strum": "X..XX..X" + }, + { + "n": 24, + "t": 52.072, + "dur": 2.035, + "beats": [ + 52.072, + 52.581, + 53.09, + 53.599, + 54.108 + ], + "bpm": 117.9, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 25, + "t": 54.108, + "dur": 2.035, + "beats": [ + 54.108, + 54.617, + 55.126, + 55.635, + 56.143 + ], + "bpm": 117.9, + "chord": "B", + "agree": 1, + "strum": "X..XX.X." + }, + { + "n": 26, + "t": 56.143, + "dur": 2.029, + "beats": [ + 56.143, + 56.65, + 57.157, + 57.664, + 58.172 + ], + "bpm": 118.3, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 27, + "t": 58.172, + "dur": 2.528, + "beats": [ + 58.172, + 58.679, + 59.185, + 59.691, + 60.196, + 60.7 + ], + "bpm": 118.7, + "chord": "B", + "agree": 0.8, + "strum": "X..XX.X..." + }, + { + "n": 28, + "t": 60.7, + "dur": 1.915, + "beats": [ + 60.7, + 61.179, + 61.658, + 62.137, + 62.615 + ], + "bpm": 125.3, + "chord": "Emaj7", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 29, + "t": 62.615, + "dur": 1.91, + "beats": [ + 62.615, + 63.093, + 63.571, + 64.048, + 64.526 + ], + "bpm": 125.6, + "chord": "Emaj7", + "agree": 0.75, + "strum": "..X.X..X" + }, + { + "n": 30, + "t": 64.526, + "dur": 1.907, + "beats": [ + 64.526, + 65.003, + 65.48, + 65.957, + 66.433 + ], + "bpm": 125.8, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 31, + "t": 66.433, + "dur": 1.903, + "beats": [ + 66.433, + 66.909, + 67.385, + 67.86, + 68.336 + ], + "bpm": 126.1, + "chord": "G#m", + "agree": 1, + "strum": "X..XXXXX" + }, + { + "n": 32, + "t": 68.336, + "dur": 1.905, + "beats": [ + 68.336, + 68.813, + 69.289, + 69.765, + 70.241 + ], + "bpm": 126, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 33, + "t": 70.241, + "dur": 1.904, + "beats": [ + 70.241, + 70.717, + 71.193, + 71.669, + 72.145 + ], + "bpm": 126.1, + "chord": "B", + "agree": 1, + "strum": "X..XX..." + }, + { + "n": 34, + "t": 72.145, + "dur": 1.902, + "beats": [ + 72.145, + 72.62, + 73.096, + 73.571, + 74.047 + ], + "bpm": 126.2, + "chord": "B", + "agree": 0.5, + "strum": "X..XX..X" + }, + { + "n": 35, + "t": 74.047, + "dur": 1.903, + "beats": [ + 74.047, + 74.523, + 74.999, + 75.475, + 75.95 + ], + "bpm": 126.1, + "chord": "G#m", + "agree": 0.75, + "strum": "X..XX.XX" + }, + { + "n": 36, + "t": 75.95, + "dur": 1.903, + "beats": [ + 75.95, + 76.426, + 76.902, + 77.378, + 77.853 + ], + "bpm": 126.1, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 37, + "t": 77.853, + "dur": 1.903, + "beats": [ + 77.853, + 78.329, + 78.805, + 79.281, + 79.757 + ], + "bpm": 126.1, + "chord": "G#m", + "agree": 1, + "strum": "X..XXXXX" + }, + { + "n": 38, + "t": 79.757, + "dur": 1.904, + "beats": [ + 79.757, + 80.232, + 80.708, + 81.184, + 81.66 + ], + "bpm": 126.1, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 39, + "t": 81.66, + "dur": 1.904, + "beats": [ + 81.66, + 82.136, + 82.612, + 83.088, + 83.564 + ], + "bpm": 126, + "chord": "B", + "agree": 0.75, + "strum": "X..XX.XX" + }, + { + "n": 40, + "t": 83.564, + "dur": 1.904, + "beats": [ + 83.564, + 84.04, + 84.516, + 84.992, + 85.468 + ], + "bpm": 126.1, + "chord": "B", + "agree": 0.5, + "strum": "X..XX..X" + }, + { + "n": 41, + "t": 85.468, + "dur": 1.902, + "beats": [ + 85.468, + 85.944, + 86.42, + 86.895, + 87.37 + ], + "bpm": 126.2, + "chord": "Emaj7", + "agree": 0.75, + "strum": "X..XX..X" + }, + { + "n": 42, + "t": 87.37, + "dur": 1.907, + "beats": [ + 87.37, + 87.846, + 88.322, + 88.8, + 89.277 + ], + "bpm": 125.8, + "chord": "B", + "agree": 0.75, + "strum": "XXXXX.X." + }, + { + "n": 43, + "t": 89.277, + "dur": 1.913, + "beats": [ + 89.277, + 89.755, + 90.233, + 90.711, + 91.19 + ], + "bpm": 125.5, + "chord": "G#m", + "agree": 0.75, + "strum": "XXX.XXXX" + }, + { + "n": 44, + "t": 91.19, + "dur": 1.918, + "beats": [ + 91.19, + 91.669, + 92.148, + 92.628, + 93.108 + ], + "bpm": 125.1, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 45, + "t": 93.108, + "dur": 1.927, + "beats": [ + 93.108, + 93.589, + 94.071, + 94.553, + 95.035 + ], + "bpm": 124.5, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 46, + "t": 95.035, + "dur": 1.928, + "beats": [ + 95.035, + 95.518, + 96, + 96.482, + 96.964 + ], + "bpm": 124.5, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 47, + "t": 96.964, + "dur": 1.929, + "beats": [ + 96.964, + 97.446, + 97.928, + 98.41, + 98.893 + ], + "bpm": 124.4, + "chord": "B", + "agree": 0.75, + "strum": "X..XX..X" + }, + { + "n": 48, + "t": 98.893, + "dur": 1.928, + "beats": [ + 98.893, + 99.375, + 99.857, + 100.339, + 100.821 + ], + "bpm": 124.5, + "chord": "G#m", + "agree": 0.75, + "strum": "X..XX..X" + }, + { + "n": 49, + "t": 100.821, + "dur": 1.924, + "beats": [ + 100.821, + 101.302, + 101.783, + 102.264, + 102.744 + ], + "bpm": 124.8, + "chord": "Emaj7", + "agree": 0.75, + "strum": "X..XX.X." + }, + { + "n": 50, + "t": 102.744, + "dur": 1.921, + "beats": [ + 102.744, + 103.225, + 103.706, + 104.186, + 104.665 + ], + "bpm": 124.9, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 51, + "t": 104.665, + "dur": 1.911, + "beats": [ + 104.665, + 105.144, + 105.622, + 106.099, + 106.577 + ], + "bpm": 125.6, + "chord": "G#m", + "agree": 1, + "strum": "X..XX.XX" + }, + { + "n": 52, + "t": 106.577, + "dur": 1.902, + "beats": [ + 106.577, + 107.053, + 107.528, + 108.004, + 108.479 + ], + "bpm": 126.2, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 53, + "t": 108.479, + "dur": 1.898, + "beats": [ + 108.479, + 108.953, + 109.428, + 109.903, + 110.377 + ], + "bpm": 126.4, + "chord": "B", + "agree": 1, + "strum": "X..XXX.X" + }, + { + "n": 54, + "t": 110.377, + "dur": 1.891, + "beats": [ + 110.377, + 110.851, + 111.323, + 111.795, + 112.268 + ], + "bpm": 126.9, + "chord": "B", + "agree": 0.5, + "strum": "X..XX..." + }, + { + "n": 55, + "t": 112.268, + "dur": 1.892, + "beats": [ + 112.268, + 112.741, + 113.215, + 113.687, + 114.16 + ], + "bpm": 126.8, + "chord": "G#m", + "agree": 0.5, + "strum": "X..XXXXX" + }, + { + "n": 56, + "t": 114.16, + "dur": 1.89, + "beats": [ + 114.16, + 114.633, + 115.105, + 115.578, + 116.05 + ], + "bpm": 127, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 57, + "t": 116.05, + "dur": 1.888, + "beats": [ + 116.05, + 116.522, + 116.994, + 117.466, + 117.938 + ], + "bpm": 127.1, + "chord": "G#m", + "agree": 1, + "strum": "X..XXXXX" + }, + { + "n": 58, + "t": 117.938, + "dur": 1.886, + "beats": [ + 117.938, + 118.409, + 118.88, + 119.352, + 119.824 + ], + "bpm": 127.3, + "chord": "B", + "agree": 0.75, + "strum": "X..XX..X" + }, + { + "n": 59, + "t": 119.824, + "dur": 1.882, + "beats": [ + 119.824, + 120.294, + 120.765, + 121.235, + 121.706 + ], + "bpm": 127.5, + "chord": "B", + "agree": 0.5, + "strum": "X..XXXXX" + }, + { + "n": 60, + "t": 121.706, + "dur": 1.884, + "beats": [ + 121.706, + 122.177, + 122.648, + 123.119, + 123.59 + ], + "bpm": 127.4, + "chord": "B", + "agree": 0.5, + "strum": "X..XX..X" + }, + { + "n": 61, + "t": 123.59, + "dur": 1.887, + "beats": [ + 123.59, + 124.061, + 124.533, + 125.005, + 125.477 + ], + "bpm": 127.2, + "chord": "B", + "agree": 0.5, + "strum": "X..XXXXX" + }, + { + "n": 62, + "t": 125.477, + "dur": 1.891, + "beats": [ + 125.477, + 125.949, + 126.422, + 126.895, + 127.368 + ], + "bpm": 126.9, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 63, + "t": 127.368, + "dur": 1.893, + "beats": [ + 127.368, + 127.84, + 128.313, + 128.787, + 129.261 + ], + "bpm": 126.8, + "chord": "G#m", + "agree": 1, + "strum": "X..XX.XX" + }, + { + "n": 64, + "t": 129.261, + "dur": 1.897, + "beats": [ + 129.261, + 129.735, + 130.209, + 130.683, + 131.158 + ], + "bpm": 126.5, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 65, + "t": 131.158, + "dur": 1.899, + "beats": [ + 131.158, + 131.633, + 132.108, + 132.583, + 133.057 + ], + "bpm": 126.4, + "chord": "B", + "agree": 1, + "strum": "X.XXX..." + }, + { + "n": 66, + "t": 133.057, + "dur": 1.895, + "beats": [ + 133.057, + 133.531, + 134.004, + 134.478, + 134.952 + ], + "bpm": 126.6, + "chord": "B", + "agree": 0.5, + "strum": "X..XXXXX" + }, + { + "n": 67, + "t": 134.952, + "dur": 1.897, + "beats": [ + 134.952, + 135.426, + 135.9, + 136.375, + 136.849 + ], + "bpm": 126.5, + "chord": "G#m", + "agree": 0.75, + "strum": "X..XXX.X" + }, + { + "n": 68, + "t": 136.849, + "dur": 1.894, + "beats": [ + 136.849, + 137.323, + 137.797, + 138.27, + 138.743 + ], + "bpm": 126.7, + "chord": "G#m", + "agree": 1, + "strum": "XXXXXXX." + }, + { + "n": 69, + "t": 138.743, + "dur": 1.89, + "beats": [ + 138.743, + 139.215, + 139.688, + 140.16, + 140.633 + ], + "bpm": 127, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 70, + "t": 140.633, + "dur": 1.89, + "beats": [ + 140.633, + 141.105, + 141.577, + 142.05, + 142.522 + ], + "bpm": 127, + "chord": "G#m", + "agree": 1, + "strum": "X..XXXXX" + }, + { + "n": 71, + "t": 142.522, + "dur": 1.889, + "beats": [ + 142.522, + 142.995, + 143.467, + 143.94, + 144.411 + ], + "bpm": 127, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 72, + "t": 144.411, + "dur": 1.888, + "beats": [ + 144.411, + 144.883, + 145.355, + 145.827, + 146.299 + ], + "bpm": 127.1, + "chord": "B", + "agree": 0.5, + "strum": "X.XXX.XX" + }, + { + "n": 73, + "t": 146.299, + "dur": 1.89, + "beats": [ + 146.299, + 146.772, + 147.244, + 147.717, + 148.189 + ], + "bpm": 127, + "chord": "Emaj7", + "agree": 0.5, + "strum": "X..XX..X" + }, + { + "n": 74, + "t": 148.189, + "dur": 1.89, + "beats": [ + 148.189, + 148.662, + 149.134, + 149.607, + 150.08 + ], + "bpm": 127, + "chord": "B", + "agree": 0.5, + "strum": "X..XX.X." + }, + { + "n": 75, + "t": 150.08, + "dur": 1.889, + "beats": [ + 150.08, + 150.552, + 151.024, + 151.496, + 151.968 + ], + "bpm": 127.1, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 76, + "t": 151.968, + "dur": 1.891, + "beats": [ + 151.968, + 152.441, + 152.914, + 153.387, + 153.859 + ], + "bpm": 126.9, + "chord": "G#m", + "agree": 1, + "strum": "XX.XXXX." + }, + { + "n": 77, + "t": 153.859, + "dur": 1.889, + "beats": [ + 153.859, + 154.332, + 154.804, + 155.276, + 155.749 + ], + "bpm": 127, + "chord": "B", + "agree": 1, + "strum": "X..XXX.X" + }, + { + "n": 78, + "t": 155.749, + "dur": 1.889, + "beats": [ + 155.749, + 156.221, + 156.694, + 157.166, + 157.638 + ], + "bpm": 127, + "chord": "B", + "agree": 1, + "strum": "XXXXX..X" + }, + { + "n": 79, + "t": 157.638, + "dur": 1.885, + "beats": [ + 157.638, + 158.11, + 158.582, + 159.053, + 159.523 + ], + "bpm": 127.3, + "chord": "B", + "agree": 0.75, + "strum": "X..XX.XX" + }, + { + "n": 80, + "t": 159.523, + "dur": 1.875, + "beats": [ + 159.523, + 159.993, + 160.462, + 160.931, + 161.399 + ], + "bpm": 128, + "chord": "Emaj7", + "agree": 0.5, + "strum": "X..XX..X" + }, + { + "n": 81, + "t": 161.399, + "dur": 1.87, + "beats": [ + 161.399, + 161.867, + 162.334, + 162.801, + 163.268 + ], + "bpm": 128.4, + "chord": "G#m", + "agree": 1, + "strum": "XXXXXXXX" + }, + { + "n": 82, + "t": 163.268, + "dur": 1.865, + "beats": [ + 163.268, + 163.735, + 164.201, + 164.667, + 165.133 + ], + "bpm": 128.7, + "chord": "G#m", + "agree": 1, + "strum": "X..XX..X" + }, + { + "n": 83, + "t": 165.133, + "dur": 1.864, + "beats": [ + 165.133, + 165.6, + 166.066, + 166.532, + 166.998 + ], + "bpm": 128.7, + "chord": "G#m", + "agree": 1, + "strum": "X..XXXXX" + }, + { + "n": 84, + "t": 166.998, + "dur": 1.87, + "beats": [ + 166.998, + 167.465, + 167.932, + 168.4, + 168.868 + ], + "bpm": 128.4, + "chord": "B", + "agree": 1, + "strum": "X..XX..X" + } + ], + "strumList": [ + { + "t": 1.707, + "amp": 0.3, + "dir": "D" + }, + { + "t": 2.159, + "amp": 0.48, + "dir": "U" + }, + { + "t": 2.798, + "amp": 0.1, + "dir": "D" + }, + { + "t": 3.274, + "amp": 0.26, + "dir": "D" + }, + { + "t": 3.599, + "amp": 1.06, + "dir": "D" + }, + { + "t": 3.68, + "amp": 0.31, + "dir": "D" + }, + { + "t": 5.317, + "amp": 0.23, + "dir": "D" + }, + { + "t": 5.677, + "amp": 0.3, + "dir": "D" + }, + { + "t": 5.968, + "amp": 0.46, + "dir": "D" + }, + { + "t": 6.223, + "amp": 0.26, + "dir": "D" + }, + { + "t": 6.931, + "amp": 0.24, + "dir": "D" + }, + { + "t": 10.832, + "amp": 1.16, + "dir": "U" + }, + { + "t": 11.285, + "amp": 0.34, + "dir": "D" + }, + { + "t": 11.459, + "amp": 0.42, + "dir": "D" + }, + { + "t": 11.598, + "amp": 0.15, + "dir": "D" + }, + { + "t": 11.773, + "amp": 0.55, + "dir": "U" + }, + { + "t": 12.307, + "amp": 0.51, + "dir": "U" + }, + { + "t": 12.562, + "amp": 0.41, + "dir": "U" + }, + { + "t": 12.748, + "amp": 0.63, + "dir": "U" + }, + { + "t": 13.247, + "amp": 0.24, + "dir": "D" + }, + { + "t": 13.41, + "amp": 0.36, + "dir": "D" + }, + { + "t": 13.537, + "amp": 0.29, + "dir": "U" + }, + { + "t": 13.711, + "amp": 0.61, + "dir": "U" + }, + { + "t": 14.176, + "amp": 0.26, + "dir": "D" + }, + { + "t": 14.35, + "amp": 0.41, + "dir": "U" + }, + { + "t": 14.512, + "amp": 0.12, + "dir": "D" + }, + { + "t": 14.663, + "amp": 0.74, + "dir": "U" + }, + { + "t": 15.163, + "amp": 0.15, + "dir": "D" + }, + { + "t": 15.453, + "amp": 0.33, + "dir": "U" + }, + { + "t": 15.627, + "amp": 0.66, + "dir": "U" + }, + { + "t": 16.173, + "amp": 0.5, + "dir": "D" + }, + { + "t": 16.335, + "amp": 0.41, + "dir": "D" + }, + { + "t": 16.625, + "amp": 0.5, + "dir": "U" + }, + { + "t": 17.16, + "amp": 0.12, + "dir": "D" + }, + { + "t": 17.415, + "amp": 0.6, + "dir": "U" + }, + { + "t": 17.589, + "amp": 0.7, + "dir": "U" + }, + { + "t": 18.019, + "amp": 0.24, + "dir": "D" + }, + { + "t": 18.181, + "amp": 0.35, + "dir": "D" + }, + { + "t": 18.332, + "amp": 0.24, + "dir": "D" + }, + { + "t": 18.506, + "amp": 0.38, + "dir": "D" + }, + { + "t": 19.203, + "amp": 0.64, + "dir": "D" + }, + { + "t": 19.365, + "amp": 0.19, + "dir": "U" + }, + { + "t": 19.505, + "amp": 0.37, + "dir": "U" + }, + { + "t": 20.004, + "amp": 0.28, + "dir": "D" + }, + { + "t": 20.132, + "amp": 0.3, + "dir": "D" + }, + { + "t": 20.283, + "amp": 0.18, + "dir": "U" + }, + { + "t": 20.434, + "amp": 0.46, + "dir": "U" + }, + { + "t": 21.072, + "amp": 0.43, + "dir": "D" + }, + { + "t": 21.211, + "amp": 0.48, + "dir": "U" + }, + { + "t": 21.386, + "amp": 0.55, + "dir": "U" + }, + { + "t": 21.966, + "amp": 0.62, + "dir": "D" + }, + { + "t": 22.163, + "amp": 0.21, + "dir": "D" + }, + { + "t": 22.326, + "amp": 0.79, + "dir": "U" + }, + { + "t": 22.883, + "amp": 0.46, + "dir": "D" + }, + { + "t": 23.023, + "amp": 0.2, + "dir": "D" + }, + { + "t": 23.174, + "amp": 0.2, + "dir": "D" + }, + { + "t": 23.336, + "amp": 0.86, + "dir": "U" + }, + { + "t": 23.859, + "amp": 0.61, + "dir": "D" + }, + { + "t": 24.009, + "amp": 0.32, + "dir": "D" + }, + { + "t": 24.311, + "amp": 0.73, + "dir": "U" + }, + { + "t": 24.892, + "amp": 0.46, + "dir": "D" + }, + { + "t": 25.031, + "amp": 0.43, + "dir": "D" + }, + { + "t": 25.217, + "amp": 0.14, + "dir": "D" + }, + { + "t": 25.368, + "amp": 0.59, + "dir": "U" + }, + { + "t": 25.902, + "amp": 0.43, + "dir": "D" + }, + { + "t": 26.204, + "amp": 0.59, + "dir": "D" + }, + { + "t": 26.529, + "amp": 0.62, + "dir": "U" + }, + { + "t": 27.365, + "amp": 0.45, + "dir": "D" + }, + { + "t": 27.655, + "amp": 0.84, + "dir": "U" + }, + { + "t": 28.456, + "amp": 0.49, + "dir": "D" + }, + { + "t": 28.735, + "amp": 0.35, + "dir": "D" + }, + { + "t": 29.513, + "amp": 0.59, + "dir": "D" + }, + { + "t": 29.78, + "amp": 0.78, + "dir": "D" + }, + { + "t": 30.557, + "amp": 0.38, + "dir": "D" + }, + { + "t": 30.836, + "amp": 0.79, + "dir": "U" + }, + { + "t": 31.684, + "amp": 0.48, + "dir": "D" + }, + { + "t": 31.939, + "amp": 0.92, + "dir": "U" + }, + { + "t": 32.74, + "amp": 0.24, + "dir": "D" + }, + { + "t": 32.984, + "amp": 0.91, + "dir": "U" + }, + { + "t": 33.75, + "amp": 0.52, + "dir": "D" + }, + { + "t": 34.017, + "amp": 0.66, + "dir": "U" + }, + { + "t": 34.795, + "amp": 0.38, + "dir": "D" + }, + { + "t": 35.097, + "amp": 0.83, + "dir": "U" + }, + { + "t": 35.944, + "amp": 0.45, + "dir": "D" + }, + { + "t": 36.188, + "amp": 0.74, + "dir": "U" + }, + { + "t": 36.989, + "amp": 0.49, + "dir": "D" + }, + { + "t": 37.268, + "amp": 0.53, + "dir": "D" + }, + { + "t": 38.081, + "amp": 0.54, + "dir": "U" + }, + { + "t": 38.348, + "amp": 0.66, + "dir": "U" + }, + { + "t": 38.882, + "amp": 0.62, + "dir": "U" + }, + { + "t": 39.219, + "amp": 0.1, + "dir": "D" + }, + { + "t": 39.427, + "amp": 0.81, + "dir": "U" + }, + { + "t": 40.252, + "amp": 0.53, + "dir": "D" + }, + { + "t": 40.496, + "amp": 0.7, + "dir": "U" + }, + { + "t": 41.355, + "amp": 0.44, + "dir": "D" + }, + { + "t": 41.575, + "amp": 0.92, + "dir": "U" + }, + { + "t": 42.4, + "amp": 0.46, + "dir": "D" + }, + { + "t": 42.632, + "amp": 0.74, + "dir": "D" + }, + { + "t": 42.887, + "amp": 0.46, + "dir": "D" + }, + { + "t": 43.143, + "amp": 0.71, + "dir": "U" + }, + { + "t": 43.41, + "amp": 0.49, + "dir": "D" + }, + { + "t": 43.677, + "amp": 0.65, + "dir": "D" + }, + { + "t": 44.501, + "amp": 0.25, + "dir": "D" + }, + { + "t": 44.756, + "amp": 0.94, + "dir": "D" + }, + { + "t": 45.569, + "amp": 0.73, + "dir": "D" + }, + { + "t": 45.836, + "amp": 0.67, + "dir": "U" + }, + { + "t": 46.624, + "amp": 0.48, + "dir": "D" + }, + { + "t": 46.891, + "amp": 0.72, + "dir": "U" + }, + { + "t": 47.645, + "amp": 0.61, + "dir": "D" + }, + { + "t": 47.947, + "amp": 0.79, + "dir": "U" + }, + { + "t": 48.739, + "amp": 0.48, + "dir": "D" + }, + { + "t": 48.988, + "amp": 0.93, + "dir": "U" + }, + { + "t": 49.76, + "amp": 0.47, + "dir": "D" + }, + { + "t": 50.025, + "amp": 0.82, + "dir": "U" + }, + { + "t": 50.815, + "amp": 0.49, + "dir": "D" + }, + { + "t": 51.048, + "amp": 0.59, + "dir": "U" + }, + { + "t": 51.784, + "amp": 0.64, + "dir": "D" + }, + { + "t": 52.072, + "amp": 0.75, + "dir": "U" + }, + { + "t": 52.836, + "amp": 0.64, + "dir": "D" + }, + { + "t": 53.09, + "amp": 0.9, + "dir": "U" + }, + { + "t": 53.833, + "amp": 0.61, + "dir": "D" + }, + { + "t": 54.108, + "amp": 0.8, + "dir": "U" + }, + { + "t": 54.836, + "amp": 0.55, + "dir": "D" + }, + { + "t": 55.126, + "amp": 0.82, + "dir": "U" + }, + { + "t": 55.635, + "amp": 0.74, + "dir": "D" + }, + { + "t": 56.143, + "amp": 1.16, + "dir": "U" + }, + { + "t": 56.912, + "amp": 0.36, + "dir": "D" + }, + { + "t": 57.157, + "amp": 0.69, + "dir": "U" + }, + { + "t": 57.928, + "amp": 0.68, + "dir": "U" + }, + { + "t": 58.172, + "amp": 1.08, + "dir": "U" + }, + { + "t": 58.949, + "amp": 0.59, + "dir": "D" + }, + { + "t": 59.185, + "amp": 0.82, + "dir": "U" + }, + { + "t": 59.691, + "amp": 0.7, + "dir": "D" + }, + { + "t": 60.7, + "amp": 0.97, + "dir": "U" + }, + { + "t": 61.359, + "amp": 0.56, + "dir": "D" + }, + { + "t": 61.658, + "amp": 0.76, + "dir": "U" + }, + { + "t": 62.285, + "amp": 0.42, + "dir": "D" + }, + { + "t": 63.093, + "amp": 1.08, + "dir": "U" + }, + { + "t": 63.571, + "amp": 0.77, + "dir": "D" + }, + { + "t": 64.298, + "amp": 0.58, + "dir": "D" + }, + { + "t": 64.526, + "amp": 0.94, + "dir": "U" + }, + { + "t": 65.327, + "amp": 0.32, + "dir": "D" + }, + { + "t": 65.48, + "amp": 0.72, + "dir": "U" + }, + { + "t": 66.226, + "amp": 0.66, + "dir": "U" + }, + { + "t": 66.456, + "amp": 0.31, + "dir": "D" + }, + { + "t": 67.046, + "amp": 0.49, + "dir": "D" + }, + { + "t": 67.385, + "amp": 0.95, + "dir": "U" + }, + { + "t": 67.628, + "amp": 0.27, + "dir": "D" + }, + { + "t": 67.86, + "amp": 0.54, + "dir": "D" + }, + { + "t": 68.06, + "amp": 0.33, + "dir": "D" + }, + { + "t": 68.336, + "amp": 0.78, + "dir": "U" + }, + { + "t": 69.087, + "amp": 0.66, + "dir": "D" + }, + { + "t": 69.289, + "amp": 0.8, + "dir": "U" + }, + { + "t": 70.031, + "amp": 0.65, + "dir": "D" + }, + { + "t": 70.241, + "amp": 0.88, + "dir": "U" + }, + { + "t": 70.961, + "amp": 0.6, + "dir": "U" + }, + { + "t": 71.193, + "amp": 0.9, + "dir": "U" + }, + { + "t": 72.145, + "amp": 1.38, + "dir": "U" + }, + { + "t": 72.875, + "amp": 0.56, + "dir": "D" + }, + { + "t": 73.096, + "amp": 0.63, + "dir": "U" + }, + { + "t": 73.665, + "amp": 0.12, + "dir": "D" + }, + { + "t": 73.861, + "amp": 0.28, + "dir": "D" + }, + { + "t": 74.047, + "amp": 0.7, + "dir": "U" + }, + { + "t": 74.779, + "amp": 0.49, + "dir": "D" + }, + { + "t": 74.999, + "amp": 0.7, + "dir": "U" + }, + { + "t": 75.27, + "amp": 0.2, + "dir": "D" + }, + { + "t": 75.475, + "amp": 1, + "dir": "U" + }, + { + "t": 75.745, + "amp": 0.3, + "dir": "D" + }, + { + "t": 75.95, + "amp": 1.24, + "dir": "U" + }, + { + "t": 76.676, + "amp": 0.59, + "dir": "D" + }, + { + "t": 76.902, + "amp": 0.84, + "dir": "U" + }, + { + "t": 77.626, + "amp": 0.64, + "dir": "D" + }, + { + "t": 77.853, + "amp": 1.03, + "dir": "U" + }, + { + "t": 78.581, + "amp": 0.53, + "dir": "D" + }, + { + "t": 78.805, + "amp": 0.6, + "dir": "D" + }, + { + "t": 79.053, + "amp": 0.22, + "dir": "D" + }, + { + "t": 79.281, + "amp": 0.81, + "dir": "U" + }, + { + "t": 79.529, + "amp": 0.32, + "dir": "D" + }, + { + "t": 79.757, + "amp": 0.95, + "dir": "U" + }, + { + "t": 80.465, + "amp": 0.67, + "dir": "D" + }, + { + "t": 80.708, + "amp": 0.91, + "dir": "U" + }, + { + "t": 81.422, + "amp": 0.54, + "dir": "U" + }, + { + "t": 81.66, + "amp": 0.89, + "dir": "U" + }, + { + "t": 82.369, + "amp": 0.5, + "dir": "D" + }, + { + "t": 82.612, + "amp": 0.69, + "dir": "U" + }, + { + "t": 83.088, + "amp": 0.66, + "dir": "D" + }, + { + "t": 83.354, + "amp": 0.35, + "dir": "D" + }, + { + "t": 83.564, + "amp": 0.77, + "dir": "U" + }, + { + "t": 84.27, + "amp": 0.57, + "dir": "D" + }, + { + "t": 84.516, + "amp": 0.69, + "dir": "U" + }, + { + "t": 85.282, + "amp": 0.48, + "dir": "D" + }, + { + "t": 85.468, + "amp": 0.98, + "dir": "U" + }, + { + "t": 86.187, + "amp": 0.67, + "dir": "D" + }, + { + "t": 86.42, + "amp": 0.76, + "dir": "U" + }, + { + "t": 87.132, + "amp": 0.52, + "dir": "D" + }, + { + "t": 87.37, + "amp": 0.76, + "dir": "U" + }, + { + "t": 87.646, + "amp": 0.27, + "dir": "D" + }, + { + "t": 87.846, + "amp": 0.5, + "dir": "D" + }, + { + "t": 88.073, + "amp": 0.36, + "dir": "D" + }, + { + "t": 88.322, + "amp": 0.61, + "dir": "U" + }, + { + "t": 88.589, + "amp": 0.11, + "dir": "D" + }, + { + "t": 88.8, + "amp": 0.39, + "dir": "D" + }, + { + "t": 89.277, + "amp": 0.42, + "dir": "D" + }, + { + "t": 89.55, + "amp": 0.26, + "dir": "D" + }, + { + "t": 89.755, + "amp": 0.71, + "dir": "D" + }, + { + "t": 90.005, + "amp": 0.16, + "dir": "D" + }, + { + "t": 90.233, + "amp": 0.47, + "dir": "D" + }, + { + "t": 90.467, + "amp": 0.3, + "dir": "D" + }, + { + "t": 90.711, + "amp": 0.78, + "dir": "U" + }, + { + "t": 91.005, + "amp": 0.39, + "dir": "D" + }, + { + "t": 91.19, + "amp": 0.98, + "dir": "U" + }, + { + "t": 91.911, + "amp": 0.45, + "dir": "D" + }, + { + "t": 92.148, + "amp": 0.75, + "dir": "D" + }, + { + "t": 92.895, + "amp": 0.53, + "dir": "D" + }, + { + "t": 93.108, + "amp": 0.7, + "dir": "D" + }, + { + "t": 93.825, + "amp": 0.49, + "dir": "D" + }, + { + "t": 94.071, + "amp": 0.56, + "dir": "D" + }, + { + "t": 94.803, + "amp": 0.6, + "dir": "D" + }, + { + "t": 95.035, + "amp": 0.79, + "dir": "U" + }, + { + "t": 95.783, + "amp": 0.46, + "dir": "D" + }, + { + "t": 96, + "amp": 0.7, + "dir": "U" + }, + { + "t": 96.72, + "amp": 0.61, + "dir": "D" + }, + { + "t": 96.964, + "amp": 0.69, + "dir": "U" + }, + { + "t": 97.742, + "amp": 0.35, + "dir": "D" + }, + { + "t": 97.928, + "amp": 0.78, + "dir": "U" + }, + { + "t": 98.63, + "amp": 0.57, + "dir": "U" + }, + { + "t": 98.893, + "amp": 1.08, + "dir": "U" + }, + { + "t": 99.618, + "amp": 0.59, + "dir": "D" + }, + { + "t": 99.857, + "amp": 0.9, + "dir": "U" + }, + { + "t": 100.596, + "amp": 0.63, + "dir": "D" + }, + { + "t": 100.821, + "amp": 0.96, + "dir": "U" + }, + { + "t": 101.532, + "amp": 0.56, + "dir": "D" + }, + { + "t": 101.783, + "amp": 0.81, + "dir": "U" + }, + { + "t": 102.264, + "amp": 0.72, + "dir": "U" + }, + { + "t": 102.744, + "amp": 0.92, + "dir": "U" + }, + { + "t": 103.479, + "amp": 0.23, + "dir": "D" + }, + { + "t": 103.706, + "amp": 0.82, + "dir": "U" + }, + { + "t": 104.426, + "amp": 0.62, + "dir": "U" + }, + { + "t": 104.665, + "amp": 0.92, + "dir": "U" + }, + { + "t": 105.394, + "amp": 0.48, + "dir": "D" + }, + { + "t": 105.622, + "amp": 0.82, + "dir": "U" + }, + { + "t": 106.099, + "amp": 1.12, + "dir": "U" + }, + { + "t": 106.37, + "amp": 0.51, + "dir": "U" + }, + { + "t": 106.577, + "amp": 0.7, + "dir": "U" + }, + { + "t": 107.285, + "amp": 0.44, + "dir": "D" + }, + { + "t": 107.528, + "amp": 0.8, + "dir": "U" + }, + { + "t": 108.255, + "amp": 0.63, + "dir": "D" + }, + { + "t": 108.479, + "amp": 0.54, + "dir": "U" + }, + { + "t": 109.182, + "amp": 0.7, + "dir": "U" + }, + { + "t": 109.428, + "amp": 0.67, + "dir": "U" + }, + { + "t": 109.682, + "amp": 0.26, + "dir": "D" + }, + { + "t": 110.135, + "amp": 0.58, + "dir": "D" + }, + { + "t": 110.377, + "amp": 0.7, + "dir": "U" + }, + { + "t": 111.096, + "amp": 0.55, + "dir": "D" + }, + { + "t": 111.323, + "amp": 0.77, + "dir": "U" + }, + { + "t": 112.268, + "amp": 0.92, + "dir": "U" + }, + { + "t": 112.975, + "amp": 0.64, + "dir": "D" + }, + { + "t": 113.215, + "amp": 0.91, + "dir": "U" + }, + { + "t": 113.468, + "amp": 0.27, + "dir": "D" + }, + { + "t": 113.687, + "amp": 0.99, + "dir": "U" + }, + { + "t": 113.924, + "amp": 0.45, + "dir": "D" + }, + { + "t": 114.16, + "amp": 0.8, + "dir": "U" + }, + { + "t": 114.883, + "amp": 0.5, + "dir": "D" + }, + { + "t": 115.105, + "amp": 0.8, + "dir": "U" + }, + { + "t": 115.852, + "amp": 0.51, + "dir": "D" + }, + { + "t": 116.05, + "amp": 1.25, + "dir": "U" + }, + { + "t": 116.761, + "amp": 0.63, + "dir": "D" + }, + { + "t": 116.994, + "amp": 0.99, + "dir": "U" + }, + { + "t": 117.248, + "amp": 0.24, + "dir": "D" + }, + { + "t": 117.466, + "amp": 0.45, + "dir": "D" + }, + { + "t": 117.707, + "amp": 0.4, + "dir": "D" + }, + { + "t": 117.938, + "amp": 0.87, + "dir": "U" + }, + { + "t": 118.639, + "amp": 0.48, + "dir": "U" + }, + { + "t": 118.88, + "amp": 0.64, + "dir": "U" + }, + { + "t": 119.61, + "amp": 0.62, + "dir": "D" + }, + { + "t": 119.824, + "amp": 0.58, + "dir": "U" + }, + { + "t": 120.396, + "amp": 0.11, + "dir": "D" + }, + { + "t": 120.541, + "amp": 0.47, + "dir": "D" + }, + { + "t": 120.765, + "amp": 1.01, + "dir": "U" + }, + { + "t": 121.005, + "amp": 0.3, + "dir": "D" + }, + { + "t": 121.235, + "amp": 0.58, + "dir": "U" + }, + { + "t": 121.482, + "amp": 0.37, + "dir": "U" + }, + { + "t": 121.706, + "amp": 0.88, + "dir": "U" + }, + { + "t": 122.438, + "amp": 0.49, + "dir": "D" + }, + { + "t": 122.648, + "amp": 0.9, + "dir": "U" + }, + { + "t": 123.355, + "amp": 0.81, + "dir": "D" + }, + { + "t": 123.59, + "amp": 0.78, + "dir": "U" + }, + { + "t": 124.335, + "amp": 0.45, + "dir": "D" + }, + { + "t": 124.533, + "amp": 0.92, + "dir": "U" + }, + { + "t": 124.781, + "amp": 0.62, + "dir": "U" + }, + { + "t": 125.005, + "amp": 0.64, + "dir": "U" + }, + { + "t": 125.268, + "amp": 0.43, + "dir": "D" + }, + { + "t": 125.477, + "amp": 1, + "dir": "U" + }, + { + "t": 126.202, + "amp": 0.49, + "dir": "D" + }, + { + "t": 126.422, + "amp": 1.04, + "dir": "U" + }, + { + "t": 127.172, + "amp": 0.71, + "dir": "D" + }, + { + "t": 127.368, + "amp": 0.93, + "dir": "U" + }, + { + "t": 128.096, + "amp": 0.61, + "dir": "D" + }, + { + "t": 128.313, + "amp": 1, + "dir": "U" + }, + { + "t": 128.583, + "amp": 0.16, + "dir": "D" + }, + { + "t": 128.787, + "amp": 0.55, + "dir": "D" + }, + { + "t": 129.056, + "amp": 0.3, + "dir": "D" + }, + { + "t": 129.261, + "amp": 1.14, + "dir": "U" + }, + { + "t": 129.986, + "amp": 0.61, + "dir": "U" + }, + { + "t": 130.209, + "amp": 0.43, + "dir": "U" + }, + { + "t": 130.506, + "amp": 0.18, + "dir": "D" + }, + { + "t": 130.964, + "amp": 0.37, + "dir": "D" + }, + { + "t": 131.158, + "amp": 0.82, + "dir": "U" + }, + { + "t": 131.633, + "amp": 0.65, + "dir": "D" + }, + { + "t": 131.915, + "amp": 0.26, + "dir": "D" + }, + { + "t": 132.108, + "amp": 0.82, + "dir": "U" + }, + { + "t": 133.057, + "amp": 0.81, + "dir": "U" + }, + { + "t": 133.349, + "amp": 0.14, + "dir": "D" + }, + { + "t": 133.568, + "amp": 0.18, + "dir": "D" + }, + { + "t": 133.796, + "amp": 0.54, + "dir": "D" + }, + { + "t": 134.004, + "amp": 0.98, + "dir": "U" + }, + { + "t": 134.264, + "amp": 0.3, + "dir": "D" + }, + { + "t": 134.478, + "amp": 0.71, + "dir": "U" + }, + { + "t": 134.716, + "amp": 0.24, + "dir": "D" + }, + { + "t": 134.952, + "amp": 1.02, + "dir": "U" + }, + { + "t": 135.22, + "amp": 0.17, + "dir": "D" + }, + { + "t": 135.678, + "amp": 0.66, + "dir": "U" + }, + { + "t": 135.9, + "amp": 0.79, + "dir": "U" + }, + { + "t": 136.171, + "amp": 0.28, + "dir": "D" + }, + { + "t": 136.612, + "amp": 0.74, + "dir": "U" + }, + { + "t": 136.849, + "amp": 0.83, + "dir": "U" + }, + { + "t": 137.091, + "amp": 0.24, + "dir": "D" + }, + { + "t": 137.323, + "amp": 0.8, + "dir": "U" + }, + { + "t": 137.554, + "amp": 0.38, + "dir": "D" + }, + { + "t": 137.797, + "amp": 0.81, + "dir": "U" + }, + { + "t": 138.04, + "amp": 0.51, + "dir": "U" + }, + { + "t": 138.27, + "amp": 0.77, + "dir": "U" + }, + { + "t": 138.512, + "amp": 0.18, + "dir": "D" + }, + { + "t": 138.743, + "amp": 0.77, + "dir": "U" + }, + { + "t": 139.457, + "amp": 0.56, + "dir": "D" + }, + { + "t": 139.688, + "amp": 0.69, + "dir": "U" + }, + { + "t": 140.388, + "amp": 0.59, + "dir": "U" + }, + { + "t": 140.633, + "amp": 0.9, + "dir": "U" + }, + { + "t": 141.341, + "amp": 0.85, + "dir": "D" + }, + { + "t": 141.577, + "amp": 0.93, + "dir": "U" + }, + { + "t": 141.819, + "amp": 0.4, + "dir": "U" + }, + { + "t": 142.05, + "amp": 0.64, + "dir": "D" + }, + { + "t": 142.274, + "amp": 0.22, + "dir": "D" + }, + { + "t": 142.522, + "amp": 0.66, + "dir": "U" + }, + { + "t": 143.209, + "amp": 0.59, + "dir": "U" + }, + { + "t": 143.467, + "amp": 0.64, + "dir": "U" + }, + { + "t": 144.156, + "amp": 0.72, + "dir": "U" + }, + { + "t": 144.411, + "amp": 0.89, + "dir": "U" + }, + { + "t": 144.883, + "amp": 1.02, + "dir": "U" + }, + { + "t": 145.102, + "amp": 0.35, + "dir": "U" + }, + { + "t": 145.355, + "amp": 0.78, + "dir": "U" + }, + { + "t": 145.585, + "amp": 0.19, + "dir": "U" + }, + { + "t": 145.827, + "amp": 0.72, + "dir": "U" + }, + { + "t": 146.1, + "amp": 0.29, + "dir": "D" + }, + { + "t": 146.299, + "amp": 0.99, + "dir": "U" + }, + { + "t": 147.028, + "amp": 0.53, + "dir": "D" + }, + { + "t": 147.244, + "amp": 0.87, + "dir": "U" + }, + { + "t": 147.947, + "amp": 0.38, + "dir": "D" + }, + { + "t": 148.189, + "amp": 0.82, + "dir": "U" + }, + { + "t": 148.922, + "amp": 0.68, + "dir": "D" + }, + { + "t": 149.134, + "amp": 1, + "dir": "U" + }, + { + "t": 149.42, + "amp": 0.12, + "dir": "D" + }, + { + "t": 149.607, + "amp": 0.73, + "dir": "U" + }, + { + "t": 150.08, + "amp": 1.23, + "dir": "U" + }, + { + "t": 150.81, + "amp": 0.73, + "dir": "D" + }, + { + "t": 151.012, + "amp": 0.27, + "dir": "D" + }, + { + "t": 151.732, + "amp": 0.4, + "dir": "D" + }, + { + "t": 151.968, + "amp": 0.93, + "dir": "U" + }, + { + "t": 152.236, + "amp": 0.23, + "dir": "D" + }, + { + "t": 152.702, + "amp": 0.34, + "dir": "D" + }, + { + "t": 152.914, + "amp": 0.87, + "dir": "D" + }, + { + "t": 153.184, + "amp": 0.25, + "dir": "D" + }, + { + "t": 153.387, + "amp": 0.36, + "dir": "D" + }, + { + "t": 153.672, + "amp": 0.15, + "dir": "D" + }, + { + "t": 153.859, + "amp": 1.18, + "dir": "U" + }, + { + "t": 154.597, + "amp": 0.51, + "dir": "D" + }, + { + "t": 154.804, + "amp": 0.73, + "dir": "U" + }, + { + "t": 155.074, + "amp": 0.3, + "dir": "D" + }, + { + "t": 155.524, + "amp": 0.51, + "dir": "U" + }, + { + "t": 155.749, + "amp": 0.81, + "dir": "U" + }, + { + "t": 156.011, + "amp": 0.29, + "dir": "D" + }, + { + "t": 156.221, + "amp": 0.45, + "dir": "D" + }, + { + "t": 156.458, + "amp": 0.44, + "dir": "U" + }, + { + "t": 156.694, + "amp": 0.61, + "dir": "U" + }, + { + "t": 157.399, + "amp": 0.48, + "dir": "U" + }, + { + "t": 157.638, + "amp": 0.89, + "dir": "U" + }, + { + "t": 158.346, + "amp": 0.36, + "dir": "D" + }, + { + "t": 158.582, + "amp": 0.91, + "dir": "U" + }, + { + "t": 159.053, + "amp": 0.76, + "dir": "U" + }, + { + "t": 159.299, + "amp": 0.22, + "dir": "D" + }, + { + "t": 159.523, + "amp": 1.12, + "dir": "U" + }, + { + "t": 160.213, + "amp": 0.89, + "dir": "U" + }, + { + "t": 160.462, + "amp": 0.95, + "dir": "U" + }, + { + "t": 161.187, + "amp": 0.71, + "dir": "D" + }, + { + "t": 161.399, + "amp": 0.73, + "dir": "U" + }, + { + "t": 161.645, + "amp": 0.42, + "dir": "U" + }, + { + "t": 161.867, + "amp": 0.75, + "dir": "U" + }, + { + "t": 162.106, + "amp": 0.36, + "dir": "D" + }, + { + "t": 162.334, + "amp": 0.91, + "dir": "U" + }, + { + "t": 162.568, + "amp": 0.33, + "dir": "U" + }, + { + "t": 162.801, + "amp": 0.78, + "dir": "U" + }, + { + "t": 163.034, + "amp": 0.36, + "dir": "D" + }, + { + "t": 163.268, + "amp": 0.91, + "dir": "U" + }, + { + "t": 163.965, + "amp": 0.54, + "dir": "D" + }, + { + "t": 164.201, + "amp": 0.78, + "dir": "U" + }, + { + "t": 164.897, + "amp": 0.64, + "dir": "U" + }, + { + "t": 165.133, + "amp": 0.8, + "dir": "U" + }, + { + "t": 165.839, + "amp": 0.57, + "dir": "D" + }, + { + "t": 166.066, + "amp": 0.73, + "dir": "U" + }, + { + "t": 166.31, + "amp": 0.51, + "dir": "D" + }, + { + "t": 166.543, + "amp": 0.23, + "dir": "D" + }, + { + "t": 166.753, + "amp": 0.54, + "dir": "D" + }, + { + "t": 166.998, + "amp": 0.92, + "dir": "U" + }, + { + "t": 167.692, + "amp": 0.56, + "dir": "U" + }, + { + "t": 167.932, + "amp": 0.71, + "dir": "U" + }, + { + "t": 168.636, + "amp": 0.62, + "dir": "U" + }, + { + "t": 168.868, + "amp": 0.8, + "dir": "U" + }, + { + "t": 169.611, + "amp": 0.64, + "dir": "U" + }, + { + "t": 169.843, + "amp": 0.74, + "dir": "U" + }, + { + "t": 170.064, + "amp": 0.28, + "dir": "U" + }, + { + "t": 170.319, + "amp": 0.49, + "dir": "U" + }, + { + "t": 170.528, + "amp": 0.33, + "dir": "U" + }, + { + "t": 170.795, + "amp": 0.95, + "dir": "U" + }, + { + "t": 173.175, + "amp": 0.3, + "dir": "D" + }, + { + "t": 173.454, + "amp": 0.25, + "dir": "D" + } + ], + "regularized": { + "keep": 0.3, + "floorBar": 24, + "medianBpm": 120.2, + "totalSec": 173.545 + } +} \ No newline at end of file diff --git a/pop/sailor-song/platter-analysis.json b/pop/sailor-song/platter-analysis.json new file mode 100644 index 0000000000..dca180eccb --- /dev/null +++ b/pop/sailor-song/platter-analysis.json @@ -0,0 +1,2228 @@ +{ + "source": "measures.json + vox-notes.json + take.analysis.json", + "date": "2026-09-29", + "rhythm": { + "motif": { + "box": "x..xx..x", + "n": 8, + "k": 4, + "onsets": [ + 0, + 3, + 4, + 7 + ], + "ioi": [ + 3, + 1, + 3, + 1 + ], + "necklace": "00110011", + "bracelet": "00110011", + "euclidean": false, + "evenness": 0.9554096625192922, + "evennessIoi": 0.33333333333333337, + "ioiVariance": 1, + "vertexDistance": 2, + "balance": { + "magnitude": 1.1102230246251565e-16, + "normalized": 2.7755575615628914e-17, + "angle": 0, + "balanced": true + }, + "rhythmicOddity": false, + "intervalVector": [ + 0, + 2, + 0, + 2, + 2 + ], + "winogradDeep": false, + "erdosDeep": false, + "offbeatness": { + "count": 2, + "positions": [ + 3, + 7 + ], + "degenerate": false + }, + "syncopation": { + "lhl": 0, + "cScore": 0, + "noteToBeat": 0.5, + "offbeatness": { + "count": 2, + "positions": [ + 3, + 7 + ], + "degenerate": false + } + }, + "periodic": true, + "metricWeights": [ + 4, + 1, + 2, + 1, + 3, + 1, + 2, + 1 + ] + }, + "kitPattern": { + "box": "x..xx..x", + "note": "render.mjs kit: kick 1 and &2, snare 3, rim &4 — the SAME necklace as her hand; the kit doubles her, it does not interlock" + }, + "distances": { + "E(4,8) straight 8ths": { + "box": "x.x.x.x.", + "chronotonic": 1, + "rotation": 0, + "swap": 2 + }, + "tresillo E(3,8)": { + "box": "x..x..x.", + "chronotonic": 0.5, + "rotation": 0, + "swap": null + }, + "cinquillo": { + "box": "x.xx.xx.", + "chronotonic": 1, + "rotation": 1, + "swap": null + }, + "her complement (hocket partner)": { + "box": ".xx..xx.", + "chronotonic": 0, + "rotation": 2, + "swap": 0 + } + }, + "patternCounts": { + ".x.xxx..": 1, + "x.......": 1, + "........": 1, + "x.xxx.xx": 4, + "x..xx.xx": 9, + "x.x.x.xx": 1, + "...xx..x": 1, + "x..xx..x": 37, + "x..xx.x.": 4, + "x..xxxxx": 10, + "..x.x..x": 1, + "x..xx...": 2, + "xxxxx.x.": 1, + "xxx.xxxx": 1, + "x..xxx.x": 3, + "x.xxx...": 1, + "xxxxxxx.": 1, + "xx.xxxx.": 1, + "xxxxx..x": 1, + "xxxxxxxx": 1 + }, + "perBarDrift": [ + { + "n": 1, + "strum": ".x.xxx..", + "chord": "G#m", + "chronotonic": 1.25, + "extraOnsets": 0 + }, + { + "n": 2, + "strum": "x.......", + "chord": "G#m", + "chronotonic": 5.5, + "extraOnsets": -3 + }, + { + "n": 3, + "strum": "........", + "chord": "G#m", + "chronotonic": 2.5, + "extraOnsets": -4 + }, + { + "n": 4, + "strum": "x.xxx.xx", + "chord": "B", + "chronotonic": 1, + "extraOnsets": 2 + }, + { + "n": 5, + "strum": "x.xxx.xx", + "chord": "B", + "chronotonic": 1, + "extraOnsets": 2 + }, + { + "n": 6, + "strum": "x..xx.xx", + "chord": "G#m", + "chronotonic": 0.5, + "extraOnsets": 1 + }, + { + "n": 7, + "strum": "x..xx.xx", + "chord": "G#m", + "chronotonic": 0.5, + "extraOnsets": 1 + }, + { + "n": 8, + "strum": "x..xx.xx", + "chord": "B", + "chronotonic": 0.5, + "extraOnsets": 1 + }, + { + "n": 9, + "strum": "x..xx.xx", + "chord": "B", + "chronotonic": 0.5, + "extraOnsets": 1 + }, + { + "n": 10, + "strum": "x.x.x.xx", + "chord": "G#m", + "chronotonic": 1, + "extraOnsets": 1 + }, + { + "n": 11, + "strum": "x.xxx.xx", + "chord": "B", + "chronotonic": 1, + "extraOnsets": 2 + }, + { + "n": 13, + "strum": "...xx..x", + "chord": "B", + "chronotonic": 0.75, + "extraOnsets": -1 + }, + { + "n": 14, + "strum": "x..xx..x", + "chord": "G#m", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 15, + "strum": "x..xx..x", + "chord": "G#m", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 16, + "strum": "x..xx..x", + "chord": "B", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 17, + "strum": "x..xx.x.", + "chord": "B", + "chronotonic": 0.5, + "extraOnsets": 0 + }, + { + "n": 18, + "strum": "x..xx..x", + "chord": "Emaj7", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 19, + "strum": "x..xxxxx", + "chord": "B", + "chronotonic": 0.75, + "extraOnsets": 2 + }, + { + "n": 20, + "strum": "x..xx..x", + "chord": "G#m", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 21, + "strum": "x..xx..x", + "chord": "G#m", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 22, + "strum": "x..xx..x", + "chord": "B", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 23, + "strum": "x..xx..x", + "chord": "B", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 24, + "strum": "x..xx..x", + "chord": "B", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 25, + "strum": "x..xx.x.", + "chord": "B", + "chronotonic": 0.5, + "extraOnsets": 0 + }, + { + "n": 26, + "strum": "x..xx..x", + "chord": "B", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 28, + "strum": "x..xx..x", + "chord": "Emaj7", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 29, + "strum": "..x.x..x", + "chord": "Emaj7", + "chronotonic": 0.5, + "extraOnsets": -1 + }, + { + "n": 30, + "strum": "x..xx..x", + "chord": "G#m", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 31, + "strum": "x..xxxxx", + "chord": "G#m", + "chronotonic": 0.75, + "extraOnsets": 2 + }, + { + "n": 32, + "strum": "x..xx..x", + "chord": "B", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 33, + "strum": "x..xx...", + "chord": "B", + "chronotonic": 0.75, + "extraOnsets": -1 + }, + { + "n": 34, + "strum": "x..xx..x", + "chord": "B", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 35, + "strum": "x..xx.xx", + "chord": "G#m", + "chronotonic": 0.5, + "extraOnsets": 1 + }, + { + "n": 36, + "strum": "x..xx..x", + "chord": "G#m", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 37, + "strum": "x..xxxxx", + "chord": "G#m", + "chronotonic": 0.75, + "extraOnsets": 2 + }, + { + "n": 38, + "strum": "x..xx..x", + "chord": "B", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 39, + "strum": "x..xx.xx", + "chord": "B", + "chronotonic": 0.5, + "extraOnsets": 1 + }, + { + "n": 40, + "strum": "x..xx..x", + "chord": "B", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 41, + "strum": "x..xx..x", + "chord": "Emaj7", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 42, + "strum": "xxxxx.x.", + "chord": "B", + "chronotonic": 1.25, + "extraOnsets": 2 + }, + { + "n": 43, + "strum": "xxx.xxxx", + "chord": "G#m", + "chronotonic": 1.5, + "extraOnsets": 3 + }, + { + "n": 44, + "strum": "x..xx..x", + "chord": "G#m", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 45, + "strum": "x..xx..x", + "chord": "G#m", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 46, + "strum": "x..xx..x", + "chord": "B", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 47, + "strum": "x..xx..x", + "chord": "B", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 48, + "strum": "x..xx..x", + "chord": "G#m", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 49, + "strum": "x..xx.x.", + "chord": "Emaj7", + "chronotonic": 0.5, + "extraOnsets": 0 + }, + { + "n": 50, + "strum": "x..xx..x", + "chord": "G#m", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 51, + "strum": "x..xx.xx", + "chord": "G#m", + "chronotonic": 0.5, + "extraOnsets": 1 + }, + { + "n": 52, + "strum": "x..xx..x", + "chord": "B", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 53, + "strum": "x..xxx.x", + "chord": "B", + "chronotonic": 0.5, + "extraOnsets": 1 + }, + { + "n": 54, + "strum": "x..xx...", + "chord": "B", + "chronotonic": 0.75, + "extraOnsets": -1 + }, + { + "n": 55, + "strum": "x..xxxxx", + "chord": "G#m", + "chronotonic": 0.75, + "extraOnsets": 2 + }, + { + "n": 56, + "strum": "x..xx..x", + "chord": "G#m", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 57, + "strum": "x..xxxxx", + "chord": "G#m", + "chronotonic": 0.75, + "extraOnsets": 2 + }, + { + "n": 58, + "strum": "x..xx..x", + "chord": "B", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 59, + "strum": "x..xxxxx", + "chord": "B", + "chronotonic": 0.75, + "extraOnsets": 2 + }, + { + "n": 60, + "strum": "x..xx..x", + "chord": "B", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 61, + "strum": "x..xxxxx", + "chord": "B", + "chronotonic": 0.75, + "extraOnsets": 2 + }, + { + "n": 62, + "strum": "x..xx..x", + "chord": "G#m", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 63, + "strum": "x..xx.xx", + "chord": "G#m", + "chronotonic": 0.5, + "extraOnsets": 1 + }, + { + "n": 64, + "strum": "x..xx..x", + "chord": "B", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 65, + "strum": "x.xxx...", + "chord": "B", + "chronotonic": 1.25, + "extraOnsets": 0 + }, + { + "n": 66, + "strum": "x..xxxxx", + "chord": "B", + "chronotonic": 0.75, + "extraOnsets": 2 + }, + { + "n": 67, + "strum": "x..xxx.x", + "chord": "G#m", + "chronotonic": 0.5, + "extraOnsets": 1 + }, + { + "n": 68, + "strum": "xxxxxxx.", + "chord": "G#m", + "chronotonic": 1.5, + "extraOnsets": 3 + }, + { + "n": 69, + "strum": "x..xx..x", + "chord": "G#m", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 70, + "strum": "x..xxxxx", + "chord": "G#m", + "chronotonic": 0.75, + "extraOnsets": 2 + }, + { + "n": 71, + "strum": "x..xx..x", + "chord": "B", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 72, + "strum": "x.xxx.xx", + "chord": "B", + "chronotonic": 1, + "extraOnsets": 2 + }, + { + "n": 73, + "strum": "x..xx..x", + "chord": "Emaj7", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 74, + "strum": "x..xx.x.", + "chord": "B", + "chronotonic": 0.5, + "extraOnsets": 0 + }, + { + "n": 75, + "strum": "x..xx..x", + "chord": "G#m", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 76, + "strum": "xx.xxxx.", + "chord": "G#m", + "chronotonic": 1.25, + "extraOnsets": 2 + }, + { + "n": 77, + "strum": "x..xxx.x", + "chord": "B", + "chronotonic": 0.5, + "extraOnsets": 1 + }, + { + "n": 78, + "strum": "xxxxx..x", + "chord": "B", + "chronotonic": 0.75, + "extraOnsets": 2 + }, + { + "n": 79, + "strum": "x..xx.xx", + "chord": "B", + "chronotonic": 0.5, + "extraOnsets": 1 + }, + { + "n": 80, + "strum": "x..xx..x", + "chord": "Emaj7", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 81, + "strum": "xxxxxxxx", + "chord": "G#m", + "chronotonic": 1.5, + "extraOnsets": 4 + }, + { + "n": 82, + "strum": "x..xx..x", + "chord": "G#m", + "chronotonic": 0, + "extraOnsets": 0 + }, + { + "n": 83, + "strum": "x..xxxxx", + "chord": "G#m", + "chronotonic": 0.75, + "extraOnsets": 2 + }, + { + "n": 84, + "strum": "x..xx..x", + "chord": "B", + "chronotonic": 0, + "extraOnsets": 0 + } + ], + "driftBySection": { + "intro": { + "bars": 10, + "meanChronotonic": 1.425, + "meanExtraOnsets": 0.2, + "verbatim": 0 + }, + "verse1": { + "bars": 15, + "meanChronotonic": 0.233, + "meanExtraOnsets": 0.2, + "verbatim": 10 + }, + "chorus1": { + "bars": 16, + "meanChronotonic": 0.406, + "meanExtraOnsets": 0.563, + "verbatim": 8 + }, + "verse2": { + "bars": 8, + "meanChronotonic": 0.125, + "meanExtraOnsets": 0.125, + "verbatim": 6 + }, + "chorus2": { + "bars": 16, + "meanChronotonic": 0.453, + "meanExtraOnsets": 0.75, + "verbatim": 6 + }, + "break": { + "bars": 4, + "meanChronotonic": 0.563, + "meanExtraOnsets": 1.25, + "verbatim": 2 + }, + "bridge": { + "bars": 9, + "meanChronotonic": 0.5, + "meanExtraOnsets": 0.889, + "verbatim": 3 + }, + "outro": { + "bars": 4, + "meanChronotonic": 0.563, + "meanExtraOnsets": 1.5, + "verbatim": 2 + } + }, + "morphToStraight": [ + "x..xx..x", + "x.x.x..x", + "x.x.x.x." + ] + }, + "beatBins": { + "offbeatStrums": 149, + "offbeatPhaseMedian": 0.511, + "offbeatPhaseP10": 0.436, + "offbeatPhaseP90": 0.578, + "swingRatio": 1.045, + "offbeatLateMsAt120": 5.5, + "voiceVsGrid": { + "all": { + "signedMedianMs": 16, + "absMedianMs": 41 + }, + "bySection": { + "verse1": { + "notes": 65, + "signedMedianMs": 38, + "absMedianMs": 49, + "lateShare": 0.646 + }, + "chorus1": { + "notes": 70, + "signedMedianMs": 16, + "absMedianMs": 36, + "lateShare": 0.514 + }, + "verse2": { + "notes": 38, + "signedMedianMs": 13, + "absMedianMs": 30, + "lateShare": 0.421 + }, + "chorus2": { + "notes": 76, + "signedMedianMs": 5, + "absMedianMs": 52, + "lateShare": 0.408 + }, + "bridge": { + "notes": 29, + "signedMedianMs": 9, + "absMedianMs": 35, + "lateShare": 0.483 + } + }, + "longNotesSignedMs": 7, + "shortNotesSignedMs": 29, + "afterLockVox": "lock-vox.mjs pulls these onto the grid: abs median 41 → 8 ms (its own readout)" + }, + "note": "digest 07 (Danielsen): a beat is a bin, not a point. Her & sits late of the mathematical 8th; that offset is the groove and must live in a separate microtiming layer, never folded into onsets" + }, + "entrainment": { + "medianBpm": 120.9, + "periodsMs": { + "eighth": 248.1, + "quarter": 496.3, + "half": 992.6, + "bar": 1985.1, + "chordCycle2bars": 3970.2 + }, + "window": { + "floorMs": 100, + "ceilingMs": 2000, + "sweetSpotMs": [ + 500, + 700 + ] + }, + "felt": { + "quarterInSweetSpot": false, + "quarterMsFromSweetSpot": -3.7, + "halfInWindow": true, + "barAtCeiling": true, + "chordCycleFelt": false + }, + "bpmBySection": { + "intro": { + "median": 125, + "min": 120.9, + "max": 127.6, + "start": 5, + "end": 24.3 + }, + "verse1": { + "median": 113.6, + "min": 109.4, + "max": 123.8, + "start": 24.3, + "end": 61.2 + }, + "chorus1": { + "median": 119.5, + "min": 113.8, + "max": 121.6, + "start": 61.2, + "end": 93.3 + }, + "verse2": { + "median": 114.8, + "min": 113, + "max": 117.5, + "start": 93.3, + "end": 110.1 + }, + "chorus2": { + "median": 123, + "min": 116.8, + "max": 125.3, + "start": 110.1, + "end": 141.5 + }, + "break": { + "median": 123, + "min": 120.9, + "max": 123.8, + "start": 141.5, + "end": 149.4 + }, + "bridge": { + "median": 121.6, + "min": 120.9, + "max": 125.3, + "start": 149.4, + "end": 167 + }, + "outro": { + "median": 130, + "min": 123.8, + "max": 130.8, + "start": 167, + "end": 174.5 + } + }, + "tempoArch": { + "verse1Sag": -7.3, + "chorus1Lift": -1.4, + "bridge": 0.7 + } + }, + "harmony": { + "key": { + "key": "G# minor", + "r": 0.814 + }, + "tuningCents": 13, + "vocabulary": { + "G#m": "vi (the Cmaj7 shape through the open low string: Emaj7/G#)", + "B": "I", + "Emaj7": "IV" + }, + "barCounts": { + "G#m": 35, + "B": 42, + "Emaj7": 7 + }, + "runs": [ + { + "chord": "G#m", + "from": 1, + "to": 3, + "bars": 3 + }, + { + "chord": "B", + "from": 4, + "to": 5, + "bars": 2 + }, + { + "chord": "G#m", + "from": 6, + "to": 7, + "bars": 2 + }, + { + "chord": "B", + "from": 8, + "to": 9, + "bars": 2 + }, + { + "chord": "G#m", + "from": 10, + "to": 10, + "bars": 1 + }, + { + "chord": "B", + "from": 11, + "to": 13, + "bars": 3 + }, + { + "chord": "G#m", + "from": 14, + "to": 15, + "bars": 2 + }, + { + "chord": "B", + "from": 16, + "to": 17, + "bars": 2 + }, + { + "chord": "Emaj7", + "from": 18, + "to": 18, + "bars": 1 + }, + { + "chord": "B", + "from": 19, + "to": 19, + "bars": 1 + }, + { + "chord": "G#m", + "from": 20, + "to": 21, + "bars": 2 + }, + { + "chord": "B", + "from": 22, + "to": 27, + "bars": 6 + }, + { + "chord": "Emaj7", + "from": 28, + "to": 29, + "bars": 2 + }, + { + "chord": "G#m", + "from": 30, + "to": 31, + "bars": 2 + }, + { + "chord": "B", + "from": 32, + "to": 34, + "bars": 3 + }, + { + "chord": "G#m", + "from": 35, + "to": 37, + "bars": 3 + }, + { + "chord": "B", + "from": 38, + "to": 40, + "bars": 3 + }, + { + "chord": "Emaj7", + "from": 41, + "to": 41, + "bars": 1 + }, + { + "chord": "B", + "from": 42, + "to": 42, + "bars": 1 + }, + { + "chord": "G#m", + "from": 43, + "to": 45, + "bars": 3 + }, + { + "chord": "B", + "from": 46, + "to": 47, + "bars": 2 + }, + { + "chord": "G#m", + "from": 48, + "to": 48, + "bars": 1 + }, + { + "chord": "Emaj7", + "from": 49, + "to": 49, + "bars": 1 + }, + { + "chord": "G#m", + "from": 50, + "to": 51, + "bars": 2 + }, + { + "chord": "B", + "from": 52, + "to": 54, + "bars": 3 + }, + { + "chord": "G#m", + "from": 55, + "to": 57, + "bars": 3 + }, + { + "chord": "B", + "from": 58, + "to": 61, + "bars": 4 + }, + { + "chord": "G#m", + "from": 62, + "to": 63, + "bars": 2 + }, + { + "chord": "B", + "from": 64, + "to": 66, + "bars": 3 + }, + { + "chord": "G#m", + "from": 67, + "to": 70, + "bars": 4 + }, + { + "chord": "B", + "from": 71, + "to": 72, + "bars": 2 + }, + { + "chord": "Emaj7", + "from": 73, + "to": 73, + "bars": 1 + }, + { + "chord": "B", + "from": 74, + "to": 74, + "bars": 1 + }, + { + "chord": "G#m", + "from": 75, + "to": 76, + "bars": 2 + }, + { + "chord": "B", + "from": 77, + "to": 79, + "bars": 3 + }, + { + "chord": "Emaj7", + "from": 80, + "to": 80, + "bars": 1 + }, + { + "chord": "G#m", + "from": 81, + "to": 83, + "bars": 3 + }, + { + "chord": "B", + "from": 84, + "to": 84, + "bars": 1 + } + ], + "runLengths": { + "G#m": { + "runs": 15, + "median": 2, + "max": 4 + }, + "B": { + "runs": 17, + "median": 2, + "max": 6 + }, + "Emaj7": { + "runs": 6, + "median": 1, + "max": 2 + } + }, + "harmonicRhythmBeats": 8.8, + "ivBars": [ + "18:verse1", + "28:chorus1", + "29:chorus1", + "41:chorus1", + "49:verse2", + "73:bridge", + "80:bridge" + ] + }, + "melody": { + "notes": 279, + "tonic": "G#3 = midi 56 (guitar frame)", + "range": { + "lo": 52, + "hi": 68 + }, + "degreeShareByDuration": { + "1": 0.073, + "2": 0.025, + "4": 0.075, + "5": 0.34, + "b3": 0.219, + "b6": 0.138, + "b7": 0.13 + }, + "motion": { + "steps": 157, + "leaps": 110, + "repeats": 8, + "stepShare": 0.571, + "intervalHist": { + "0": 8, + "1": 17, + "2": 49, + "3": 10, + "4": 6, + "5": 19, + "6": 1, + "7": 10, + "8": 4, + "9": 4, + "-8": 5, + "-2": 60, + "-4": 25, + "-5": 9, + "-10": 1, + "-1": 31, + "-3": 13, + "-7": 2, + "-9": 1 + } + }, + "phrases": [ + { + "start": 26.3, + "bar": 11, + "section": "verse1", + "beatIn": 4, + "offsetMs": 368, + "beats": 17.1, + "notes": 15, + "lo": 56, + "hi": 68, + "first": 64, + "last": 66, + "degreeLast": "b7", + "contour": "rise" + }, + { + "start": 35.1, + "bar": 16, + "section": "verse1", + "beatIn": 1, + "offsetMs": 43, + "beats": 3.6, + "notes": 4, + "lo": 58, + "hi": 63, + "first": 58, + "last": 63, + "degreeLast": "5", + "contour": "rise" + }, + { + "start": 37.3, + "bar": 17, + "section": "verse1", + "beatIn": 1, + "offsetMs": 22, + "beats": 7.4, + "notes": 8, + "lo": 59, + "hi": 68, + "first": 59, + "last": 63, + "degreeLast": "5", + "contour": "rise" + }, + { + "start": 41.3, + "bar": 18, + "section": "verse1", + "beatIn": 4, + "offsetMs": 295, + "beats": 6, + "notes": 6, + "lo": 56, + "hi": 68, + "first": 68, + "last": 56, + "degreeLast": "1", + "contour": "fall" + }, + { + "start": 45.6, + "bar": 20, + "section": "verse1", + "beatIn": 4, + "offsetMs": 294, + "beats": 4.1, + "notes": 4, + "lo": 59, + "hi": 63, + "first": 61, + "last": 63, + "degreeLast": "5", + "contour": "rise" + }, + { + "start": 48, + "bar": 22, + "section": "verse1", + "beatIn": 1, + "offsetMs": 21, + "beats": 3.6, + "notes": 3, + "lo": 59, + "hi": 63, + "first": 59, + "last": 63, + "degreeLast": "5", + "contour": "rise" + }, + { + "start": 50.1, + "bar": 23, + "section": "verse1", + "beatIn": 1, + "offsetMs": 28, + "beats": 16, + "notes": 12, + "lo": 56, + "hi": 68, + "first": 59, + "last": 63, + "degreeLast": "5", + "contour": "rise" + }, + { + "start": 58.4, + "bar": 26, + "section": "verse1", + "beatIn": 4, + "offsetMs": 315, + "beats": 4, + "notes": 6, + "lo": 59, + "hi": 68, + "first": 68, + "last": 59, + "degreeLast": "b3", + "contour": "fall" + }, + { + "start": 61.2, + "bar": 28, + "section": "chorus1", + "beatIn": 1, + "offsetMs": 57, + "beats": 2.7, + "notes": 2, + "lo": 64, + "hi": 66, + "first": 66, + "last": 64, + "degreeLast": "b6", + "contour": "fall" + }, + { + "start": 63, + "bar": 28, + "section": "chorus1", + "beatIn": 4, + "offsetMs": 193, + "beats": 7.6, + "notes": 11, + "lo": 56, + "hi": 66, + "first": 63, + "last": 63, + "degreeLast": "5", + "contour": "level" + }, + { + "start": 67.1, + "bar": 30, + "section": "chorus1", + "beatIn": 4, + "offsetMs": 344, + "beats": 11.3, + "notes": 14, + "lo": 58, + "hi": 66, + "first": 66, + "last": 59, + "degreeLast": "b3", + "contour": "fall" + }, + { + "start": 73.1, + "bar": 33, + "section": "chorus1", + "beatIn": 4, + "offsetMs": 278, + "beats": 23.6, + "notes": 27, + "lo": 59, + "hi": 66, + "first": 59, + "last": 63, + "degreeLast": "5", + "contour": "rise" + }, + { + "start": 85.2, + "bar": 39, + "section": "chorus1", + "beatIn": 4, + "offsetMs": 293, + "beats": 15.3, + "notes": 11, + "lo": 61, + "hi": 68, + "first": 61, + "last": 63, + "degreeLast": "5", + "contour": "rise" + }, + { + "start": 93.1, + "bar": 43, + "section": "chorus1", + "beatIn": 4, + "offsetMs": 257, + "beats": 16.7, + "notes": 17, + "lo": 56, + "hi": 68, + "first": 56, + "last": 64, + "degreeLast": "b6", + "contour": "rise" + }, + { + "start": 101.8, + "bar": 47, + "section": "verse2", + "beatIn": 4, + "offsetMs": 509, + "beats": 4, + "notes": 5, + "lo": 56, + "hi": 61, + "first": 56, + "last": 61, + "degreeLast": "4", + "contour": "rise" + }, + { + "start": 104.7, + "bar": 49, + "section": "verse2", + "beatIn": 3, + "offsetMs": -208, + "beats": 10, + "notes": 12, + "lo": 61, + "hi": 68, + "first": 61, + "last": 61, + "degreeLast": "4", + "contour": "level" + }, + { + "start": 110.1, + "bar": 52, + "section": "chorus2", + "beatIn": 1, + "offsetMs": 67, + "beats": 2.6, + "notes": 3, + "lo": 63, + "hi": 66, + "first": 66, + "last": 63, + "degreeLast": "5", + "contour": "fall" + }, + { + "start": 111.8, + "bar": 52, + "section": "chorus2", + "beatIn": 4, + "offsetMs": 202, + "beats": 3.4, + "notes": 6, + "lo": 59, + "hi": 66, + "first": 63, + "last": 59, + "degreeLast": "b3", + "contour": "fall" + }, + { + "start": 113.8, + "bar": 53, + "section": "chorus2", + "beatIn": 4, + "offsetMs": 296, + "beats": 1.8, + "notes": 3, + "lo": 59, + "hi": 66, + "first": 59, + "last": 66, + "degreeLast": "b7", + "contour": "rise" + }, + { + "start": 115.1, + "bar": 54, + "section": "chorus2", + "beatIn": 3, + "offsetMs": 63, + "beats": 0.7, + "notes": 1, + "lo": 63, + "hi": 63, + "first": 63, + "last": 63, + "degreeLast": "5", + "contour": "one" + }, + { + "start": 115.8, + "bar": 54, + "section": "chorus2", + "beatIn": 4, + "offsetMs": 330, + "beats": 14.8, + "notes": 18, + "lo": 58, + "hi": 66, + "first": 66, + "last": 63, + "degreeLast": "5", + "contour": "fall" + }, + { + "start": 123.6, + "bar": 58, + "section": "chorus2", + "beatIn": 4, + "offsetMs": 299, + "beats": 1.5, + "notes": 2, + "lo": 64, + "hi": 66, + "first": 66, + "last": 64, + "degreeLast": "b6", + "contour": "fall" + }, + { + "start": 124.7, + "bar": 59, + "section": "chorus2", + "beatIn": 3, + "offsetMs": -54, + "beats": 16.6, + "notes": 24, + "lo": 52, + "hi": 68, + "first": 63, + "last": 63, + "degreeLast": "5", + "contour": "level" + }, + { + "start": 133.4, + "bar": 63, + "section": "chorus2", + "beatIn": 4, + "offsetMs": 265, + "beats": 14.4, + "notes": 10, + "lo": 59, + "hi": 66, + "first": 59, + "last": 63, + "degreeLast": "5", + "contour": "rise" + }, + { + "start": 151.1, + "bar": 72, + "section": "bridge", + "beatIn": 4, + "offsetMs": 268, + "beats": 3.2, + "notes": 4, + "lo": 58, + "hi": 66, + "first": 58, + "last": 63, + "degreeLast": "5", + "contour": "rise" + }, + { + "start": 153.1, + "bar": 73, + "section": "bridge", + "beatIn": 4, + "offsetMs": 286, + "beats": 3.4, + "notes": 3, + "lo": 63, + "hi": 66, + "first": 66, + "last": 63, + "degreeLast": "5", + "contour": "fall" + }, + { + "start": 155.3, + "bar": 75, + "section": "bridge", + "beatIn": 1, + "offsetMs": 35, + "beats": 6.6, + "notes": 9, + "lo": 59, + "hi": 64, + "first": 63, + "last": 63, + "degreeLast": "5", + "contour": "level" + }, + { + "start": 159.2, + "bar": 77, + "section": "bridge", + "beatIn": 1, + "offsetMs": 20, + "beats": 2.6, + "notes": 4, + "lo": 63, + "hi": 66, + "first": 64, + "last": 63, + "degreeLast": "5", + "contour": "fall" + }, + { + "start": 160.9, + "bar": 77, + "section": "bridge", + "beatIn": 4, + "offsetMs": 321, + "beats": 12.1, + "notes": 5, + "lo": 59, + "hi": 66, + "first": 66, + "last": 59, + "degreeLast": "b3", + "contour": "fall" + } + ], + "phraseBeats": { + "median": 6, + "p90": 16.7 + }, + "cadenceDegrees": { + "1": 1, + "4": 2, + "5": 17, + "b7": 2, + "b3": 4, + "b6": 3 + }, + "registerBySection": { + "intro": { + "notes": 1, + "medianMidi": 68, + "lo": 68, + "hi": 68, + "meanDurMs": 235 + }, + "verse1": { + "notes": 65, + "medianMidi": 61, + "lo": 56, + "hi": 68, + "meanDurMs": 513.5 + }, + "chorus1": { + "notes": 70, + "medianMidi": 63, + "lo": 52, + "hi": 68, + "meanDurMs": 445.5 + }, + "verse2": { + "notes": 38, + "medianMidi": 61, + "lo": 56, + "hi": 68, + "meanDurMs": 424.9 + }, + "chorus2": { + "notes": 76, + "medianMidi": 63, + "lo": 52, + "hi": 68, + "meanDurMs": 382.8 + }, + "break": null, + "bridge": { + "notes": 29, + "medianMidi": 63, + "lo": 58, + "hi": 66, + "meanDurMs": 519.5 + }, + "outro": null + }, + "topDegree4grams": [ + [ + "b7 b6 5 b3", + 12 + ], + [ + "5 1 b7 1", + 6 + ], + [ + "1 b7 1 5", + 5 + ], + [ + "b6 b7 b6 5", + 5 + ], + [ + "5 b6 5 b3", + 5 + ], + [ + "b3 4 b7 b6", + 4 + ] + ], + "phrasesBySection": { + "verse1": 8, + "chorus1": 6, + "verse2": 2, + "chorus2": 8, + "bridge": 5 + }, + "chordToneShare": 0.687, + "nonChordToneByChord": { + "B": 20.9, + "G#m": 15.2, + "Emaj7": 3.1 + }, + "harmonyStemChordToneShare": { + "shift+2": 0.546, + "shift-2": 0.604, + "shift+4": 0.348, + "shift-5": 0.546, + "shift-7": 0.687 + } + }, + "form": { + "durationS": 179.14, + "bars": 84, + "sections": [ + { + "section": "intro", + "bars": 10, + "from": 1, + "to": 10, + "start": 5, + "end": 24.3, + "frac": 0.028 + }, + { + "section": "verse1", + "bars": 17, + "from": 11, + "to": 27, + "start": 24.3, + "end": 61.2, + "frac": 0.136 + }, + { + "section": "chorus1", + "bars": 16, + "from": 28, + "to": 43, + "start": 61.2, + "end": 93.3, + "frac": 0.341 + }, + { + "section": "verse2", + "bars": 8, + "from": 44, + "to": 51, + "start": 93.3, + "end": 110.1, + "frac": 0.521 + }, + { + "section": "chorus2", + "bars": 16, + "from": 52, + "to": 67, + "start": 110.1, + "end": 141.5, + "frac": 0.614 + }, + { + "section": "break", + "bars": 4, + "from": 68, + "to": 71, + "start": 141.5, + "end": 149.4, + "frac": 0.79 + }, + { + "section": "bridge", + "bars": 9, + "from": 72, + "to": 80, + "start": 149.4, + "end": 167, + "frac": 0.834 + }, + { + "section": "outro", + "bars": 4, + "from": 81, + "to": 84, + "start": 167, + "end": 174.5, + "frac": 0.932 + } + ], + "phraseLengthCheck": [ + { + "section": "intro", + "bars": 10, + "inR1Set": false + }, + { + "section": "verse1", + "bars": 17, + "inR1Set": false + }, + { + "section": "chorus1", + "bars": 16, + "inR1Set": true + }, + { + "section": "verse2", + "bars": 8, + "inR1Set": true + }, + { + "section": "chorus2", + "bars": 16, + "inR1Set": true + }, + { + "section": "break", + "bars": 4, + "inR1Set": false + }, + { + "section": "bridge", + "bars": 9, + "inR1Set": false + }, + { + "section": "outro", + "bars": 4, + "inR1Set": false + } + ], + "rules": { + "R5climaxAt": { + "frac": 0.71, + "t": 127.2, + "bar": 60, + "section": "chorus2" + }, + "R6stillnessAt": { + "frac": 0.36, + "t": 64.5, + "bar": 29, + "section": "chorus1" + }, + "popBuildLayerEntries": [ + { + "frac": 0.1, + "bar": 9, + "section": "intro" + }, + { + "frac": 0.28, + "bar": 24, + "section": "verse1" + }, + { + "frac": 0.4, + "bar": 34, + "section": "chorus1" + }, + { + "frac": 0.62, + "bar": 52, + "section": "chorus2" + }, + { + "frac": 0.78, + "bar": 66, + "section": "chorus2" + } + ] + }, + "measured": { + "voxDbBySection": { + "intro": -76.5, + "verse1": -24.1, + "chorus1": -22.1, + "verse2": -21, + "chorus2": -24, + "break": -76.6, + "bridge": -22.8, + "outro": -73.8 + }, + "gtrDbBySection": { + "intro": -21.5, + "verse1": -20.2, + "chorus1": -19.9, + "verse2": -20.7, + "chorus2": -20, + "break": -17.3, + "bridge": -19.6, + "outro": -17.3 + }, + "loudestVoxBars": [ + { + "bar": 15, + "section": "verse1", + "t": 33, + "dB": -16.5 + }, + { + "bar": 22, + "section": "verse1", + "t": 47.9, + "dB": -16.5 + }, + { + "bar": 18, + "section": "verse1", + "t": 39.4, + "dB": -16.9 + }, + { + "bar": 47, + "section": "verse2", + "t": 99.7, + "dB": -16.9 + }, + { + "bar": 19, + "section": "verse1", + "t": 41.6, + "dB": -17.6 + }, + { + "bar": 50, + "section": "verse2", + "t": 106, + "dB": -17.8 + }, + { + "bar": 80, + "section": "bridge", + "t": 165, + "dB": -18.3 + }, + { + "bar": 45, + "section": "verse2", + "t": 95.5, + "dB": -19 + } + ], + "herClimax": { + "bar": 15, + "t": 33, + "frac": 0.184, + "section": "verse1", + "plateauBars": [ + 18, + 19 + ] + }, + "voxRmsPerBar": [ + -79.3, + -83.3, + -83.1, + -75.3, + -74.3, + -74.9, + -75.9, + -76.2, + -75.4, + -67.8, + -43.7, + -27.3, + -19.3, + -24.3, + -16.5, + -25, + -23.9, + -16.9, + -17.6, + -29.1, + -26.6, + -16.5, + -26.2, + -26.9, + -20.8, + -25.1, + -23.6, + -19.6, + -20.5, + -22.1, + -20.9, + -19.4, + -22.8, + -20.6, + -21.8, + -23.1, + -20.3, + -28, + -25.3, + -24.9, + -20.5, + -20.6, + -23.4, + -21.3, + -19, + -20.6, + -16.9, + -27.1, + -23.9, + -17.8, + -21.1, + -22.1, + -23.3, + -26.4, + -22.3, + -20.8, + -23.6, + -24.8, + -27.7, + -24, + -20.9, + -27, + -24.9, + -27.3, + -22.4, + -21.5, + -25.3, + -76.1, + -77.1, + -77.2, + -76.2, + -36.5, + -20, + -19, + -23.4, + -23.5, + -21.9, + -21.6, + -21.4, + -18.3, + -64.5, + -77.5, + -76.9, + -76.4 + ], + "gtrRmsPerBar": [ + -21.2, + -32.6, + -47.7, + -14.5, + -14.3, + -14.5, + -14.1, + -18.9, + -18.5, + -18.3, + -18.1, + -19.9, + -22.5, + -15.7, + -17.1, + -23.5, + -22.4, + -21.8, + -19.9, + -17.8, + -17.6, + -21.9, + -22.8, + -20.7, + -19.7, + -21.6, + -21.2, + -18.4, + -21.6, + -16.6, + -17.2, + -21.5, + -21.3, + -20.5, + -20.1, + -18.1, + -17.1, + -22.5, + -21.5, + -20.6, + -19.8, + -22.3, + -19.3, + -17.8, + -18.9, + -22.4, + -24.9, + -23.1, + -21.9, + -18.5, + -18.3, + -22.8, + -22.9, + -22, + -19.2, + -18.8, + -17.7, + -20.9, + -20.7, + -20.3, + -18.1, + -17.8, + -17.4, + -21, + -21.7, + -20.1, + -19, + -17.4, + -16.2, + -15.2, + -20.3, + -18.6, + -19.3, + -20.3, + -18, + -17.2, + -20.9, + -21, + -20.7, + -20.2, + -17, + -16.6, + -15.9, + -19.9 + ] + } + }, + "cut": { + "rule": "chamber-04 form facts: tracks run about 1:30; wannadash went 3:28 → 1:54 by removing repeated statements, seams on downbeats with 10 ms fades", + "keepBars": [ + [ + 7, + 43 + ], + [ + 68, + 84 + ] + ], + "dropBars": [ + [ + 1, + 6 + ], + [ + 44, + 67 + ] + ], + "lengthS": 109.7, + "lengthMMSS": "1:50", + "seams": [ + { + "bar": 68, + "t": 141.514, + "chord": "G#m", + "prevChord": "G#m" + } + ], + "note": "verse2 + chorus2 restate verse1 + chorus1 on the same chords; the break (68–71) then lands right after chorus1 — check the lyric carries" + }, + "bass": { + "tempoBand": "dub techno 110–125 (R12)", + "subRoots": { + "G#1 (midi 32)": 52.3, + "B1 (midi 35)": 62.2, + "E1 (midi 28)": 41.5, + "E2 (midi 40)": 83 + }, + "R1": "sub 30–60 Hz octave (C1–C2), mono, sine plus 2nd/3rd harmonic; B1 at 62 Hz sits at the top of the band — E1 and G#1 are the sub's home", + "R3": "kit is kick 1 / snare 3 at 120.9: the dubstep half-time layout; a one-drop variant leaves beat 1 empty and puts kick + cross-stick on 3", + "R4": "first bass entry late: at least one 8-bar phrase of drums and space before it (her intro is 10 bars — the sub can wait until bar 11)", + "R5": { + "delayMs": 461.5, + "repeats": "4–5", + "note": "0.93 of a beat, so it revolves around her beat rather than locking; in follow mode compute it per bar from measures.json" + }, + "R8": "duck the sub 2–3 dB per kick, fastest attack; her guitar's low G#2 (104 Hz) and B2 (124 Hz) share the kick body band 100–200 Hz — high-pass the guitar or thin the kick body", + "R9": "mono below 100 Hz; stereo only on returns and on the sines above 100 Hz", + "R10": "a harmonic copy of the sub high-passed at 150 Hz carrying 250–700 Hz so the note survives a phone", + "phoneBand": "MASTERING.md: loudest 8 s may lose ≤ 5 dB through the mono 180 Hz–8 kHz proxy" + } +} \ No newline at end of file diff --git a/pop/sailor-song/take.analysis.json b/pop/sailor-song/take.analysis.json new file mode 100644 index 0000000000..f1d476316d --- /dev/null +++ b/pop/sailor-song/take.analysis.json @@ -0,0 +1,2458 @@ +{ + "source": "pop/sailor-song/src/take.wav", + "duration": 179.14, + "tuningCents": 13.0, + "pulse": { + "tempoBPM": 123.05, + "doubleTimeBPM": 246.09, + "beats": [ + 3.622, + 4.087, + 4.551, + 5.016, + 5.503, + 5.991, + 6.478, + 6.966, + 7.477, + 7.988, + 8.475, + 8.963, + 9.451, + 9.915, + 10.403, + 10.867, + 11.331, + 11.819, + 12.353, + 12.794, + 13.282, + 13.746, + 14.211, + 14.698, + 15.209, + 15.673, + 16.208, + 16.672, + 17.16, + 17.624, + 18.065, + 18.553, + 19.04, + 19.551, + 20.039, + 20.48, + 20.944, + 21.432, + 21.896, + 22.361, + 22.918, + 23.382, + 23.893, + 24.358, + 24.915, + 25.403, + 25.937, + 26.424, + 26.912, + 27.4, + 27.864, + 28.305, + 28.77, + 29.28, + 29.814, + 30.348, + 30.883, + 31.417, + 31.974, + 32.508, + 33.019, + 33.553, + 34.064, + 34.598, + 35.132, + 35.666, + 36.223, + 36.757, + 37.314, + 37.849, + 38.383, + 38.917, + 39.451, + 39.985, + 40.542, + 41.076, + 41.61, + 42.121, + 42.655, + 43.166, + 43.7, + 44.257, + 44.791, + 45.325, + 45.859, + 46.393, + 46.928, + 47.462, + 47.972, + 48.506, + 49.041, + 49.575, + 50.109, + 50.62, + 51.154, + 51.688, + 52.222, + 52.756, + 53.29, + 53.824, + 54.335, + 54.892, + 55.449, + 55.983, + 56.517, + 57.051, + 57.585, + 58.12, + 58.654, + 59.164, + 59.699, + 60.233, + 60.72, + 61.208, + 61.742, + 62.276, + 62.787, + 63.321, + 63.832, + 64.319, + 64.807, + 65.318, + 65.805, + 66.316, + 66.827, + 67.361, + 67.849, + 68.336, + 68.87, + 69.358, + 69.869, + 70.356, + 70.844, + 71.355, + 71.843, + 72.33, + 72.841, + 73.352, + 73.839, + 74.35, + 74.861, + 75.372, + 75.883, + 76.37, + 76.858, + 77.369, + 77.857, + 78.344, + 78.855, + 79.366, + 79.853, + 80.364, + 80.852, + 81.363, + 81.874, + 82.361, + 82.872, + 83.36, + 83.87, + 84.381, + 84.892, + 85.403, + 85.891, + 86.378, + 86.889, + 87.4, + 87.911, + 88.398, + 88.909, + 89.42, + 89.908, + 90.395, + 90.883, + 91.394, + 91.881, + 92.369, + 92.88, + 93.391, + 93.925, + 94.459, + 94.993, + 95.504, + 96.015, + 96.549, + 97.059, + 97.593, + 98.104, + 98.638, + 99.172, + 99.706, + 100.241, + 100.775, + 101.285, + 101.796, + 102.307, + 102.841, + 103.352, + 103.863, + 104.397, + 104.931, + 105.465, + 105.999, + 106.51, + 107.044, + 107.555, + 108.066, + 108.577, + 109.087, + 109.575, + 110.109, + 110.62, + 111.131, + 111.618, + 112.083, + 112.57, + 113.081, + 113.569, + 114.056, + 114.544, + 115.032, + 115.519, + 116.03, + 116.518, + 117.029, + 117.516, + 118.027, + 118.491, + 118.956, + 119.443, + 119.954, + 120.442, + 120.93, + 121.417, + 121.928, + 122.392, + 122.88, + 123.368, + 123.832, + 124.32, + 124.807, + 125.318, + 125.806, + 126.27, + 126.758, + 127.245, + 127.71, + 128.197, + 128.662, + 129.126, + 129.66, + 130.148, + 130.635, + 131.123, + 131.611, + 132.098, + 132.609, + 133.12, + 133.631, + 134.142, + 134.629, + 135.094, + 135.581, + 136.069, + 136.557, + 137.044, + 137.532, + 138.089, + 138.6, + 139.088, + 139.598, + 140.086, + 140.574, + 141.061, + 141.549, + 142.036, + 142.524, + 143.012, + 143.499, + 144.01, + 144.498, + 144.985, + 145.473, + 145.961, + 146.448, + 146.936, + 147.423, + 147.911, + 148.399, + 148.91, + 149.374, + 149.862, + 150.349, + 150.86, + 151.371, + 151.835, + 152.323, + 152.811, + 153.275, + 153.763, + 154.273, + 154.761, + 155.249, + 155.736, + 156.224, + 156.711, + 157.199, + 157.687, + 158.198, + 158.685, + 159.173, + 159.66, + 160.148, + 160.636, + 161.123, + 161.634, + 162.122, + 162.609, + 163.097, + 163.585, + 164.072, + 164.56, + 165.071, + 165.558, + 166.023, + 166.51, + 166.998, + 167.462, + 167.927, + 168.391, + 168.832, + 169.297, + 169.761, + 170.225, + 170.69, + 171.154, + 171.619, + 172.106, + 172.547, + 173.035, + 173.523, + 174.01, + 174.498, + 174.986, + 175.473, + 175.961, + 176.425 + ], + "ibiMedian": 0.488, + "ibiCV": 0.05, + "localBPM": { + "min": 107.7, + "p10": 112.3, + "median": 123.0, + "p90": 129.2, + "max": 136.0 + }, + "drift": [ + { + "t": 3.62, + "bpm": 124.5 + }, + { + "t": 7.48, + "bpm": 124.5 + }, + { + "t": 11.33, + "bpm": 123.8 + }, + { + "t": 15.21, + "bpm": 125.3 + }, + { + "t": 19.04, + "bpm": 123.8 + }, + { + "t": 22.92, + "bpm": 120.2 + }, + { + "t": 26.91, + "bpm": 120.9 + }, + { + "t": 30.88, + "bpm": 113.0 + }, + { + "t": 35.13, + "bpm": 111.1 + }, + { + "t": 39.45, + "bpm": 113.0 + }, + { + "t": 43.7, + "bpm": 112.3 + }, + { + "t": 47.97, + "bpm": 113.0 + }, + { + "t": 52.22, + "bpm": 111.7 + }, + { + "t": 56.52, + "bpm": 114.2 + }, + { + "t": 60.72, + "bpm": 117.5 + }, + { + "t": 64.81, + "bpm": 118.1 + }, + { + "t": 68.87, + "bpm": 120.9 + }, + { + "t": 72.84, + "bpm": 119.5 + }, + { + "t": 76.86, + "bpm": 120.2 + }, + { + "t": 80.85, + "bpm": 118.8 + }, + { + "t": 84.89, + "bpm": 119.5 + }, + { + "t": 88.91, + "bpm": 120.9 + }, + { + "t": 92.88, + "bpm": 114.8 + }, + { + "t": 97.06, + "bpm": 113.6 + }, + { + "t": 101.29, + "bpm": 114.8 + }, + { + "t": 105.47, + "bpm": 116.8 + }, + { + "t": 109.57, + "bpm": 120.2 + }, + { + "t": 113.57, + "bpm": 121.6 + }, + { + "t": 117.52, + "bpm": 123.0 + }, + { + "t": 121.42, + "bpm": 123.0 + }, + { + "t": 125.32, + "bpm": 126.0 + }, + { + "t": 129.13, + "bpm": 120.2 + }, + { + "t": 133.12, + "bpm": 122.3 + }, + { + "t": 137.04, + "bpm": 119.5 + }, + { + "t": 141.06, + "bpm": 122.3 + }, + { + "t": 144.99, + "bpm": 122.3 + }, + { + "t": 148.91, + "bpm": 123.0 + }, + { + "t": 152.81, + "bpm": 123.0 + }, + { + "t": 156.71, + "bpm": 122.3 + }, + { + "t": 160.64, + "bpm": 122.3 + }, + { + "t": 164.56, + "bpm": 125.3 + }, + { + "t": 168.39, + "bpm": 129.2 + }, + { + "t": 172.11, + "bpm": 124.5 + } + ] + }, + "key": [ + { + "key": "G# minor", + "r": 0.814 + }, + { + "key": "B major", + "r": 0.609 + }, + { + "key": "E major", + "r": 0.572 + }, + { + "key": "B minor", + "r": 0.267 + } + ], + "chords": { + "runs": [ + { + "t": 0.0, + "chord": "F#m", + "beats": 1 + }, + { + "t": 3.62, + "chord": "G#m", + "beats": 1 + }, + { + "t": 4.09, + "chord": "C#sus4", + "beats": 7 + }, + { + "t": 7.48, + "chord": "F#m", + "beats": 1 + }, + { + "t": 7.99, + "chord": "C#", + "beats": 1 + }, + { + "t": 8.47, + "chord": "C#sus4", + "beats": 1 + }, + { + "t": 8.96, + "chord": "Amaj7", + "beats": 1 + }, + { + "t": 9.45, + "chord": "A#7", + "beats": 1 + }, + { + "t": 9.91, + "chord": "G#maj7", + "beats": 2 + }, + { + "t": 10.87, + "chord": "E", + "beats": 7 + }, + { + "t": 14.21, + "chord": "Amaj7", + "beats": 1 + }, + { + "t": 14.7, + "chord": "G#", + "beats": 2 + }, + { + "t": 15.67, + "chord": "G#m", + "beats": 11 + }, + { + "t": 20.94, + "chord": "B", + "beats": 2 + }, + { + "t": 21.9, + "chord": "E", + "beats": 17 + }, + { + "t": 30.35, + "chord": "G#m", + "beats": 5 + }, + { + "t": 33.02, + "chord": "G#", + "beats": 4 + }, + { + "t": 35.13, + "chord": "B", + "beats": 5 + }, + { + "t": 37.85, + "chord": "Bmaj7", + "beats": 2 + }, + { + "t": 38.92, + "chord": "G#m", + "beats": 1 + }, + { + "t": 39.45, + "chord": "E", + "beats": 7 + }, + { + "t": 43.17, + "chord": "G#m", + "beats": 3 + }, + { + "t": 44.79, + "chord": "G#", + "beats": 5 + }, + { + "t": 47.46, + "chord": "G#m", + "beats": 1 + }, + { + "t": 47.97, + "chord": "B", + "beats": 7 + }, + { + "t": 51.69, + "chord": "G#m7", + "beats": 1 + }, + { + "t": 52.22, + "chord": "E", + "beats": 7 + }, + { + "t": 55.98, + "chord": "G#m", + "beats": 1 + }, + { + "t": 56.52, + "chord": "B", + "beats": 3 + }, + { + "t": 58.12, + "chord": "Bmaj7", + "beats": 3 + }, + { + "t": 59.7, + "chord": "B", + "beats": 3 + }, + { + "t": 61.21, + "chord": "E", + "beats": 7 + }, + { + "t": 64.81, + "chord": "Amaj7", + "beats": 1 + }, + { + "t": 65.32, + "chord": "G#", + "beats": 2 + }, + { + "t": 66.32, + "chord": "G#m", + "beats": 4 + }, + { + "t": 68.34, + "chord": "G#", + "beats": 1 + }, + { + "t": 68.87, + "chord": "D#sus4", + "beats": 1 + }, + { + "t": 69.36, + "chord": "B", + "beats": 3 + }, + { + "t": 70.84, + "chord": "G#m", + "beats": 2 + }, + { + "t": 71.84, + "chord": "B", + "beats": 3 + }, + { + "t": 73.35, + "chord": "E", + "beats": 7 + }, + { + "t": 76.86, + "chord": "G#m", + "beats": 1 + }, + { + "t": 77.37, + "chord": "G#", + "beats": 7 + }, + { + "t": 80.85, + "chord": "G#m", + "beats": 3 + }, + { + "t": 82.36, + "chord": "B", + "beats": 3 + }, + { + "t": 83.87, + "chord": "G#m", + "beats": 1 + }, + { + "t": 84.38, + "chord": "Bmaj7", + "beats": 1 + }, + { + "t": 84.89, + "chord": "E", + "beats": 16 + }, + { + "t": 92.88, + "chord": "G#", + "beats": 8 + }, + { + "t": 97.06, + "chord": "G#m", + "beats": 1 + }, + { + "t": 97.59, + "chord": "B", + "beats": 7 + }, + { + "t": 101.28, + "chord": "B7", + "beats": 1 + }, + { + "t": 101.8, + "chord": "E", + "beats": 7 + }, + { + "t": 105.47, + "chord": "G#m", + "beats": 1 + }, + { + "t": 106.0, + "chord": "G#", + "beats": 7 + }, + { + "t": 109.58, + "chord": "G#m", + "beats": 7 + }, + { + "t": 113.08, + "chord": "B", + "beats": 1 + }, + { + "t": 113.57, + "chord": "E", + "beats": 8 + }, + { + "t": 117.52, + "chord": "Amaj7", + "beats": 1 + }, + { + "t": 118.03, + "chord": "G#", + "beats": 7 + }, + { + "t": 121.42, + "chord": "D#sus4", + "beats": 1 + }, + { + "t": 121.93, + "chord": "G#m", + "beats": 8 + }, + { + "t": 125.81, + "chord": "E", + "beats": 7 + }, + { + "t": 129.13, + "chord": "G#m", + "beats": 1 + }, + { + "t": 129.66, + "chord": "G#", + "beats": 5 + }, + { + "t": 132.1, + "chord": "G#m", + "beats": 5 + }, + { + "t": 134.63, + "chord": "B7", + "beats": 1 + }, + { + "t": 135.09, + "chord": "B", + "beats": 6 + }, + { + "t": 138.09, + "chord": "G#m", + "beats": 3 + }, + { + "t": 139.6, + "chord": "E", + "beats": 8 + }, + { + "t": 143.5, + "chord": "G#", + "beats": 7 + }, + { + "t": 146.94, + "chord": "G#m", + "beats": 2 + }, + { + "t": 147.91, + "chord": "B", + "beats": 2 + }, + { + "t": 148.91, + "chord": "B7", + "beats": 2 + }, + { + "t": 149.86, + "chord": "G#m", + "beats": 2 + }, + { + "t": 150.86, + "chord": "E", + "beats": 8 + }, + { + "t": 154.76, + "chord": "G#m", + "beats": 1 + }, + { + "t": 155.25, + "chord": "G#", + "beats": 7 + }, + { + "t": 158.69, + "chord": "G#m", + "beats": 1 + }, + { + "t": 159.17, + "chord": "B", + "beats": 1 + }, + { + "t": 159.66, + "chord": "B7", + "beats": 1 + }, + { + "t": 160.15, + "chord": "G#m", + "beats": 6 + }, + { + "t": 163.1, + "chord": "B", + "beats": 3 + }, + { + "t": 164.56, + "chord": "G#m", + "beats": 1 + }, + { + "t": 165.07, + "chord": "E", + "beats": 6 + }, + { + "t": 167.93, + "chord": "Esus4", + "beats": 1 + }, + { + "t": 168.39, + "chord": "Amaj7", + "beats": 1 + }, + { + "t": 168.83, + "chord": "G#", + "beats": 7 + }, + { + "t": 172.11, + "chord": "D#sus4", + "beats": 1 + }, + { + "t": 172.55, + "chord": "B", + "beats": 5 + }, + { + "t": 174.99, + "chord": "G#m", + "beats": 1 + }, + { + "t": 175.47, + "chord": "B7", + "beats": 1 + }, + { + "t": 175.96, + "chord": "B", + "beats": 1 + }, + { + "t": 176.43, + "chord": "E", + "beats": 1 + } + ], + "vocab": { + "E": 113, + "G#m": 74, + "G#": 69, + "B": 55, + "C#sus4": 8, + "Bmaj7": 6, + "B7": 6, + "Amaj7": 5, + "D#sus4": 3, + "F#m": 2, + "G#maj7": 2, + "C#": 1, + "A#7": 1, + "G#m7": 1, + "Esus4": 1 + }, + "chromaMean": [ + 0.118, + 0.063, + 0.034, + 0.236, + 0.172, + 0.045, + 0.173, + 0.091, + 0.564, + 0.317, + 0.198, + 0.658 + ] + }, + "voice": { + "range": [ + "G#3", + "G#4" + ], + "median": "D#4", + "voicedFraction": 0.347, + "pitchClassHist": { + "C": 103, + "C#": 146, + "D": 98, + "D#": 1986, + "E": 516, + "F": 66, + "F#": 671, + "G": 72, + "G#": 358, + "A": 67, + "A#": 97, + "B": 1176 + }, + "notes": [ + { + "t": 23.208, + "dur": 0.151, + "midi": 56, + "note": "G#3", + "cents": -14 + }, + { + "t": 26.761, + "dur": 0.104, + "midi": 57, + "note": "A3", + "cents": -34 + }, + { + "t": 26.865, + "dur": 0.488, + "midi": 56, + "note": "G#3", + "cents": 6 + }, + { + "t": 27.492, + "dur": 0.186, + "midi": 57, + "note": "A3", + "cents": 36 + }, + { + "t": 27.818, + "dur": 0.279, + "midi": 59, + "note": "B3", + "cents": -14 + }, + { + "t": 28.793, + "dur": 0.719, + "midi": 59, + "note": "B3", + "cents": 16 + }, + { + "t": 30.047, + "dur": 0.534, + "midi": 63, + "note": "D#4", + "cents": 6 + }, + { + "t": 30.906, + "dur": 0.209, + "midi": 60, + "note": "C4", + "cents": -24 + }, + { + "t": 31.115, + "dur": 0.093, + "midi": 59, + "note": "B3", + "cents": 46 + }, + { + "t": 31.208, + "dur": 0.174, + "midi": 60, + "note": "C4", + "cents": -24 + }, + { + "t": 32.264, + "dur": 0.337, + "midi": 63, + "note": "D#4", + "cents": -14 + }, + { + "t": 33.088, + "dur": 0.546, + "midi": 59, + "note": "B3", + "cents": -4 + }, + { + "t": 33.855, + "dur": 0.174, + "midi": 68, + "note": "G#4", + "cents": -34 + }, + { + "t": 34.203, + "dur": 0.615, + "midi": 66, + "note": "F#4", + "cents": -14 + }, + { + "t": 35.19, + "dur": 0.093, + "midi": 58, + "note": "A#3", + "cents": -24 + }, + { + "t": 35.364, + "dur": 0.557, + "midi": 59, + "note": "B3", + "cents": -24 + }, + { + "t": 36.362, + "dur": 0.522, + "midi": 63, + "note": "D#4", + "cents": -14 + }, + { + "t": 37.465, + "dur": 0.174, + "midi": 59, + "note": "B3", + "cents": 6 + }, + { + "t": 37.779, + "dur": 0.232, + "midi": 59, + "note": "B3", + "cents": 6 + }, + { + "t": 38.464, + "dur": 0.209, + "midi": 63, + "note": "D#4", + "cents": 11 + }, + { + "t": 39.277, + "dur": 0.139, + "midi": 68, + "note": "G#4", + "cents": 6 + }, + { + "t": 39.578, + "dur": 0.209, + "midi": 66, + "note": "F#4", + "cents": 26 + }, + { + "t": 39.845, + "dur": 0.116, + "midi": 68, + "note": "G#4", + "cents": -24 + }, + { + "t": 40.078, + "dur": 0.929, + "midi": 63, + "note": "D#4", + "cents": 6 + }, + { + "t": 41.691, + "dur": 0.232, + "midi": 66, + "note": "F#4", + "cents": -4 + }, + { + "t": 41.97, + "dur": 0.116, + "midi": 68, + "note": "G#4", + "cents": -24 + }, + { + "t": 42.202, + "dur": 1.103, + "midi": 63, + "note": "D#4", + "cents": 6 + }, + { + "t": 43.862, + "dur": 0.197, + "midi": 57, + "note": "A3", + "cents": -34 + }, + { + "t": 44.13, + "dur": 0.244, + "midi": 56, + "note": "G#3", + "cents": 6 + }, + { + "t": 44.838, + "dur": 0.685, + "midi": 59, + "note": "B3", + "cents": -4 + }, + { + "t": 45.883, + "dur": 0.754, + "midi": 59, + "note": "B3", + "cents": -4 + }, + { + "t": 46.707, + "dur": 0.174, + "midi": 61, + "note": "C#4", + "cents": 36 + }, + { + "t": 47.311, + "dur": 0.151, + "midi": 63, + "note": "D#4", + "cents": -14 + }, + { + "t": 48.054, + "dur": 0.662, + "midi": 59, + "note": "B3", + "cents": 16 + }, + { + "t": 49.087, + "dur": 0.441, + "midi": 63, + "note": "D#4", + "cents": -24 + }, + { + "t": 50.225, + "dur": 0.511, + "midi": 59, + "note": "B3", + "cents": -4 + }, + { + "t": 50.945, + "dur": 0.163, + "midi": 67, + "note": "G4", + "cents": 26 + }, + { + "t": 51.525, + "dur": 0.163, + "midi": 66, + "note": "F#4", + "cents": 6 + }, + { + "t": 52.431, + "dur": 0.592, + "midi": 68, + "note": "G#4", + "cents": 6 + }, + { + "t": 53.116, + "dur": 0.093, + "midi": 58, + "note": "A#3", + "cents": 1 + }, + { + "t": 53.406, + "dur": 0.511, + "midi": 59, + "note": "B3", + "cents": 16 + }, + { + "t": 54.462, + "dur": 0.406, + "midi": 59, + "note": "B3", + "cents": 6 + }, + { + "t": 55.484, + "dur": 0.093, + "midi": 62, + "note": "D4", + "cents": 36 + }, + { + "t": 55.6, + "dur": 0.279, + "midi": 63, + "note": "D#4", + "cents": -19 + }, + { + "t": 55.995, + "dur": 0.279, + "midi": 68, + "note": "G#4", + "cents": -4 + }, + { + "t": 56.332, + "dur": 0.093, + "midi": 66, + "note": "F#4", + "cents": 36 + }, + { + "t": 56.564, + "dur": 0.255, + "midi": 66, + "note": "F#4", + "cents": 6 + }, + { + "t": 57.237, + "dur": 0.383, + "midi": 63, + "note": "D#4", + "cents": -24 + }, + { + "t": 58.421, + "dur": 0.151, + "midi": 67, + "note": "G4", + "cents": 36 + }, + { + "t": 58.723, + "dur": 0.151, + "midi": 66, + "note": "F#4", + "cents": -14 + }, + { + "t": 59.977, + "dur": 0.116, + "midi": 62, + "note": "D4", + "cents": -34 + }, + { + "t": 60.244, + "dur": 0.174, + "midi": 59, + "note": "B3", + "cents": 26 + }, + { + "t": 61.846, + "dur": 0.139, + "midi": 64, + "note": "E4", + "cents": -24 + }, + { + "t": 62.485, + "dur": 0.116, + "midi": 63, + "note": "D#4", + "cents": 1 + }, + { + "t": 63.135, + "dur": 0.372, + "midi": 66, + "note": "F#4", + "cents": -4 + }, + { + "t": 64.668, + "dur": 0.104, + "midi": 59, + "note": "B3", + "cents": 16 + }, + { + "t": 65.097, + "dur": 0.093, + "midi": 60, + "note": "C4", + "cents": 6 + }, + { + "t": 65.562, + "dur": 0.116, + "midi": 66, + "note": "F#4", + "cents": -34 + }, + { + "t": 66.235, + "dur": 0.116, + "midi": 62, + "note": "D4", + "cents": 21 + }, + { + "t": 67.315, + "dur": 0.279, + "midi": 66, + "note": "F#4", + "cents": -14 + }, + { + "t": 67.837, + "dur": 0.139, + "midi": 64, + "note": "E4", + "cents": -4 + }, + { + "t": 68.127, + "dur": 0.151, + "midi": 64, + "note": "E4", + "cents": -14 + }, + { + "t": 68.685, + "dur": 0.116, + "midi": 59, + "note": "B3", + "cents": -44 + }, + { + "t": 69.741, + "dur": 0.093, + "midi": 64, + "note": "E4", + "cents": -34 + }, + { + "t": 70.089, + "dur": 0.151, + "midi": 63, + "note": "D#4", + "cents": -4 + }, + { + "t": 70.705, + "dur": 0.313, + "midi": 59, + "note": "B3", + "cents": 6 + }, + { + "t": 71.146, + "dur": 0.093, + "midi": 61, + "note": "C#4", + "cents": -9 + }, + { + "t": 71.843, + "dur": 0.174, + "midi": 64, + "note": "E4", + "cents": -14 + }, + { + "t": 74.106, + "dur": 0.163, + "midi": 64, + "note": "E4", + "cents": 31 + }, + { + "t": 74.408, + "dur": 0.232, + "midi": 63, + "note": "D#4", + "cents": -24 + }, + { + "t": 75.233, + "dur": 0.29, + "midi": 66, + "note": "F#4", + "cents": -14 + }, + { + "t": 75.639, + "dur": 0.116, + "midi": 66, + "note": "F#4", + "cents": 1 + }, + { + "t": 76.672, + "dur": 0.116, + "midi": 59, + "note": "B3", + "cents": -14 + }, + { + "t": 77.137, + "dur": 0.151, + "midi": 59, + "note": "B3", + "cents": 26 + }, + { + "t": 77.555, + "dur": 0.186, + "midi": 66, + "note": "F#4", + "cents": -24 + }, + { + "t": 78.17, + "dur": 0.337, + "midi": 63, + "note": "D#4", + "cents": 26 + }, + { + "t": 78.739, + "dur": 0.197, + "midi": 59, + "note": "B3", + "cents": 6 + }, + { + "t": 79.168, + "dur": 0.104, + "midi": 61, + "note": "C#4", + "cents": 26 + }, + { + "t": 79.482, + "dur": 0.163, + "midi": 66, + "note": "F#4", + "cents": -34 + }, + { + "t": 80.097, + "dur": 0.941, + "midi": 63, + "note": "D#4", + "cents": 16 + }, + { + "t": 81.676, + "dur": 0.116, + "midi": 66, + "note": "F#4", + "cents": -24 + }, + { + "t": 82.477, + "dur": 0.128, + "midi": 63, + "note": "D#4", + "cents": 26 + }, + { + "t": 84.126, + "dur": 0.151, + "midi": 64, + "note": "E4", + "cents": 6 + }, + { + "t": 84.706, + "dur": 0.116, + "midi": 60, + "note": "C4", + "cents": -34 + }, + { + "t": 85.682, + "dur": 0.139, + "midi": 66, + "note": "F#4", + "cents": -19 + }, + { + "t": 86.552, + "dur": 0.197, + "midi": 63, + "note": "D#4", + "cents": 16 + }, + { + "t": 87.667, + "dur": 0.163, + "midi": 66, + "note": "F#4", + "cents": 16 + }, + { + "t": 88.224, + "dur": 0.093, + "midi": 64, + "note": "E4", + "cents": 21 + }, + { + "t": 88.549, + "dur": 0.244, + "midi": 63, + "note": "D#4", + "cents": -14 + }, + { + "t": 89.223, + "dur": 0.139, + "midi": 63, + "note": "D#4", + "cents": 16 + }, + { + "t": 89.536, + "dur": 1.544, + "midi": 63, + "note": "D#4", + "cents": -14 + }, + { + "t": 91.22, + "dur": 1.521, + "midi": 63, + "note": "D#4", + "cents": 6 + }, + { + "t": 93.147, + "dur": 0.174, + "midi": 63, + "note": "D#4", + "cents": -24 + }, + { + "t": 93.693, + "dur": 0.337, + "midi": 56, + "note": "G#3", + "cents": -4 + }, + { + "t": 94.238, + "dur": 0.163, + "midi": 58, + "note": "A#3", + "cents": -39 + }, + { + "t": 94.459, + "dur": 0.662, + "midi": 59, + "note": "B3", + "cents": -14 + }, + { + "t": 95.306, + "dur": 0.174, + "midi": 61, + "note": "C#4", + "cents": 26 + }, + { + "t": 95.631, + "dur": 0.592, + "midi": 59, + "note": "B3", + "cents": -14 + }, + { + "t": 96.282, + "dur": 0.104, + "midi": 61, + "note": "C#4", + "cents": 26 + }, + { + "t": 96.653, + "dur": 0.093, + "midi": 63, + "note": "D#4", + "cents": 36 + }, + { + "t": 97.652, + "dur": 0.128, + "midi": 59, + "note": "B3", + "cents": -44 + }, + { + "t": 97.872, + "dur": 0.406, + "midi": 59, + "note": "B3", + "cents": -34 + }, + { + "t": 98.766, + "dur": 0.139, + "midi": 63, + "note": "D#4", + "cents": 11 + }, + { + "t": 99.683, + "dur": 0.093, + "midi": 59, + "note": "B3", + "cents": 26 + }, + { + "t": 99.776, + "dur": 0.116, + "midi": 60, + "note": "C4", + "cents": -34 + }, + { + "t": 99.892, + "dur": 0.395, + "midi": 59, + "note": "B3", + "cents": 36 + }, + { + "t": 100.531, + "dur": 0.104, + "midi": 68, + "note": "G#4", + "cents": 26 + }, + { + "t": 100.821, + "dur": 0.244, + "midi": 66, + "note": "F#4", + "cents": -4 + }, + { + "t": 101.065, + "dur": 0.116, + "midi": 65, + "note": "F4", + "cents": -4 + }, + { + "t": 101.854, + "dur": 0.685, + "midi": 56, + "note": "G#3", + "cents": 6 + }, + { + "t": 102.632, + "dur": 0.116, + "midi": 58, + "note": "A#3", + "cents": -4 + }, + { + "t": 103.921, + "dur": 0.279, + "midi": 59, + "note": "B3", + "cents": 6 + }, + { + "t": 104.699, + "dur": 0.186, + "midi": 61, + "note": "C#4", + "cents": 26 + }, + { + "t": 106.011, + "dur": 0.29, + "midi": 66, + "note": "F#4", + "cents": 36 + }, + { + "t": 106.58, + "dur": 0.639, + "midi": 63, + "note": "D#4", + "cents": 6 + }, + { + "t": 107.799, + "dur": 0.128, + "midi": 68, + "note": "G#4", + "cents": -24 + }, + { + "t": 108.042, + "dur": 0.279, + "midi": 66, + "note": "F#4", + "cents": -34 + }, + { + "t": 108.716, + "dur": 0.163, + "midi": 63, + "note": "D#4", + "cents": 16 + }, + { + "t": 109.343, + "dur": 0.151, + "midi": 62, + "note": "D4", + "cents": -34 + }, + { + "t": 109.598, + "dur": 0.128, + "midi": 58, + "note": "A#3", + "cents": 26 + }, + { + "t": 110.666, + "dur": 0.302, + "midi": 64, + "note": "E4", + "cents": -24 + }, + { + "t": 111.293, + "dur": 0.151, + "midi": 63, + "note": "D#4", + "cents": 26 + }, + { + "t": 111.572, + "dur": 0.128, + "midi": 59, + "note": "B3", + "cents": -14 + }, + { + "t": 111.932, + "dur": 0.348, + "midi": 66, + "note": "F#4", + "cents": -24 + }, + { + "t": 113.325, + "dur": 0.197, + "midi": 59, + "note": "B3", + "cents": 16 + }, + { + "t": 114.265, + "dur": 0.174, + "midi": 66, + "note": "F#4", + "cents": -24 + }, + { + "t": 115.066, + "dur": 0.093, + "midi": 63, + "note": "D#4", + "cents": -14 + }, + { + "t": 116.019, + "dur": 0.394, + "midi": 66, + "note": "F#4", + "cents": -24 + }, + { + "t": 116.495, + "dur": 0.128, + "midi": 64, + "note": "E4", + "cents": -34 + }, + { + "t": 117.307, + "dur": 0.104, + "midi": 58, + "note": "A#3", + "cents": 36 + }, + { + "t": 118.34, + "dur": 0.209, + "midi": 64, + "note": "E4", + "cents": -14 + }, + { + "t": 118.7, + "dur": 0.174, + "midi": 63, + "note": "D#4", + "cents": -14 + }, + { + "t": 119.327, + "dur": 0.244, + "midi": 59, + "note": "B3", + "cents": 6 + }, + { + "t": 120.465, + "dur": 0.278, + "midi": 64, + "note": "E4", + "cents": -4 + }, + { + "t": 122.88, + "dur": 0.221, + "midi": 63, + "note": "D#4", + "cents": -14 + }, + { + "t": 124.575, + "dur": 0.093, + "midi": 64, + "note": "E4", + "cents": -19 + }, + { + "t": 124.668, + "dur": 0.209, + "midi": 63, + "note": "D#4", + "cents": 36 + }, + { + "t": 126.026, + "dur": 0.104, + "midi": 66, + "note": "F#4", + "cents": -14 + }, + { + "t": 126.502, + "dur": 0.488, + "midi": 63, + "note": "D#4", + "cents": 6 + }, + { + "t": 127.187, + "dur": 0.197, + "midi": 59, + "note": "B3", + "cents": -24 + }, + { + "t": 127.536, + "dur": 0.093, + "midi": 61, + "note": "C#4", + "cents": 36 + }, + { + "t": 127.849, + "dur": 0.128, + "midi": 66, + "note": "F#4", + "cents": -4 + }, + { + "t": 128.36, + "dur": 0.906, + "midi": 63, + "note": "D#4", + "cents": 6 + }, + { + "t": 129.335, + "dur": 0.104, + "midi": 59, + "note": "B3", + "cents": -14 + }, + { + "t": 129.962, + "dur": 0.093, + "midi": 66, + "note": "F#4", + "cents": -44 + }, + { + "t": 130.148, + "dur": 0.104, + "midi": 64, + "note": "E4", + "cents": -34 + }, + { + "t": 131.901, + "dur": 0.116, + "midi": 66, + "note": "F#4", + "cents": -34 + }, + { + "t": 132.052, + "dur": 0.151, + "midi": 64, + "note": "E4", + "cents": -14 + }, + { + "t": 132.331, + "dur": 0.174, + "midi": 64, + "note": "E4", + "cents": -4 + }, + { + "t": 132.667, + "dur": 0.209, + "midi": 63, + "note": "D#4", + "cents": 6 + }, + { + "t": 133.863, + "dur": 0.186, + "midi": 66, + "note": "F#4", + "cents": -19 + }, + { + "t": 135.535, + "dur": 0.104, + "midi": 66, + "note": "F#4", + "cents": 36 + }, + { + "t": 135.918, + "dur": 0.093, + "midi": 66, + "note": "F#4", + "cents": 31 + }, + { + "t": 136.301, + "dur": 0.197, + "midi": 64, + "note": "E4", + "cents": -14 + }, + { + "t": 136.731, + "dur": 0.186, + "midi": 63, + "note": "D#4", + "cents": 36 + }, + { + "t": 137.381, + "dur": 0.116, + "midi": 63, + "note": "D#4", + "cents": -29 + }, + { + "t": 137.764, + "dur": 2.682, + "midi": 63, + "note": "D#4", + "cents": -4 + }, + { + "t": 140.643, + "dur": 0.174, + "midi": 63, + "note": "D#4", + "cents": 16 + }, + { + "t": 151.185, + "dur": 0.116, + "midi": 57, + "note": "A3", + "cents": 21 + }, + { + "t": 151.8, + "dur": 0.302, + "midi": 64, + "note": "E4", + "cents": 36 + }, + { + "t": 152.358, + "dur": 0.139, + "midi": 63, + "note": "D#4", + "cents": 36 + }, + { + "t": 152.869, + "dur": 0.093, + "midi": 59, + "note": "B3", + "cents": -44 + }, + { + "t": 153.205, + "dur": 0.093, + "midi": 65, + "note": "F4", + "cents": 46 + }, + { + "t": 153.298, + "dur": 0.349, + "midi": 66, + "note": "F#4", + "cents": -24 + }, + { + "t": 154.32, + "dur": 0.372, + "midi": 63, + "note": "D#4", + "cents": 26 + }, + { + "t": 156.166, + "dur": 0.244, + "midi": 63, + "note": "D#4", + "cents": 16 + }, + { + "t": 157.931, + "dur": 0.174, + "midi": 64, + "note": "E4", + "cents": -34 + }, + { + "t": 158.29, + "dur": 0.163, + "midi": 63, + "note": "D#4", + "cents": 26 + }, + { + "t": 159.626, + "dur": 0.464, + "midi": 64, + "note": "E4", + "cents": -14 + }, + { + "t": 160.194, + "dur": 0.291, + "midi": 63, + "note": "D#4", + "cents": -4 + }, + { + "t": 161.042, + "dur": 0.267, + "midi": 66, + "note": "F#4", + "cents": 16 + }, + { + "t": 162.261, + "dur": 2.473, + "midi": 63, + "note": "D#4", + "cents": 16 + }, + { + "t": 164.827, + "dur": 0.139, + "midi": 62, + "note": "D4", + "cents": -34 + }, + { + "t": 165.129, + "dur": 1.416, + "midi": 59, + "note": "B3", + "cents": 6 + }, + { + "t": 166.789, + "dur": 0.163, + "midi": 59, + "note": "B3", + "cents": -24 + } + ] + }, + "phrases": [ + { + "start": 23.21, + "end": 23.36, + "line": "G#3" + }, + { + "start": 26.76, + "end": 58.87, + "line": "A3 G#3 A3 B3 B3 D#4 C4 B3 C4 D#4 B3 G#4 F#4 A#3 B3 D#4 B3 B3 D#4 G#4 F#4 G#4 D#4 F#4 G#4 D#4 A3 G#3 B3 B3 C#4 D#4 B3 D#4 B3 G4 F#4 G#4 A#3 B3 B3 D4 D#4 G#4 F#4 F#4 D#4 G4 F#4" + }, + { + "start": 59.98, + "end": 60.42, + "line": "D4 B3" + }, + { + "start": 61.85, + "end": 63.51, + "line": "E4 D#4 F#4" + }, + { + "start": 64.67, + "end": 66.35, + "line": "B3 C4 F#4 D4" + }, + { + "start": 67.31, + "end": 68.8, + "line": "F#4 E4 E4 B3" + }, + { + "start": 69.74, + "end": 72.02, + "line": "E4 D#4 B3 C#4 E4" + }, + { + "start": 74.11, + "end": 75.75, + "line": "E4 D#4 F#4 F#4" + }, + { + "start": 76.67, + "end": 82.61, + "line": "B3 B3 F#4 D#4 B3 C#4 F#4 D#4 F#4 D#4" + }, + { + "start": 84.13, + "end": 86.75, + "line": "E4 C4 F#4 D#4" + }, + { + "start": 87.67, + "end": 96.75, + "line": "F#4 E4 D#4 D#4 D#4 D#4 D#4 G#3 A#3 B3 C#4 B3 C#4 D#4" + }, + { + "start": 97.65, + "end": 102.75, + "line": "B3 B3 D#4 B3 C4 B3 G#4 F#4 F4 G#3 A#3" + }, + { + "start": 103.92, + "end": 104.89, + "line": "B3 C#4" + }, + { + "start": 106.01, + "end": 109.73, + "line": "F#4 D#4 G#4 F#4 D#4 D4 A#3" + }, + { + "start": 110.67, + "end": 112.28, + "line": "E4 D#4 B3 F#4" + }, + { + "start": 113.33, + "end": 117.41, + "line": "B3 F#4 D#4 F#4 E4 A#3" + }, + { + "start": 118.34, + "end": 120.74, + "line": "E4 D#4 B3 E4" + }, + { + "start": 122.88, + "end": 123.1, + "line": "D#4" + }, + { + "start": 124.58, + "end": 124.88, + "line": "E4 D#4" + }, + { + "start": 126.03, + "end": 130.25, + "line": "F#4 D#4 B3 C#4 F#4 D#4 B3 F#4 E4" + }, + { + "start": 131.9, + "end": 132.88, + "line": "F#4 E4 E4 D#4" + }, + { + "start": 133.86, + "end": 134.05, + "line": "F#4" + }, + { + "start": 135.53, + "end": 140.82, + "line": "F#4 F#4 E4 D#4 D#4 D#4 D#4" + }, + { + "start": 151.19, + "end": 154.69, + "line": "A3 E4 D#4 B3 F4 F#4 D#4" + }, + { + "start": 156.17, + "end": 156.41, + "line": "D#4" + }, + { + "start": 157.93, + "end": 158.45, + "line": "E4 D#4" + }, + { + "start": 159.63, + "end": 161.31, + "line": "E4 D#4 F#4" + }, + { + "start": 162.26, + "end": 166.95, + "line": "D#4 D4 B3 B3" + } + ], + "stems": { + "vocals": "pop/samples/sailor-song-take/stems/htdemucs/vocals.wav", + "guitar": "pop/samples/sailor-song-take/stems/htdemucs/guitar.wav" + } +} \ No newline at end of file diff --git a/pop/sailor-song/vox-notes.json b/pop/sailor-song/vox-notes.json new file mode 100644 index 0000000000..3818a7ece9 --- /dev/null +++ b/pop/sailor-song/vox-notes.json @@ -0,0 +1,1693 @@ +{ + "grid": "G# natural minor, +13c frame", + "snap": 1.0, + "smoothMs": 50.0, + "hyst": 0.45, + "vibKeep": 0.3, + "centsToGrid": { + "before": 16.0, + "after": 4.0 + }, + "notes": [ + { + "t": 15.685, + "dur": 0.205, + "midi": 54, + "note": "F#3" + }, + { + "t": 23.19, + "dur": 0.235, + "midi": 68, + "note": "G#4" + }, + { + "t": 24.64, + "dur": 0.385, + "midi": 56, + "note": "G#3" + }, + { + "t": 26.25, + "dur": 0.15, + "midi": 64, + "note": "E4" + }, + { + "t": 26.62, + "dur": 0.84, + "midi": 56, + "note": "G#3" + }, + { + "t": 27.46, + "dur": 0.22, + "midi": 58, + "note": "A#3" + }, + { + "t": 27.775, + "dur": 0.71, + "midi": 59, + "note": "B3" + }, + { + "t": 28.485, + "dur": 0.27, + "midi": 61, + "note": "C#4" + }, + { + "t": 28.755, + "dur": 0.785, + "midi": 59, + "note": "B3" + }, + { + "t": 29.54, + "dur": 0.45, + "midi": 61, + "note": "C#4" + }, + { + "t": 29.99, + "dur": 0.885, + "midi": 63, + "note": "D#4" + }, + { + "t": 30.875, + "dur": 0.845, + "midi": 59, + "note": "B3" + }, + { + "t": 31.72, + "dur": 0.335, + "midi": 61, + "note": "C#4" + }, + { + "t": 32.055, + "dur": 0.765, + "midi": 63, + "note": "D#4" + }, + { + "t": 32.82, + "dur": 0.195, + "midi": 61, + "note": "C#4" + }, + { + "t": 33.015, + "dur": 0.81, + "midi": 59, + "note": "B3" + }, + { + "t": 33.825, + "dur": 0.225, + "midi": 68, + "note": "G#4" + }, + { + "t": 34.05, + "dur": 0.965, + "midi": 66, + "note": "F#4" + }, + { + "t": 35.17, + "dur": 0.175, + "midi": 58, + "note": "A#3" + }, + { + "t": 35.345, + "dur": 0.615, + "midi": 59, + "note": "B3" + }, + { + "t": 35.96, + "dur": 0.3, + "midi": 61, + "note": "C#4" + }, + { + "t": 36.26, + "dur": 0.73, + "midi": 63, + "note": "D#4" + }, + { + "t": 36.99, + "dur": 0.305, + "midi": 61, + "note": "C#4" + }, + { + "t": 37.295, + "dur": 0.26, + "midi": 59, + "note": "B3" + }, + { + "t": 37.565, + "dur": 0.485, + "midi": 59, + "note": "B3" + }, + { + "t": 38.05, + "dur": 0.31, + "midi": 61, + "note": "C#4" + }, + { + "t": 38.36, + "dur": 0.785, + "midi": 63, + "note": "D#4" + }, + { + "t": 39.145, + "dur": 0.395, + "midi": 68, + "note": "G#4" + }, + { + "t": 39.54, + "dur": 0.28, + "midi": 66, + "note": "F#4" + }, + { + "t": 39.82, + "dur": 0.21, + "midi": 68, + "note": "G#4" + }, + { + "t": 40.03, + "dur": 1.07, + "midi": 63, + "note": "D#4" + }, + { + "t": 41.12, + "dur": 0.2, + "midi": 63, + "note": "D#4" + }, + { + "t": 41.36, + "dur": 0.22, + "midi": 68, + "note": "G#4" + }, + { + "t": 41.66, + "dur": 0.29, + "midi": 66, + "note": "F#4" + }, + { + "t": 41.95, + "dur": 0.21, + "midi": 68, + "note": "G#4" + }, + { + "t": 42.16, + "dur": 1.17, + "midi": 63, + "note": "D#4" + }, + { + "t": 43.33, + "dur": 0.39, + "midi": 64, + "note": "E4" + }, + { + "t": 43.74, + "dur": 0.705, + "midi": 56, + "note": "G#3" + }, + { + "t": 44.445, + "dur": 0.33, + "midi": 58, + "note": "A#3" + }, + { + "t": 44.775, + "dur": 0.78, + "midi": 59, + "note": "B3" + }, + { + "t": 45.555, + "dur": 0.28, + "midi": 61, + "note": "C#4" + }, + { + "t": 45.835, + "dur": 0.845, + "midi": 59, + "note": "B3" + }, + { + "t": 46.68, + "dur": 0.285, + "midi": 61, + "note": "C#4" + }, + { + "t": 46.965, + "dur": 0.745, + "midi": 63, + "note": "D#4" + }, + { + "t": 47.96, + "dur": 0.81, + "midi": 59, + "note": "B3" + }, + { + "t": 48.77, + "dur": 0.18, + "midi": 61, + "note": "C#4" + }, + { + "t": 48.95, + "dur": 0.955, + "midi": 63, + "note": "D#4" + }, + { + "t": 49.905, + "dur": 0.195, + "midi": 61, + "note": "C#4" + }, + { + "t": 50.1, + "dur": 0.81, + "midi": 59, + "note": "B3" + }, + { + "t": 50.91, + "dur": 0.32, + "midi": 68, + "note": "G#4" + }, + { + "t": 51.465, + "dur": 0.805, + "midi": 66, + "note": "F#4" + }, + { + "t": 52.27, + "dur": 1.01, + "midi": 56, + "note": "G#3" + }, + { + "t": 53.28, + "dur": 0.81, + "midi": 59, + "note": "B3" + }, + { + "t": 54.09, + "dur": 0.25, + "midi": 61, + "note": "C#4" + }, + { + "t": 54.34, + "dur": 0.64, + "midi": 59, + "note": "B3" + }, + { + "t": 55.13, + "dur": 0.83, + "midi": 63, + "note": "D#4" + }, + { + "t": 55.96, + "dur": 0.335, + "midi": 68, + "note": "G#4" + }, + { + "t": 56.295, + "dur": 0.585, + "midi": 66, + "note": "F#4" + }, + { + "t": 56.88, + "dur": 0.19, + "midi": 68, + "note": "G#4" + }, + { + "t": 57.07, + "dur": 1.1, + "midi": 63, + "note": "D#4" + }, + { + "t": 58.35, + "dur": 0.29, + "midi": 68, + "note": "G#4" + }, + { + "t": 58.64, + "dur": 0.365, + "midi": 66, + "note": "F#4" + }, + { + "t": 59.005, + "dur": 0.215, + "midi": 68, + "note": "G#4" + }, + { + "t": 59.22, + "dur": 0.735, + "midi": 63, + "note": "D#4" + }, + { + "t": 59.955, + "dur": 0.205, + "midi": 61, + "note": "C#4" + }, + { + "t": 60.16, + "dur": 0.3, + "midi": 59, + "note": "B3" + }, + { + "t": 60.46, + "dur": 0.235, + "midi": 61, + "note": "C#4" + }, + { + "t": 61.19, + "dur": 0.54, + "midi": 66, + "note": "F#4" + }, + { + "t": 61.73, + "dur": 0.655, + "midi": 64, + "note": "E4" + }, + { + "t": 62.385, + "dur": 0.395, + "midi": 63, + "note": "D#4" + }, + { + "t": 62.78, + "dur": 0.135, + "midi": 59, + "note": "B3" + }, + { + "t": 62.96, + "dur": 0.13, + "midi": 63, + "note": "D#4" + }, + { + "t": 63.09, + "dur": 0.675, + "midi": 66, + "note": "F#4" + }, + { + "t": 63.765, + "dur": 0.585, + "midi": 64, + "note": "E4" + }, + { + "t": 64.35, + "dur": 0.21, + "midi": 61, + "note": "C#4" + }, + { + "t": 64.56, + "dur": 0.305, + "midi": 59, + "note": "B3" + }, + { + "t": 65.015, + "dur": 0.285, + "midi": 59, + "note": "B3" + }, + { + "t": 65.3, + "dur": 0.13, + "midi": 64, + "note": "E4" + }, + { + "t": 65.43, + "dur": 0.345, + "midi": 66, + "note": "F#4" + }, + { + "t": 65.775, + "dur": 0.28, + "midi": 64, + "note": "E4" + }, + { + "t": 66.105, + "dur": 0.175, + "midi": 56, + "note": "G#3" + }, + { + "t": 66.28, + "dur": 0.58, + "midi": 63, + "note": "D#4" + }, + { + "t": 66.86, + "dur": 0.33, + "midi": 59, + "note": "B3" + }, + { + "t": 67.19, + "dur": 0.605, + "midi": 66, + "note": "F#4" + }, + { + "t": 67.795, + "dur": 0.49, + "midi": 64, + "note": "E4" + }, + { + "t": 68.285, + "dur": 0.31, + "midi": 63, + "note": "D#4" + }, + { + "t": 68.595, + "dur": 0.315, + "midi": 59, + "note": "B3" + }, + { + "t": 69.09, + "dur": 0.22, + "midi": 59, + "note": "B3" + }, + { + "t": 69.355, + "dur": 1.215, + "midi": 63, + "note": "D#4" + }, + { + "t": 70.57, + "dur": 0.13, + "midi": 58, + "note": "A#3" + }, + { + "t": 70.7, + "dur": 0.395, + "midi": 59, + "note": "B3" + }, + { + "t": 71.095, + "dur": 0.24, + "midi": 61, + "note": "C#4" + }, + { + "t": 71.335, + "dur": 0.26, + "midi": 64, + "note": "E4" + }, + { + "t": 71.595, + "dur": 0.22, + "midi": 66, + "note": "F#4" + }, + { + "t": 71.815, + "dur": 0.465, + "midi": 64, + "note": "E4" + }, + { + "t": 72.36, + "dur": 0.21, + "midi": 63, + "note": "D#4" + }, + { + "t": 72.57, + "dur": 0.25, + "midi": 59, + "note": "B3" + }, + { + "t": 72.82, + "dur": 0.23, + "midi": 52, + "note": "E3" + }, + { + "t": 73.05, + "dur": 0.745, + "midi": 59, + "note": "B3" + }, + { + "t": 73.835, + "dur": 0.49, + "midi": 64, + "note": "E4" + }, + { + "t": 74.385, + "dur": 0.82, + "midi": 63, + "note": "D#4" + }, + { + "t": 75.205, + "dur": 0.57, + "midi": 66, + "note": "F#4" + }, + { + "t": 75.775, + "dur": 0.12, + "midi": 63, + "note": "D#4" + }, + { + "t": 75.895, + "dur": 0.24, + "midi": 64, + "note": "E4" + }, + { + "t": 76.2, + "dur": 0.395, + "midi": 63, + "note": "D#4" + }, + { + "t": 76.595, + "dur": 0.295, + "midi": 59, + "note": "B3" + }, + { + "t": 76.93, + "dur": 0.425, + "midi": 59, + "note": "B3" + }, + { + "t": 77.355, + "dur": 0.125, + "midi": 64, + "note": "E4" + }, + { + "t": 77.48, + "dur": 0.325, + "midi": 66, + "note": "F#4" + }, + { + "t": 77.805, + "dur": 0.74, + "midi": 63, + "note": "D#4" + }, + { + "t": 78.545, + "dur": 0.58, + "midi": 59, + "note": "B3" + }, + { + "t": 79.125, + "dur": 0.345, + "midi": 61, + "note": "C#4" + }, + { + "t": 79.47, + "dur": 0.215, + "midi": 66, + "note": "F#4" + }, + { + "t": 79.685, + "dur": 1.44, + "midi": 63, + "note": "D#4" + }, + { + "t": 81.125, + "dur": 0.27, + "midi": 59, + "note": "B3" + }, + { + "t": 81.395, + "dur": 0.23, + "midi": 64, + "note": "E4" + }, + { + "t": 81.625, + "dur": 0.23, + "midi": 66, + "note": "F#4" + }, + { + "t": 81.855, + "dur": 0.225, + "midi": 63, + "note": "D#4" + }, + { + "t": 82.08, + "dur": 0.29, + "midi": 64, + "note": "E4" + }, + { + "t": 82.37, + "dur": 0.445, + "midi": 63, + "note": "D#4" + }, + { + "t": 82.815, + "dur": 0.47, + "midi": 59, + "note": "B3" + }, + { + "t": 83.285, + "dur": 0.57, + "midi": 66, + "note": "F#4" + }, + { + "t": 83.855, + "dur": 0.125, + "midi": 63, + "note": "D#4" + }, + { + "t": 83.995, + "dur": 0.41, + "midi": 64, + "note": "E4" + }, + { + "t": 84.405, + "dur": 0.28, + "midi": 63, + "note": "D#4" + }, + { + "t": 84.685, + "dur": 0.315, + "midi": 59, + "note": "B3" + }, + { + "t": 85.125, + "dur": 0.435, + "midi": 61, + "note": "C#4" + }, + { + "t": 85.56, + "dur": 0.295, + "midi": 66, + "note": "F#4" + }, + { + "t": 85.855, + "dur": 0.39, + "midi": 64, + "note": "E4" + }, + { + "t": 86.245, + "dur": 0.15, + "midi": 68, + "note": "G#4" + }, + { + "t": 86.395, + "dur": 0.75, + "midi": 63, + "note": "D#4" + }, + { + "t": 87.145, + "dur": 0.165, + "midi": 64, + "note": "E4" + }, + { + "t": 87.31, + "dur": 0.53, + "midi": 66, + "note": "F#4" + }, + { + "t": 87.84, + "dur": 0.15, + "midi": 64, + "note": "E4" + }, + { + "t": 87.99, + "dur": 0.165, + "midi": 63, + "note": "D#4" + }, + { + "t": 88.155, + "dur": 0.245, + "midi": 64, + "note": "E4" + }, + { + "t": 88.51, + "dur": 4.87, + "midi": 63, + "note": "D#4" + }, + { + "t": 93.41, + "dur": 0.755, + "midi": 56, + "note": "G#3" + }, + { + "t": 94.165, + "dur": 0.285, + "midi": 58, + "note": "A#3" + }, + { + "t": 94.45, + "dur": 0.765, + "midi": 59, + "note": "B3" + }, + { + "t": 95.215, + "dur": 0.325, + "midi": 61, + "note": "C#4" + }, + { + "t": 95.54, + "dur": 0.135, + "midi": 58, + "note": "A#3" + }, + { + "t": 95.675, + "dur": 0.59, + "midi": 59, + "note": "B3" + }, + { + "t": 96.265, + "dur": 0.255, + "midi": 61, + "note": "C#4" + }, + { + "t": 96.52, + "dur": 0.21, + "midi": 64, + "note": "E4" + }, + { + "t": 96.73, + "dur": 0.555, + "midi": 63, + "note": "D#4" + }, + { + "t": 97.285, + "dur": 0.325, + "midi": 61, + "note": "C#4" + }, + { + "t": 97.61, + "dur": 1.13, + "midi": 59, + "note": "B3" + }, + { + "t": 98.74, + "dur": 0.74, + "midi": 63, + "note": "D#4" + }, + { + "t": 99.48, + "dur": 0.17, + "midi": 61, + "note": "C#4" + }, + { + "t": 99.65, + "dur": 0.79, + "midi": 59, + "note": "B3" + }, + { + "t": 100.47, + "dur": 0.24, + "midi": 68, + "note": "G#4" + }, + { + "t": 100.745, + "dur": 0.38, + "midi": 66, + "note": "F#4" + }, + { + "t": 101.125, + "dur": 0.385, + "midi": 64, + "note": "E4" + }, + { + "t": 101.83, + "dur": 0.72, + "midi": 56, + "note": "G#3" + }, + { + "t": 102.55, + "dur": 0.25, + "midi": 58, + "note": "A#3" + }, + { + "t": 102.825, + "dur": 0.56, + "midi": 59, + "note": "B3" + }, + { + "t": 103.385, + "dur": 0.23, + "midi": 58, + "note": "A#3" + }, + { + "t": 103.615, + "dur": 0.25, + "midi": 61, + "note": "C#4" + }, + { + "t": 103.865, + "dur": 0.8, + "midi": 59, + "note": "B3" + }, + { + "t": 104.665, + "dur": 0.25, + "midi": 61, + "note": "C#4" + }, + { + "t": 104.915, + "dur": 0.77, + "midi": 63, + "note": "D#4" + }, + { + "t": 105.685, + "dur": 0.205, + "midi": 68, + "note": "G#4" + }, + { + "t": 105.89, + "dur": 0.455, + "midi": 66, + "note": "F#4" + }, + { + "t": 106.345, + "dur": 0.195, + "midi": 68, + "note": "G#4" + }, + { + "t": 106.54, + "dur": 1.02, + "midi": 63, + "note": "D#4" + }, + { + "t": 107.755, + "dur": 0.26, + "midi": 68, + "note": "G#4" + }, + { + "t": 108.015, + "dur": 0.365, + "midi": 66, + "note": "F#4" + }, + { + "t": 108.38, + "dur": 0.19, + "midi": 68, + "note": "G#4" + }, + { + "t": 108.57, + "dur": 0.12, + "midi": 64, + "note": "E4" + }, + { + "t": 108.69, + "dur": 0.62, + "midi": 63, + "note": "D#4" + }, + { + "t": 109.31, + "dur": 0.28, + "midi": 61, + "note": "C#4" + }, + { + "t": 109.59, + "dur": 0.195, + "midi": 59, + "note": "B3" + }, + { + "t": 109.785, + "dur": 0.255, + "midi": 61, + "note": "C#4" + }, + { + "t": 110.06, + "dur": 0.12, + "midi": 68, + "note": "G#4" + }, + { + "t": 110.18, + "dur": 0.44, + "midi": 66, + "note": "F#4" + }, + { + "t": 110.62, + "dur": 0.61, + "midi": 64, + "note": "E4" + }, + { + "t": 111.23, + "dur": 0.265, + "midi": 63, + "note": "D#4" + }, + { + "t": 111.495, + "dur": 0.265, + "midi": 59, + "note": "B3" + }, + { + "t": 111.795, + "dur": 0.125, + "midi": 63, + "note": "D#4" + }, + { + "t": 111.92, + "dur": 0.665, + "midi": 66, + "note": "F#4" + }, + { + "t": 112.585, + "dur": 0.225, + "midi": 63, + "note": "D#4" + }, + { + "t": 112.81, + "dur": 0.21, + "midi": 64, + "note": "E4" + }, + { + "t": 113.02, + "dur": 0.275, + "midi": 61, + "note": "C#4" + }, + { + "t": 113.295, + "dur": 0.295, + "midi": 59, + "note": "B3" + }, + { + "t": 113.7, + "dur": 0.15, + "midi": 56, + "note": "G#3" + }, + { + "t": 113.85, + "dur": 0.165, + "midi": 59, + "note": "B3" + }, + { + "t": 114.015, + "dur": 0.16, + "midi": 64, + "note": "E4" + }, + { + "t": 114.175, + "dur": 0.46, + "midi": 66, + "note": "F#4" + }, + { + "t": 114.635, + "dur": 0.23, + "midi": 64, + "note": "E4" + }, + { + "t": 114.865, + "dur": 0.15, + "midi": 61, + "note": "C#4" + }, + { + "t": 115.015, + "dur": 0.535, + "midi": 63, + "note": "D#4" + }, + { + "t": 115.55, + "dur": 0.33, + "midi": 59, + "note": "B3" + }, + { + "t": 115.97, + "dur": 0.15, + "midi": 66, + "note": "F#4" + }, + { + "t": 116.14, + "dur": 0.315, + "midi": 66, + "note": "F#4" + }, + { + "t": 116.455, + "dur": 0.575, + "midi": 64, + "note": "E4" + }, + { + "t": 117.03, + "dur": 0.215, + "midi": 63, + "note": "D#4" + }, + { + "t": 117.245, + "dur": 0.28, + "midi": 59, + "note": "B3" + }, + { + "t": 117.79, + "dur": 0.355, + "midi": 58, + "note": "A#3" + }, + { + "t": 118.145, + "dur": 0.16, + "midi": 66, + "note": "F#4" + }, + { + "t": 118.305, + "dur": 0.13, + "midi": 64, + "note": "E4" + }, + { + "t": 118.435, + "dur": 0.87, + "midi": 63, + "note": "D#4" + }, + { + "t": 119.305, + "dur": 0.395, + "midi": 59, + "note": "B3" + }, + { + "t": 119.7, + "dur": 0.35, + "midi": 61, + "note": "C#4" + }, + { + "t": 120.05, + "dur": 0.455, + "midi": 66, + "note": "F#4" + }, + { + "t": 120.505, + "dur": 0.44, + "midi": 64, + "note": "E4" + }, + { + "t": 120.945, + "dur": 0.395, + "midi": 63, + "note": "D#4" + }, + { + "t": 121.34, + "dur": 0.84, + "midi": 59, + "note": "B3" + }, + { + "t": 122.18, + "dur": 0.185, + "midi": 66, + "note": "F#4" + }, + { + "t": 122.365, + "dur": 0.495, + "midi": 64, + "note": "E4" + }, + { + "t": 122.86, + "dur": 0.515, + "midi": 63, + "note": "D#4" + }, + { + "t": 123.375, + "dur": 0.315, + "midi": 61, + "note": "C#4" + }, + { + "t": 123.69, + "dur": 0.42, + "midi": 66, + "note": "F#4" + }, + { + "t": 124.11, + "dur": 0.255, + "midi": 64, + "note": "E4" + }, + { + "t": 124.365, + "dur": 0.175, + "midi": 66, + "note": "F#4" + }, + { + "t": 124.54, + "dur": 0.22, + "midi": 64, + "note": "E4" + }, + { + "t": 124.76, + "dur": 0.24, + "midi": 63, + "note": "D#4" + }, + { + "t": 125.0, + "dur": 0.205, + "midi": 59, + "note": "B3" + }, + { + "t": 125.205, + "dur": 0.13, + "midi": 61, + "note": "C#4" + }, + { + "t": 125.45, + "dur": 0.155, + "midi": 52, + "note": "E3" + }, + { + "t": 125.605, + "dur": 0.145, + "midi": 58, + "note": "A#3" + }, + { + "t": 125.915, + "dur": 0.325, + "midi": 66, + "note": "F#4" + }, + { + "t": 126.24, + "dur": 0.775, + "midi": 63, + "note": "D#4" + }, + { + "t": 127.015, + "dur": 0.48, + "midi": 59, + "note": "B3" + }, + { + "t": 127.495, + "dur": 0.215, + "midi": 61, + "note": "C#4" + }, + { + "t": 127.82, + "dur": 0.19, + "midi": 66, + "note": "F#4" + }, + { + "t": 128.01, + "dur": 0.34, + "midi": 64, + "note": "E4" + }, + { + "t": 128.35, + "dur": 0.965, + "midi": 63, + "note": "D#4" + }, + { + "t": 129.315, + "dur": 0.245, + "midi": 59, + "note": "B3" + }, + { + "t": 129.635, + "dur": 0.185, + "midi": 64, + "note": "E4" + }, + { + "t": 129.87, + "dur": 0.225, + "midi": 66, + "note": "F#4" + }, + { + "t": 130.095, + "dur": 0.595, + "midi": 64, + "note": "E4" + }, + { + "t": 130.69, + "dur": 0.35, + "midi": 63, + "note": "D#4" + }, + { + "t": 131.04, + "dur": 0.19, + "midi": 59, + "note": "B3" + }, + { + "t": 131.23, + "dur": 0.15, + "midi": 68, + "note": "G#4" + }, + { + "t": 131.4, + "dur": 0.215, + "midi": 63, + "note": "D#4" + }, + { + "t": 131.615, + "dur": 0.395, + "midi": 66, + "note": "F#4" + }, + { + "t": 132.01, + "dur": 0.26, + "midi": 64, + "note": "E4" + }, + { + "t": 132.3, + "dur": 0.3, + "midi": 64, + "note": "E4" + }, + { + "t": 132.6, + "dur": 0.315, + "midi": 63, + "note": "D#4" + }, + { + "t": 132.915, + "dur": 0.195, + "midi": 61, + "note": "C#4" + }, + { + "t": 133.315, + "dur": 0.245, + "midi": 59, + "note": "B3" + }, + { + "t": 133.855, + "dur": 0.22, + "midi": 66, + "note": "F#4" + }, + { + "t": 134.075, + "dur": 0.515, + "midi": 64, + "note": "E4" + }, + { + "t": 134.59, + "dur": 0.385, + "midi": 63, + "note": "D#4" + }, + { + "t": 134.975, + "dur": 0.505, + "midi": 59, + "note": "B3" + }, + { + "t": 135.48, + "dur": 0.245, + "midi": 66, + "note": "F#4" + }, + { + "t": 135.795, + "dur": 0.235, + "midi": 66, + "note": "F#4" + }, + { + "t": 136.03, + "dur": 0.5, + "midi": 64, + "note": "E4" + }, + { + "t": 136.53, + "dur": 0.145, + "midi": 61, + "note": "C#4" + }, + { + "t": 136.675, + "dur": 4.25, + "midi": 63, + "note": "D#4" + }, + { + "t": 151.065, + "dur": 0.535, + "midi": 58, + "note": "A#3" + }, + { + "t": 151.6, + "dur": 0.165, + "midi": 66, + "note": "F#4" + }, + { + "t": 151.765, + "dur": 0.675, + "midi": 64, + "note": "E4" + }, + { + "t": 152.44, + "dur": 0.32, + "midi": 63, + "note": "D#4" + }, + { + "t": 152.76, + "dur": 0.44, + "midi": 59, + "note": "B3" + }, + { + "t": 153.2, + "dur": 0.56, + "midi": 66, + "note": "F#4" + }, + { + "t": 153.76, + "dur": 0.53, + "midi": 64, + "note": "E4" + }, + { + "t": 154.29, + "dur": 0.395, + "midi": 63, + "note": "D#4" + }, + { + "t": 154.685, + "dur": 0.29, + "midi": 64, + "note": "E4" + }, + { + "t": 155.28, + "dur": 0.305, + "midi": 63, + "note": "D#4" + }, + { + "t": 155.585, + "dur": 0.51, + "midi": 64, + "note": "E4" + }, + { + "t": 156.095, + "dur": 0.395, + "midi": 63, + "note": "D#4" + }, + { + "t": 156.605, + "dur": 0.36, + "midi": 59, + "note": "B3" + }, + { + "t": 156.965, + "dur": 0.28, + "midi": 61, + "note": "C#4" + }, + { + "t": 157.245, + "dur": 0.42, + "midi": 64, + "note": "E4" + }, + { + "t": 157.665, + "dur": 0.26, + "midi": 63, + "note": "D#4" + }, + { + "t": 157.925, + "dur": 0.3, + "midi": 64, + "note": "E4" + }, + { + "t": 158.225, + "dur": 0.29, + "midi": 63, + "note": "D#4" + }, + { + "t": 158.515, + "dur": 0.225, + "midi": 59, + "note": "B3" + }, + { + "t": 159.185, + "dur": 0.235, + "midi": 64, + "note": "E4" + }, + { + "t": 159.42, + "dur": 0.19, + "midi": 66, + "note": "F#4" + }, + { + "t": 159.61, + "dur": 0.55, + "midi": 64, + "note": "E4" + }, + { + "t": 160.16, + "dur": 0.355, + "midi": 63, + "note": "D#4" + }, + { + "t": 160.595, + "dur": 0.285, + "midi": 58, + "note": "A#3" + }, + { + "t": 161.005, + "dur": 0.52, + "midi": 66, + "note": "F#4" + }, + { + "t": 161.595, + "dur": 0.47, + "midi": 64, + "note": "E4" + }, + { + "t": 162.065, + "dur": 2.73, + "midi": 63, + "note": "D#4" + }, + { + "t": 164.795, + "dur": 0.245, + "midi": 61, + "note": "C#4" + }, + { + "t": 165.04, + "dur": 2.23, + "midi": 59, + "note": "B3" + } + ] +} \ No newline at end of file diff --git a/slab/menubar-swift/Sources/SlabMenubar/AXTiler.swift b/slab/menubar-swift/Sources/SlabMenubar/AXTiler.swift index 312ccf2a98..6be1ccc76e 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/AXTiler.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/AXTiler.swift @@ -47,8 +47,12 @@ enum AXTiler { /// column of its own rather than a grid cell. Only the first stage /// window is staged; any extra ones fall into the ordinary grid. let stage: [Window] + /// Wizard windows (SyllaWizard, JukeWizard, …): ordinary grid cells, + /// found by executable name because the wizards are bare SwiftPM + /// binaries with no bundle identifier. + let wizards: [Window] - var all: [Window] { iterm + terminal + acPanes + stage } + var all: [Window] { iterm + terminal + acPanes + wizards + stage } var signature: [CGWindowID] { all.map(\.id).sorted() } } @@ -58,6 +62,32 @@ enum AXTiler { /// measuring what the window accepted rather than assuming. static let stageBundleIDs = [GameMode.gfnBundleID] + /// The wizard roster (date-wizard/…/WizardRoster.swift plus the wizards + /// it omits), by executable name. `swift build` products carry no + /// CFBundleIdentifier, so `NSRunningApplication` is matched on the + /// binary's file name instead. Every wizard window tiles as an equal + /// grid cell, the same way GeForce NOW joins the grid. + static let wizardExecutables: [String] = [ + "ChartWizard", "ClipWizard", "DateWizard", "GlyphWizard", "JukeWizard", + "NarratorWizard", "ShotWizard", "SyllaWizard", "WaveWizard", + ] + + private static func wizardApps(_ exe: String) -> [NSRunningApplication] { + NSWorkspace.shared.runningApplications.filter { + $0.executableURL?.lastPathComponent == exe && !$0.isTerminated + } + } + + private static func wizardWindowRefs(liveWindows: [LiveWindow], + requireGeometry: Bool = true) -> [Window] { + wizardExecutables.flatMap { exe in + let apps = wizardApps(exe) + guard !apps.isEmpty else { return [Window]() } + return windowRefs(bundleId: "wizard:\(exe)", liveWindows: liveWindows, + requireGeometry: requireGeometry, apps: apps) + } + } + /// Leave native full screen (own Space) so the window can be framed on /// this Space. Returns true when the attribute was set and cleared. @discardableResult @@ -98,7 +128,8 @@ enum AXTiler { + easelWindowRefs(liveWindows: liveWindows), stage: stageBundleIDs.flatMap { windowRefs(bundleId: $0, liveWindows: liveWindows) - } + }, + wizards: wizardWindowRefs(liveWindows: liveWindows) ) } @@ -116,6 +147,7 @@ enum AXTiler { + windowRefs(bundleId: "computer.aesthetic.app", requireStandardSubrole: false, liveWindows: liveWindows, requireGeometry: false) + easelWindowRefs(liveWindows: liveWindows, requireGeometry: false) + + wizardWindowRefs(liveWindows: liveWindows, requireGeometry: false) + stageBundleIDs.flatMap { windowRefs(bundleId: $0, liveWindows: liveWindows, requireGeometry: false) } @@ -152,8 +184,11 @@ enum AXTiler { requireStandardSubrole: Bool = true, liveWindows: [LiveWindow], requireGeometry: Bool = true, - requireEaselIdentity: Bool = false) -> [Window] { - let apps = NSRunningApplication.runningApplications(withBundleIdentifier: bundleId) + requireEaselIdentity: Bool = false, + apps explicitApps: [NSRunningApplication]? = nil) -> [Window] { + // `bundleId` is the census/cache key; a bundle-less app (a wizard) + // passes its processes explicitly under a synthetic key. + let apps = explicitApps ?? NSRunningApplication.runningApplications(withBundleIdentifier: bundleId) let liveIDs = Set(liveWindows.map(\.id)) var out: [Window] = [] var seen = Set() diff --git a/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift b/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift index bd6527c947..a91318e5ff 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift @@ -4024,11 +4024,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { @discardableResult private static func axTilePass(snapshot: AXTiler.Snapshot, geom: ScreenGeom, textSize: TextSize) -> AXPass? { - NSLog("🧩 [tile] windows=%d ids=%@ iterm=%d term=%d acpane=%d stage=%d", + NSLog("🧩 [tile] windows=%d ids=%@ iterm=%d term=%d acpane=%d wizard=%d stage=%d", snapshot.all.count, snapshot.signature.map(String.init).joined(separator: ","), snapshot.iterm.count, snapshot.terminal.count, snapshot.acPanes.count, - snapshot.stage.count) + snapshot.wizards.count, snapshot.stage.count) // A stage window (GeForce NOW) is an ordinary, equal grid cell first. // Only when the app clamps above its cell — its configured floor is // bigger than the grid can offer — does it get a column of its own, diff --git a/sylla-wizard/Sources/SyllaWizard/main.swift b/sylla-wizard/Sources/SyllaWizard/main.swift index a33b4ec980..ddd66f393a 100644 --- a/sylla-wizard/Sources/SyllaWizard/main.swift +++ b/sylla-wizard/Sources/SyllaWizard/main.swift @@ -6,6 +6,13 @@ // from pop/imab/out/imab-sets-successive.mp3 // Edits: pop/imab/processed-boundaries-.json and set-fixes/.json // +// swift run --package-path sylla-wizard SyllaWizard --spec pop//src/sylla/spec.json [n] +// +// SPEC mode opens any lane's vocal: the spec lists takes, each with its own clip +// (audio), seed words (fix: {"words":[{text,fromMs,toMs}]}, ms into the clip), and +// where the drawn syllables go (out). Nothing IMAB is read. The lane collects the +// saved words back into its own timebase (sailor-song: bin/sylla-collect.py). +// // A syllable is a box in two dimensions: when it happens and which part of // the spectrum it occupies. The queue build had collapsed the second one — // it kept fLo/fHi in the schema and wrote them to every export, but hardcoded @@ -22,6 +29,9 @@ import AppKit import AVFoundation let PXS: CGFloat = 260 +let PAPER = NSColor(calibratedRed: 0.97, green: 0.955, blue: 0.93, alpha: 1) +let INK = NSColor(calibratedRed: 0.17, green: 0.12, blue: 0.2, alpha: 1) +let LINK_MS = 15 // edges this close are one separator: moving it moves both words let SPEC_TOP: CGFloat = 44 var SYLS: [(String, Int)] = [ ("i'm", 0), ("a", 1), ("but", 2), ("ter", 2), ("fly", 2), @@ -51,10 +61,52 @@ struct Take { let date: String let title: String let role: String? + var audio: URL? = nil // SPEC mode: this take's own clip, and its sidecars + var fix: URL? = nil + var out: URL? = nil + var drawn: URL? = nil +} + +func argument(after flag: String) -> String? { + let args = CommandLine.arguments + guard let i = args.firstIndex(of: flag), i + 1 < args.count else { return nil } + return args[i + 1] +} +let specURL: URL? = argument(after: "--spec").map { path in + path.hasPrefix("/") ? URL(fileURLWithPath: path) : URL(fileURLWithPath: FileManager.default.currentDirectoryPath).appendingPathComponent(path) +} +let spec: [String: Any]? = specURL.flatMap { jsonObject($0) } +func specPath(_ value: Any?) -> URL? { + guard let path = value as? String else { return nil } + return path.hasPrefix("/") ? URL(fileURLWithPath: path) : repo.appendingPathComponent(path) +} +func fixURL(_ take: Take) -> URL { take.fix ?? repo.appendingPathComponent("pop/imab/set-fixes/\(take.id).json") } +func outURLFor(_ take: Take) -> URL { take.out ?? repo.appendingPathComponent("pop/imab/processed-boundaries-\(take.id).json") } +func drawnURLFor(_ take: Take) -> URL { take.drawn ?? repo.appendingPathComponent("pop/imab/boundaries-drawn-\(take.id).json") } +func assetsDir(_ take: Take) -> URL { + let name = (spec?["name"] as? String) ?? "imab" + return home.appendingPathComponent(".cache/ac/\(name == "imab" ? "imab" : "sylla/" + name)/takes/\(take.id)") } let leadTake = "7311159624588070175" +/// Track mode: the spec names the whole vocal (`"track": "