Skip to content

Commit

Permalink
match algorithms to praat's
Browse files Browse the repository at this point in the history
  • Loading branch information
hlorenzi committed Aug 12, 2024
1 parent 27f4518 commit af9ea1e
Show file tree
Hide file tree
Showing 9 changed files with 524 additions and 58 deletions.
20 changes: 11 additions & 9 deletions src/AnalysisChart.tsx
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))

Expand All @@ -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
Expand Down Expand Up @@ -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++)
Expand Down
4 changes: 3 additions & 1 deletion src/RecordingPanel.tsx
Original file line number Diff line number Diff line change
@@ -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"


Expand Down Expand Up @@ -199,6 +199,8 @@ async function importWav(state: State)
state.sampleBuffer.set(waveform, 0)
state.recordingIndex = waveform.length
recordingFinish(state)

testExtractFormants([...waveform], buffer.sampleRate)
}


Expand Down
76 changes: 56 additions & 20 deletions src/VowelChart.tsx
Original file line number Diff line number Diff line change
@@ -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"


Expand Down Expand Up @@ -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))
})


Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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)
}
Expand All @@ -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)
}

Expand All @@ -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]
Expand All @@ -294,14 +297,15 @@ 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)

// 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(
Expand All @@ -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()
}
11 changes: 11 additions & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 0 additions & 4 deletions src/data.ts

This file was deleted.

19 changes: 13 additions & 6 deletions src/formantExtractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}

Expand Down Expand Up @@ -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)
}


Expand All @@ -101,7 +105,7 @@ function preemphasisFilter(
}


function praatPreemphasis(
export function praatPreemphasis(
array: Float32Array,
samplingFrequency: number)
{
Expand Down Expand Up @@ -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)
Expand All @@ -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 })
}

Expand Down
5 changes: 3 additions & 2 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ function Page()
"margin": "auto",
"text-align": "center",
}}>
Click and drag to synthesize vowel sounds via formant frequencies.<br/><br/>
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.<br/>
<br/>
Red: computed frequency-domain data<br/>
Blue: computed formant frequencies<br/>
<br/>
<VowelChart synth={ synth() }/>
<br/>
Expand Down
Loading

0 comments on commit af9ea1e

Please sign in to comment.