From af9ea1ea08204cf4c121ce76cc9172a262391a54 Mon Sep 17 00:00:00 2001 From: hlorenzi Date: Sun, 11 Aug 2024 21:02:59 -0300 Subject: [PATCH] match algorithms to praat's --- src/AnalysisChart.tsx | 20 +- src/RecordingPanel.tsx | 4 +- src/VowelChart.tsx | 76 ++++++-- src/common.ts | 11 ++ src/data.ts | 4 - src/formantExtractor.ts | 19 +- src/main.tsx | 5 +- src/test.ts | 423 ++++++++++++++++++++++++++++++++++++++-- src/vowelSynth.ts | 20 +- 9 files changed, 524 insertions(+), 58 deletions(-) delete mode 100644 src/data.ts diff --git a/src/AnalysisChart.tsx b/src/AnalysisChart.tsx index d32d7c1..7421282 100644 --- a/src/AnalysisChart.tsx +++ b/src/AnalysisChart.tsx @@ -1,7 +1,7 @@ import * as Solid from "solid-js" import { VowelSynth } from "./vowelSynth.ts" import { extractFormants } from "./formantExtractor.ts" -import * as Data from "./data.ts" +import * as Common from "./common.ts" export function AnalysisChart(props: { @@ -32,8 +32,8 @@ function mapValueToView( x: number, w: number) { - const min = Data.f1Min - 100 - const max = Data.f2Max + 500 + const min = Common.f1Min - 100 + const max = Common.f2Max + 500 const p = (x - min) / (max - min) const logScale = 10 const t = Math.log((logScale - 1) * p + 1) / Math.log(logScale) @@ -76,7 +76,7 @@ function draw( canvasCtx.fillStyle = "#000" canvasCtx.textAlign = "left" canvasCtx.textBaseline = "top" - for (let freq = Data.f2Min + 500; freq <= Data.f2Max; freq += 500) + for (let freq = Common.f2Min; freq <= Common.f2Max; freq += 500) { const x = Math.floor(mapValueToView(freq, w)) @@ -88,14 +88,14 @@ function draw( } canvasCtx.lineWidth = 2 - canvasCtx.strokeStyle = "#f20" + canvasCtx.strokeStyle = Common.colorFrequencyDomain canvasCtx.beginPath() for (let i = 0; i < freqData.length; i++) { - const freq = i / freqData.length * (synth.ctx.sampleRate / 2) - if (freq < Data.f1Min - 100) + const freq = i / freqData.length * (synth.ctx.sampleRate / 1) + if (freq < Common.f1Min - 100) continue - if (freq > Data.f2Max + 500) + if (freq > Common.f2Max + 500) break const v = freqData[i] / 255 @@ -128,8 +128,10 @@ function draw( canvasCtx.stroke() const formants = extractFormants(timeData, synth.ctx.sampleRate) + synth.cacheFormants(formants) + canvasCtx.lineWidth = 2 - canvasCtx.strokeStyle = "#02f" + canvasCtx.strokeStyle = Common.colorFormants canvasCtx.globalAlpha = 0.75 canvasCtx.beginPath() for (let i = 0; i < formants.length; i++) diff --git a/src/RecordingPanel.tsx b/src/RecordingPanel.tsx index f75de59..5aa9aae 100644 --- a/src/RecordingPanel.tsx +++ b/src/RecordingPanel.tsx @@ -1,8 +1,8 @@ import * as Solid from "solid-js" import { VowelSynth } from "./vowelSynth.ts" -import * as Data from "./data.ts" import * as Common from "./common.ts" import * as Wav from "./wavEncode.ts" +import { testExtractFormants } from "./test.ts" //import * as Styled from "solid-styled-components" @@ -199,6 +199,8 @@ async function importWav(state: State) state.sampleBuffer.set(waveform, 0) state.recordingIndex = waveform.length recordingFinish(state) + + testExtractFormants([...waveform], buffer.sampleRate) } diff --git a/src/VowelChart.tsx b/src/VowelChart.tsx index 29a4269..64a8ff1 100644 --- a/src/VowelChart.tsx +++ b/src/VowelChart.tsx @@ -1,6 +1,7 @@ import * as Solid from "solid-js" import { VowelSynth } from "./vowelSynth.ts" -import * as Data from "./data.ts" +import * as Common from "./common.ts" +import { extractFormants } from "./formantExtractor.ts" //import * as Styled from "solid-styled-components" @@ -32,7 +33,7 @@ export function VowelChart(props: { window.addEventListener("touchend", (ev) => mouseUp(state, ev)); window.addEventListener("touchcancel", (ev) => mouseUp(state, ev)); window.addEventListener("touchmove", (ev) => mouseMove(state, ev)); - window.requestAnimationFrame(() => draw(state)) + window.requestAnimationFrame(() => draw(props.synth, state)) }) @@ -94,8 +95,8 @@ function updateMousePos( state.mousePosNormalized = { x, y } state.mousePosFormants = { - f1: Math.floor(mapViewToValue(y, Data.f1Min, Data.f1Max)), - f2: Math.floor(mapViewToValue(1 - x, Data.f2Min, Data.f2Max)), + f1: Math.floor(mapViewToValue(y, Common.f1Min, Common.f1Max)), + f2: Math.floor(mapViewToValue(1 - x, Common.f2Min, Common.f2Max)), } /*console.log( @@ -189,9 +190,10 @@ const isMobile = window.matchMedia("(pointer: coarse)").matches function draw( + synth: VowelSynth, state: State) { - window.requestAnimationFrame(() => draw(state)) + window.requestAnimationFrame(() => draw(synth, state)) const pixelRatio = window.devicePixelRatio const rect = state.canvas.getBoundingClientRect() @@ -216,10 +218,10 @@ function draw( state.ctx.fillStyle = "#ccc" state.ctx.beginPath() state.ctx.moveTo(w, h) - for (let freq = Data.f2Min; freq <= Data.f1Max; freq += 100) + for (let freq = Common.f2Min; freq <= Common.f1Max; freq += 100) { - const x = w - mapValueToView(freq, Data.f2Min, Data.f2Max) * w - const y = mapValueToView(freq, Data.f1Min, Data.f1Max) * h + const x = w - mapValueToView(freq, Common.f2Min, Common.f2Max) * w + const y = mapValueToView(freq, Common.f1Min, Common.f1Max) * h state.ctx.lineTo(x, y) } state.ctx.fill() @@ -232,28 +234,28 @@ function draw( state.ctx.fillStyle = "#000" state.ctx.textAlign = "left" state.ctx.textBaseline = "top" - for (let freq = Data.f2Min + 500; freq < Data.f2Max; freq += 500) + for (let freq = Common.f2Min + 500; freq < Common.f2Max; freq += 500) { - const x = Math.floor(w - mapValueToView(freq, Data.f2Min, Data.f2Max) * w) + const x = Math.floor(w - mapValueToView(freq, Common.f2Min, Common.f2Max) * w) state.ctx.beginPath() state.ctx.moveTo(x, 0) state.ctx.lineTo(x, h) state.ctx.stroke() state.ctx.fillText( - freq == Data.f2Min + 500 ? `F2 = ${ freq } Hz` : `${ freq }`, + freq == Common.f2Min + 500 ? `F2 = ${ freq } Hz` : `${ freq }`, x + 2, 2) } state.ctx.textBaseline = "bottom" - for (let freq = Data.f1Min + 200; freq <= Data.f1Max; freq += 200) + for (let freq = Common.f1Min + 200; freq <= Common.f1Max; freq += 200) { - const y = Math.floor(mapValueToView(freq, Data.f1Min, Data.f1Max) * h) + const y = Math.floor(mapValueToView(freq, Common.f1Min, Common.f1Max) * h) state.ctx.beginPath() state.ctx.moveTo(0, y) state.ctx.lineTo(w, y) state.ctx.stroke() state.ctx.fillText( - freq == Data.f1Max ? `F1 = ${ freq } Hz` : `${ freq }`, + freq == Common.f1Max ? `F1 = ${ freq } Hz` : `${ freq }`, 2, y - 2) } @@ -265,8 +267,8 @@ function draw( state.ctx.textBaseline = "middle" for (const vowel of ipaVowels) { - const x = Math.floor(w - mapValueToView(vowel.f2, Data.f2Min, Data.f2Max) * w) - const y = Math.floor(mapValueToView(vowel.f1, Data.f1Min, Data.f1Max) * h) + const x = Math.floor(w - mapValueToView(vowel.f2, Common.f2Min, Common.f2Max) * w) + const y = Math.floor(mapValueToView(vowel.f1, Common.f1Min, Common.f1Max) * h) state.ctx.fillText(vowel.symbol, x, y) } @@ -277,7 +279,8 @@ function draw( { state.ctx.beginPath() const timer = Math.max(0, state.mousePath[p].timer) - state.ctx.strokeStyle = `rgb(0 40 255 / ${ 0.25 + 0.75 * timer })` + state.ctx.strokeStyle = Common.colorSynth + state.ctx.globalAlpha = 0.25 + 0.75 * timer const pA = state.mousePath[p - 1] const pB = state.mousePath[p] @@ -294,6 +297,7 @@ function draw( state.ctx.lineTo(pA.x * w + vecYN * crossSize, pA.y * h - vecXN * crossSize) state.ctx.stroke() + state.ctx.globalAlpha = 1 } state.mousePath.forEach((p) => p.timer -= 1 / 30) @@ -301,7 +305,7 @@ function draw( // Draw mouse if (state.mouseDown) { - state.ctx.strokeStyle = "rgb(0 40 255)" + state.ctx.strokeStyle = Common.colorSynth state.ctx.lineWidth = 2 state.ctx.beginPath() state.ctx.arc( @@ -316,11 +320,43 @@ function draw( state.ctx.textAlign = "center" state.ctx.textBaseline = "bottom" state.ctx.fillText( - `(${ state.mousePosFormants.f1 }, ` + - `${ state.mousePosFormants.f2 } Hz)`, + `(${ state.mousePosFormants.f1.toFixed(0) }, ` + + `${ state.mousePosFormants.f2.toFixed(0) } Hz)`, state.mousePosNormalized.x * w, state.mousePosNormalized.y * h + (isMobile ? -120 : -10)) } + + const formants = synth.getCachedFormants() + if (formants.length >= 2) + { + const f1 = formants[0] + const f2 = formants[1] + + const x = Math.floor(w - mapValueToView(f2, Common.f2Min, Common.f2Max) * w) + const y = Math.floor(mapValueToView(f1, Common.f1Min, Common.f1Max) * h) + + state.ctx.strokeStyle = Common.colorFormants + state.ctx.fillStyle = Common.colorFormants + state.ctx.lineWidth = 2 + state.ctx.beginPath() + state.ctx.arc( + x, + y, + 2, + 0, + Math.PI * 2) + state.ctx.stroke() + + state.ctx.font = `${ isMobile ? "1.5em" : "0.75em" } Times New Roman` + state.ctx.textAlign = "center" + state.ctx.textBaseline = "bottom" + state.ctx.fillText( + `(${ f1.toFixed(0) }, ` + + `${ f2.toFixed(0) } Hz)`, + x, + y) + } + state.ctx.restore() } \ No newline at end of file diff --git a/src/common.ts b/src/common.ts index 6d084aa..f256f63 100644 --- a/src/common.ts +++ b/src/common.ts @@ -1,3 +1,14 @@ +export const f1Min = 200 +export const f1Max = 1200 +export const f2Min = 500 +export const f2Max = 3500 + + +export const colorSynth = "#284" +export const colorFormants = "#02f" +export const colorFrequencyDomain = "#f20" + + export function canvasResize(canvas: HTMLCanvasElement) { const pixelRatio = window.devicePixelRatio || 1 diff --git a/src/data.ts b/src/data.ts deleted file mode 100644 index e829e94..0000000 --- a/src/data.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const f1Min = 200 -export const f1Max = 1200 -export const f2Min = 500 -export const f2Max = 3500 \ No newline at end of file diff --git a/src/formantExtractor.ts b/src/formantExtractor.ts index d1f4eca..818f7e8 100644 --- a/src/formantExtractor.ts +++ b/src/formantExtractor.ts @@ -15,6 +15,10 @@ export function extractFormants( if (sample.every(s => s === 0)) return [] + console.log("== extractFormants ==") + console.log("samplingFrequency", samplingFrequency, "Hz") + console.log("sample length", sample.length, "samples =", sample.length / samplingFrequency, "seconds") + const samplePreemphasized = //preemphasisFilter(sampleWindowed) praatPreemphasis(sample, samplingFrequency) @@ -34,7 +38,7 @@ export function extractFormants( .map(c => praatFixRootToUnitCircle(c)) const formants = rootsToFormants(roots, samplingFrequency) - console.log("formants", formants) + console.log("formants", formants.map(f => f.frequency.toFixed(0).padStart(4, " ")).join(", "), "Hz", formants) return formants.map(f => f.frequency) } @@ -73,14 +77,14 @@ function hammingWindow( } -function praatGaussianWindow( +export function praatGaussianWindow( n: number, nMax: number) { + n += 1 const nMid = 0.5 * (nMax + 1) const edge = Math.exp(-12.0) - return (Math.exp(-48.0 * (n - nMid) * (n - nMid) / (nMax + 1) / (nMax + 1)) - edge) / - (1.0 - edge) + return (Math.exp(-48.0 * (n - nMid) * (n - nMid) / (nMax + 1) / (nMax + 1)) - edge) / (1.0 - edge) } @@ -101,7 +105,7 @@ function preemphasisFilter( } -function praatPreemphasis( +export function praatPreemphasis( array: Float32Array, samplingFrequency: number) { @@ -153,6 +157,7 @@ export function rootsToFormants( : Formant[] { const nyquistFrequency = samplingFrequency / 2 + const safetyMargin = 50 const frequencies = roots .map(c => Math.abs(Math.atan2(c.imag, c.real)) * nyquistFrequency / Math.PI) @@ -165,7 +170,9 @@ export function rootsToFormants( { const frequency = frequencies[i] const bandwidth = bandwidths[i] - //if (frequency > 90 && frequency < 3500 && bandwidth < 1000) + if (frequency > safetyMargin && + frequency < nyquistFrequency - safetyMargin && + frequency < 3500) formants.push({ frequency, bandwidth }) } diff --git a/src/main.tsx b/src/main.tsx index 1e56e9c..b6d4b92 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -22,9 +22,10 @@ function Page() "margin": "auto", "text-align": "center", }}> - Click and drag to synthesize vowel sounds via formant frequencies.

- The blue bars on the bottom chart shows formant frequencies extracted from the waveform data. (Not working properly) + Click and drag around the top chart to synthesize vowel sounds via formant frequencies.

+ Red: computed frequency-domain data
+ Blue: computed formant frequencies


diff --git a/src/test.ts b/src/test.ts index f7a0614..bb14a4d 100644 --- a/src/test.ts +++ b/src/test.ts @@ -1,5 +1,5 @@ import { forwardLinearPrediction, praatBurgMethod } from "./lpc.ts" -import { praatFixRootToUnitCircle, praatLpcToPolynomial, rootsToFormants } from "./formantExtractor.ts" +import { praatFixRootToUnitCircle, praatGaussianWindow, praatLpcToPolynomial, praatPreemphasis, rootsToFormants } from "./formantExtractor.ts" import { findRootsOfPolynomial, Complex } from "./roots.ts" @@ -377,20 +377,415 @@ const data = [ ] -const lpc = praatBurgMethod(new Float32Array(data), 10) -console.log("lpc", lpc) +const data2 = [ + 0.001366, + 0.003476, + 0.001267, + 0.005753, + -0.019464, + -0.100137, + -0.124634, + -0.090486, + -0.022001, + 0.040711, + 0.073084, + 0.067449, + 0.038528, + 0.005065, + -0.016519, + -0.023300, + -0.019323, + -0.013170, + -0.008315, + -0.004817, + 0.001071, + 0.010109, + 0.020205, + 0.025492, + 0.021955, + 0.009186, + -0.007365, + -0.020336, + -0.023219, + -0.014987, + 0.000384, + 0.015369, + 0.023504, + 0.021837, + 0.012423, + 0.000365, + -0.008795, + -0.011887, + -0.008869, + -0.002311, + 0.004357, + 0.008717, + 0.009838, + 0.008444, + 0.005685, + 0.002876, + 0.000508, + -0.000969, + -0.001705, + -0.001319, + -0.000049, + 0.002175, + 0.004454, + 0.006250, + 0.006491, + 0.005347, + 0.002910, + 0.000557, + -0.001123, + -0.001103, + 0.000064, + 0.002254, + 0.004032, + 0.005181, + 0.004869, + 0.003950, + 0.002352, + 0.001337, + 0.000578, + 0.000907, + 0.001329, + 0.002359, + 0.002823, + 0.003482, + 0.003303, + 0.003348, + 0.002614, + 0.002446, + 0.001687, + 0.001827, + 0.001457, + 0.002138, + 0.002106, + 0.003017, + 0.002750, + 0.003370, + 0.002499, + 0.002872, + 0.001687, + 0.002412, + 0.001288, + 0.002784, + 0.001420, + 0.003824, + 0.000938, + 0.005702, + -0.045451, + -0.117520, + -0.117889, + -0.070282, + 0.001761, + 0.055200, + 0.075442, + 0.059100, + 0.027213, + -0.004143, + -0.020077, + -0.022989, + -0.016994, + -0.011539, + -0.006962, + -0.003428, + 0.003941, + 0.013440, + 0.022930, + 0.025265, + 0.018703, + 0.003593, + -0.012335, + -0.022730, + -0.021480, + -0.010446, + 0.005920, + 0.018963, + 0.024149, + 0.019233, + 0.008513, + -0.003341, + -0.010459, + -0.011580, + -0.006799, + -0.000053, + 0.006248, + 0.009341, + 0.009696, + 0.007509, + 0.004810, + 0.001919, + 0.000011, + -0.001400, + -0.001610, + -0.001086, + 0.000702, + 0.002874, + 0.005243, + 0.006432, + 0.006347, + 0.004544, + 0.002155, + -0.000213, + -0.001220, + -0.000914, + 0.000818, + 0.002842, + 0.004605, + 0.005139, + 0.004704, + 0.003362, + 0.002025, + 0.000942, + 0.000676, + 0.000947, + 0.001728, + 0.002479, + 0.003137, + 0.003402, + 0.003408, + 0.003061, + 0.002613, + 0.002125, + 0.001759, + 0.001626, + 0.001708, + 0.002069, + 0.002465, + 0.002906, + 0.003029, + 0.003063, + 0.002670, + 0.002435, + 0.001940, + 0.001986, + 0.001792, + 0.002303, + 0.002251, + 0.002826, + 0.002874, + -0.001885, + -0.074512, + -0.125252, + -0.106388, + -0.046794, + 0.022890, + 0.066151, + 0.073310, + 0.049252, + 0.015814, + -0.011343, + -0.022299, + -0.021530, + -0.014912, + -0.009912, + -0.005857, + -0.001472, + 0.006977, + 0.016886, + 0.024747, + 0.024091, + 0.014377, + -0.002023, + -0.016741, + -0.023709, + -0.018685, + -0.005228, + 0.011006, + 0.021705, + 0.023573, + 0.016019, + 0.004430, + -0.006444, + -0.011480, + -0.010549, + -0.004557, + 0.002189, + 0.007724, + 0.009692, + 0.009229, + 0.006559, + 0.003882, + 0.001110, + -0.000464, + -0.001665, + -0.001463, + -0.000679, + 0.001471, + 0.003625, + 0.005872, + 0.006493, + 0.005982, + 0.003695, + 0.001381, + -0.000804, + -0.001187, + -0.000543, + 0.001586, + 0.003418, + 0.005020, + 0.005002, + 0.004421, + 0.002790, + 0.001715, + 0.000654, + 0.000824, + 0.001055, + 0.002113, + 0.002595, + 0.003399, + 0.003297, + 0.003468, + 0.002764, + 0.002609, + 0.001813, + 0.001866, + 0.001438, + 0.002005, + 0.001986, + 0.002851, + 0.002730, + 0.003332, + 0.002664, + 0.002913, + 0.001902, + 0.002338, + 0.001427, + 0.002515, + 0.001568, + 0.003435, + 0.001280, + 0.005749, + -0.019465, + -0.100133, + -0.124639, + -0.090480, + -0.022006, + 0.040717, + 0.073078, + 0.067455, + 0.038524, + 0.005070, + -0.016523, + -0.023296, + -0.019326, + -0.013168, + -0.008316, + -0.004816, + 0.001071, + 0.010108, + 0.020206, + 0.025490, + 0.021958, + 0.009182, + -0.007361, + -0.020341, + -0.023216, + -0.015000, + 0.000395, + 0.015358, + 0.023516, + 0.021824, + 0.012437, + 0.000350, + -0.008779, + -0.011904, + -0.008850, + -0.002330, + 0.004377, + 0.008695, + 0.009861, + 0.008419, + 0.005711, + 0.002848, + 0.000537, + -0.001000, + -0.001672, + -0.001354, + -0.000013, + 0.002137, + 0.004493, + 0.006208, + 0.006535, + 0.005301, + 0.002958, + 0.000506, + -0.001069, + -0.001159, + 0.000123, + 0.002192, + 0.004097, + 0.005113, + 0.004941, + 0.003874, + 0.002431, + 0.001253, + 0.000667, + 0.000813, + 0.001427, + 0.002255, + 0.002934, + 0.003365, + 0.003427, + 0.003216, + 0.002755, + 0.002296, + 0.001847, + 0.001655, + 0.001643, + 0.001937, + 0.002324, + 0.002779, + 0.003011, + 0.003081, + 0.002821, + 0.002510, + 0.002100, + 0.001934, + 0.001853, + 0.002099, + 0.002284, + 0.002683, + 0.002449 +] + + +//testExtractFormants(data2, 11000) + + +export function testExtractFormants(data: number[], samplingFrequency: number) +{ + console.log("testExtractFormants") + console.log("samplingFrequency", samplingFrequency) + console.log("data", data) + + const preemphasized = praatPreemphasis(new Float32Array(data), samplingFrequency) + console.log("preemphasized", preemphasized) + + const window = new Array(preemphasized.length) + .fill(0) + .map((_, i) => praatGaussianWindow(i, preemphasized.length)) + console.log("window", window) + + const windowed = preemphasized.map((s, i) => s * window[i]) + console.log("windowed", windowed) + + const lpc = praatBurgMethod(windowed, 10) + console.log("lpc", lpc) -const polynomial = praatLpcToPolynomial(lpc) -console.log("polynomial", polynomial) -console.log("polynomial", polynomial.map((c, i) => c.toFixed(5) + "*x^" + i).join("+")) + const polynomial = praatLpcToPolynomial(lpc) + console.log("polynomial", polynomial) + console.log("polynomial", polynomial.map((c, i) => c.toFixed(5) + "*x^" + i).join("+")) -const roots = findRootsOfPolynomial(polynomial) -console.log("roots", roots) + const roots = findRootsOfPolynomial(polynomial) + console.log("roots", roots) -const fixedRoots = roots - .filter(c => c.imag >= 0) - .map(c => praatFixRootToUnitCircle(c)) -console.log("fixedRoots", fixedRoots) + const fixedRoots = roots + .filter(c => c.imag >= 0) + .map(c => praatFixRootToUnitCircle(c)) + console.log("fixedRoots", fixedRoots) -const formants = rootsToFormants(fixedRoots, 11000) -console.log("formants", formants) \ No newline at end of file + const formants = rootsToFormants(fixedRoots, samplingFrequency) + console.log("formants", formants.map(f => f.frequency.toFixed(0).padStart(4, " ")).join(", "), "Hz", formants) +} \ No newline at end of file diff --git a/src/vowelSynth.ts b/src/vowelSynth.ts index d8256fc..8ec61d8 100644 --- a/src/vowelSynth.ts +++ b/src/vowelSynth.ts @@ -22,10 +22,12 @@ export class VowelSynth waveformBufferReturn: Float32Array waveformBufferIndex: number + cachedFormants: number[] + static async create(): Promise { - const ctx = new AudioContext() + const ctx = new AudioContext({ sampleRate: 11000 }) await ctx.audioWorklet.addModule("build/audioWorklet.js") return new VowelSynth(ctx) } @@ -85,7 +87,7 @@ export class VowelSynth this.nodeSource = this.ctx.createOscillator() this.nodeSource.type = "sawtooth" - this.nodeSource.frequency.value = 120 + this.nodeSource.frequency.value = 90 this.nodeSource.connect(this.nodeFormant1Filter) this.nodeSource.connect(this.nodeFormant2Filter) this.nodeSource.start() @@ -93,6 +95,8 @@ export class VowelSynth this.recordingBuffer = this.ctx.createBuffer(1, recordingBufferLength, this.ctx.sampleRate) this.recordingPlaybackStartTime = 0 + + this.cachedFormants = [] } @@ -157,6 +161,18 @@ export class VowelSynth } + cacheFormants(formants: number[]) + { + this.cachedFormants = formants + } + + + getCachedFormants(): number[] + { + return this.cachedFormants + } + + getAnalyserData(): Uint8Array { this.nodeAnalyser.getByteFrequencyData(this.nodeAnalyserData)