Created
February 16, 2026 19:33
-
-
Save arturitu/b9fac1c294405700b429aa50b7190c06 to your computer and use it in GitHub Desktop.
PhonemeAnalyzer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { PhonemeKey } from '../types'; | |
| /** | |
| * PhonemeAnalyzer redesigned for the Hanna-Barbera animation system (Preston Blair). | |
| * Classifies audio input into 6 mouth shapes (A-F) based on formants and amplitude. | |
| */ | |
| export class PhonemeAnalyzer { | |
| private analyser: AnalyserNode; | |
| private dataArray: Uint8Array; | |
| private binFrequency: number; | |
| private lastPhoneme: PhonemeKey = 'A'; | |
| private phonemeHoldFrames: number = 0; | |
| private readonly MIN_HOLD_FRAMES = 1; | |
| constructor(audioContext: AudioContext, source: AudioNode) { | |
| this.analyser = audioContext.createAnalyser(); | |
| this.analyser.fftSize = 512; | |
| this.analyser.smoothingTimeConstant = 0.35; | |
| source.connect(this.analyser); | |
| this.dataArray = new Uint8Array(this.analyser.frequencyBinCount); | |
| this.binFrequency = audioContext.sampleRate / this.analyser.fftSize; | |
| } | |
| private hzToBin(hz: number): number { | |
| return Math.round(hz / this.binFrequency); | |
| } | |
| private getAverageFrequencyRangeHz(startHz: number, endHz: number): number { | |
| const startBin = this.hzToBin(startHz); | |
| const endBin = this.hzToBin(endHz); | |
| let sum = 0; | |
| const start = Math.max(0, startBin); | |
| const end = Math.min(this.dataArray.length, endBin); | |
| const count = end - start; | |
| if (count <= 0) return 0; | |
| for (let i = start; i < end; i++) sum += this.dataArray[i]; | |
| return sum / count / 255; | |
| } | |
| public getPhoneme(): PhonemeKey { | |
| const detected = this.detectHannaBarberaShape(); | |
| if (detected === this.lastPhoneme) { | |
| this.phonemeHoldFrames = 0; | |
| return detected; | |
| } | |
| this.phonemeHoldFrames++; | |
| if (detected === 'D' || detected === 'A' || this.phonemeHoldFrames >= this.MIN_HOLD_FRAMES) { | |
| this.lastPhoneme = detected; | |
| this.phonemeHoldFrames = 0; | |
| } | |
| return this.lastPhoneme; | |
| } | |
| private detectHannaBarberaShape(): PhonemeKey { | |
| this.analyser.getByteFrequencyData(this.dataArray); | |
| const energyLow = this.getAverageFrequencyRangeHz(80, 400); | |
| const energyMid = this.getAverageFrequencyRangeHz(400, 1500); | |
| const energyHigh = this.getAverageFrequencyRangeHz(2000, 6000); | |
| const totalVolume = (energyLow + energyMid + energyHigh) / 3; | |
| // If there is silence, we return 'A' (Rests/Closed) | |
| if (totalVolume < 0.05) return "A"; | |
| if (totalVolume < 0.12 && energyLow > energyHigh) return "A"; | |
| if (totalVolume > 0.4 && energyMid > energyHigh * 1.5) return "D"; | |
| if (energyHigh > energyMid * 0.8 || (energyHigh > 0.2 && energyLow < 0.3)) return "B"; | |
| if (energyLow > energyMid * 2.5 && energyHigh < 0.1) return "F"; | |
| if (energyLow > energyHigh * 2 && energyMid > energyHigh) return "E"; | |
| return "C"; | |
| } | |
| public dispose() { | |
| try { this.analyser.disconnect(); } catch (e) {} | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment