// Measure f0 for a handful of clips the palette never measured. // // The "and um" clips carry f0 0 and have no corepitch entry, which is exactly why // the arranger has never been able to pick one: it matches on pitch, and these have // none recorded. They are two-syllable, so a single f0 over the whole clip is // meaningless -- the pitch that matters is the SUSTAINED second syllable, which is // what actually rings on the note. So measure the back half only. import { readFileSync, readdirSync } from "node:fs"; import path from "node:path"; import { regionPitch } from "./pitch.mjs"; const DIR = path.resolve(path.dirname(new URL(import.meta.url).pathname)); const SR = 48000; function readWav(file) { const b = readFileSync(file); let p = 12, fmt = null, data = null; while (p + 8 <= b.length) { const id = b.toString("latin1", p, p + 4), sz = b.readUInt32LE(p + 4), body = p + 8; if (id === "fmt ") fmt = { ch: b.readUInt16LE(body + 2), sr: b.readUInt32LE(body + 4), bits: b.readUInt16LE(body + 14) }; if (id === "data") { data = b.subarray(body, body + sz); break; } p = body + sz + (sz & 1); } const by = fmt.bits / 8, n = Math.floor(data.length / by / fmt.ch), x = new Float32Array(n); for (let i = 0; i < n; i += 1) { let s = 0; for (let c = 0; c < fmt.ch; c += 1) s += data.readInt16LE((i * fmt.ch + c) * by) / 32768; x[i] = s / fmt.ch; } return { x, sr: fmt.sr }; } let all = []; for (const f of readdirSync(path.join(DIR, "cand2")).filter((f) => f.endsWith(".json"))) { all.push(...JSON.parse(readFileSync(path.join(DIR, "cand2", f), "utf8")).candidates); } const midiOf = (hz) => 69 + 12 * Math.log2(hz / 440); const NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; const nm = (m) => `${NAMES[((Math.round(m) % 12) + 12) % 12]}${Math.floor(Math.round(m) / 12) - 1}`; for (const K of process.argv.slice(2)) { const c = all.find((x) => `${x.video}@${(+x.start).toFixed(2)}` === K); if (!c) { console.log(`${K} NOT FOUND`); continue; } const { x, sr } = readWav(path.join(DIR, "wav48", `${c.video}.wav`)); const a = Math.round(c.start * sr), b = Math.round(c.end * sr); const seg = x.subarray(a, b); // back half = the sustained vowel, past the consonant onset const half = Math.floor(seg.length / 2); const r = regionPitch(seg, half, seg.length - 1, sr, 55, 260); const whole = regionPitch(seg, Math.round(0.01 * sr), seg.length - 1, sr, 55, 260); console.log(`${K.padEnd(24)} dur ${(c.end - c.start).toFixed(3)}s ` + `back-half f0 ${r.f0 ? r.f0.toFixed(1) : "none"}Hz` + (r.f0 ? ` (MIDI ${midiOf(r.f0).toFixed(2)} ${nm(midiOf(r.f0))})` : "") + ` whole-clip f0 ${whole.f0 ? whole.f0.toFixed(1) : "none"}Hz`); }