From 6fc22ec5107bc7fe308d5deafbb22302309bd050 Mon Sep 17 00:00:00 2001 From: hlorenzi Date: Thu, 15 Aug 2024 21:22:14 -0300 Subject: [PATCH] add formant path --- src/AnalysisChart.tsx | 6 +++--- src/RecordingPanel.tsx | 2 +- src/VowelChart.tsx | 40 ++++++++++++++++++++++++++++++++++++--- src/main.tsx | 8 ++++---- src/vowelSynth.ts | 43 +++++++++++++++++++++++++++++++++++++++++- 5 files changed, 87 insertions(+), 12 deletions(-) diff --git a/src/AnalysisChart.tsx b/src/AnalysisChart.tsx index 7421282..faeff5e 100644 --- a/src/AnalysisChart.tsx +++ b/src/AnalysisChart.tsx @@ -107,10 +107,10 @@ function draw( } canvasCtx.stroke() - const timeData = synth.getWaveformLatest(Math.floor(synth.ctx.sampleRate * 0.025)) + const timeData = synth.getWaveformLatest(Math.floor(synth.ctx.sampleRate * 0.05)) - canvasCtx.lineWidth = 2 - canvasCtx.strokeStyle = "#000" + canvasCtx.lineWidth = 1 + canvasCtx.strokeStyle = "#0008" canvasCtx.beginPath() canvasCtx.moveTo(0, h / 2) for (let i = 0; i < timeData.length; i++) diff --git a/src/RecordingPanel.tsx b/src/RecordingPanel.tsx index 5aa9aae..348dc86 100644 --- a/src/RecordingPanel.tsx +++ b/src/RecordingPanel.tsx @@ -268,7 +268,7 @@ function draw(state: State) // Draw playing head if (state.playing) { - state.ctx.strokeStyle = "#00f" + state.ctx.strokeStyle = Common.colorSynth state.ctx.lineWidth = 2 state.ctx.beginPath() state.ctx.moveTo(sampleIndexToX(state.playingIndex), 0) diff --git a/src/VowelChart.tsx b/src/VowelChart.tsx index 64a8ff1..b96e02c 100644 --- a/src/VowelChart.tsx +++ b/src/VowelChart.tsx @@ -20,6 +20,7 @@ export function VowelChart(props: { mousePosNormalized: { x: 0, y: 0 }, mousePosFormants: { f1: 0, f2: 0 }, mousePath: [], + formantPath: [], } @@ -76,6 +77,7 @@ interface State f2: number } mousePath: PathPoint[] + formantPath: PathPoint[] } @@ -272,15 +274,40 @@ function draw( state.ctx.fillText(vowel.symbol, x, y) } + // Draw extracted formants path + state.ctx.strokeStyle = Common.colorFormants + state.ctx.lineWidth = 2 + for (let p = 1; p < state.formantPath.length; p++) + { + state.ctx.beginPath() + const timer = Math.max(0, state.formantPath[p].timer) + state.ctx.globalAlpha = timer + + const pA = state.formantPath[p - 1] + const pB = state.formantPath[p] + const vecX = (pB.x - pA.x) * w + const vecY = (pB.y - pA.y) * h + const vecMagn = Math.sqrt(vecX * vecX + vecY * vecY) + + if (vecMagn < w / 4) + { + state.ctx.moveTo(pA.x * w, pA.y * h) + state.ctx.lineTo(pB.x * w, pB.y * h) + state.ctx.arc(pA.x * w, pA.y * h, 2, 0, Math.PI * 2) + } + + state.ctx.stroke() + state.ctx.globalAlpha = 1 + } + // Draw mouse path - state.ctx.strokeStyle = "#048" + state.ctx.strokeStyle = Common.colorSynth state.ctx.lineWidth = 2 for (let p = 1; p < state.mousePath.length; p++) { state.ctx.beginPath() const timer = Math.max(0, state.mousePath[p].timer) - state.ctx.strokeStyle = Common.colorSynth - state.ctx.globalAlpha = 0.25 + 0.75 * timer + state.ctx.globalAlpha = timer const pA = state.mousePath[p - 1] const pB = state.mousePath[p] @@ -306,6 +333,7 @@ function draw( if (state.mouseDown) { state.ctx.strokeStyle = Common.colorSynth + state.ctx.fillStyle = Common.colorSynth state.ctx.lineWidth = 2 state.ctx.beginPath() state.ctx.arc( @@ -328,6 +356,8 @@ function draw( const formants = synth.getCachedFormants() + state.formantPath.forEach((p) => p.timer -= 1 / 60) + if (formants.length >= 2) { const f1 = formants[0] @@ -336,6 +366,10 @@ function draw( const x = Math.floor(w - mapValueToView(f2, Common.f2Min, Common.f2Max) * w) const y = Math.floor(mapValueToView(f1, Common.f1Min, Common.f1Max) * h) + state.formantPath.push({ x: x / w, y: y / h, timer: 1 }) + while (state.formantPath.length > 100) + state.formantPath.splice(0, 1) + state.ctx.strokeStyle = Common.colorFormants state.ctx.fillStyle = Common.colorFormants state.ctx.lineWidth = 2 diff --git a/src/main.tsx b/src/main.tsx index b6d4b92..1465877 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -24,8 +24,8 @@ function Page() }}> Click and drag around the top chart to synthesize vowel sounds via formant frequencies.

- Red: computed frequency-domain data
- Blue: computed formant frequencies
+ Red: extracted frequency-domain data from waveform
+ Blue: extracted formant frequencies from waveform


@@ -33,8 +33,8 @@ function Page()
{ " " } - diff --git a/src/vowelSynth.ts b/src/vowelSynth.ts index 8ec61d8..517a825 100644 --- a/src/vowelSynth.ts +++ b/src/vowelSynth.ts @@ -9,6 +9,10 @@ export class VowelSynth nodeFormant1Gain: GainNode nodeFormant2Filter: BiquadFilterNode nodeFormant2Gain: GainNode + nodeFormant3Filter: BiquadFilterNode + nodeFormant3Gain: GainNode + nodeFormant4Filter: BiquadFilterNode + nodeFormant4Gain: GainNode nodeAnalyser: AnalyserNode nodeAnalyserData: Uint8Array nodeAnalyserTimeDomainData: Float32Array @@ -84,12 +88,38 @@ export class VowelSynth this.nodeFormant2Filter.Q.value = 5 this.nodeFormant2Filter.connect(this.nodeFormant2Gain) + + this.nodeFormant3Gain = this.ctx.createGain() + this.nodeFormant3Gain.gain.value = 0 + this.nodeFormant3Gain.connect(this.nodeAnalyser) + this.nodeFormant3Gain.connect(this.ctx.destination) + + this.nodeFormant3Filter = this.ctx.createBiquadFilter() + this.nodeFormant3Filter.type = "bandpass" + this.nodeFormant3Filter.frequency.value = 0 + this.nodeFormant3Filter.Q.value = 5 + this.nodeFormant3Filter.connect(this.nodeFormant3Gain) + + + this.nodeFormant4Gain = this.ctx.createGain() + this.nodeFormant4Gain.gain.value = 0 + this.nodeFormant4Gain.connect(this.nodeAnalyser) + this.nodeFormant4Gain.connect(this.ctx.destination) + + this.nodeFormant4Filter = this.ctx.createBiquadFilter() + this.nodeFormant4Filter.type = "bandpass" + this.nodeFormant4Filter.frequency.value = 0 + this.nodeFormant4Filter.Q.value = 5 + this.nodeFormant4Filter.connect(this.nodeFormant4Gain) + this.nodeSource = this.ctx.createOscillator() this.nodeSource.type = "sawtooth" this.nodeSource.frequency.value = 90 this.nodeSource.connect(this.nodeFormant1Filter) this.nodeSource.connect(this.nodeFormant2Filter) + //this.nodeSource.connect(this.nodeFormant3Filter) + //this.nodeSource.connect(this.nodeFormant4Filter) this.nodeSource.start() @@ -111,13 +141,20 @@ export class VowelSynth const generalGain = 0.5 this.nodeFormant1Gain.gain.value = gain * generalGain this.nodeFormant2Gain.gain.value = gain * generalGain * 0.8 + this.nodeFormant3Gain.gain.value = gain * generalGain * 0.5 + this.nodeFormant4Gain.gain.value = gain * generalGain * 0.2 } setFrequencies(formant1Freq: number, formant2Freq: number) { + formant1Freq = Math.max(0, Math.min(5000, formant1Freq)) + formant2Freq = Math.max(0, Math.min(5000, formant2Freq)) + this.nodeFormant1Filter.frequency.value = formant1Freq this.nodeFormant2Filter.frequency.value = formant2Freq + this.nodeFormant3Filter.frequency.value = 2700 + this.nodeFormant4Filter.frequency.value = 3500 } @@ -205,10 +242,14 @@ export class VowelSynth } - async openMic() + async toggleMic() { if (this.nodeMicSrc) + { + this.nodeMicSrc.disconnect() + this.nodeMicSrc = undefined return + } try {