Project 4 min read
Interactive Pinyin Chart
An interactive Mandarin pronunciation tool with 814 syllable cells, runtime audio normalization.
A fully static, zero-backend Mandarin pronunciation tool with 814 interactive syllable cells, runtime audio normalization across 2,019 files, tone-pair drilling, and a custom Web Audio API engine — built for Arabic-speaking learners.

Problem #
Mandarin pronunciation is one of the biggest barriers for beginner learners, especially underserved for Arabic-speaking students. Two specific gaps needed solving:
- No single reference existed showing every valid initial-final-tone combination in Mandarin (814 total) with on-demand audio, filterable by tone.
- Tone pairs — the two-syllable pitch combinations essential for natural Mandarin — are difficult to internalize from static charts.
Solution #
A three-part interactive tool, rendered as a single Astro page with a React application layer.
Architecture #
Astro — Static output, fast initial load, minimal JS for non-interactive content
React 18 — Component-driven state for three independent views
Custom Web Audio API wrapper — Howler.js did not support required normalization pipeline
Data Model #
The core challenge: representing Mandarin phonology as clean, enumerable data. The grid iterates initials against finals at module load, producing 814 valid cells — every real initial-final combination.
814 cells × 4 tones ≈ 3,256 tonal slots. Audio exists only where a tone is actually attested — the data model encodes linguistic reality, not a full combinatorial grid.
The grid holds 814 valid initial-final combinations, but audio is attached only where a tone is attested in Mandarin — the data model reflects phonological reality, not a full 3,256-cell combinatorial matrix.
Audio at Scale #
The tool ships 2,019 individual MP3 files, sourced from two open-source native-speaker recording projects (CC-BY-SA licensed). The custom audio engine handles normalization at runtime rather than via offline batch preprocessing.
AUDIO PIPELINE
fetch(url) → decodeAudioData → per-clip gain (RMS normalization) → shared compressor → destination
Per-clip RMS normalization. On first decode, the engine computes RMS loudness and derives a gain value: gain = TARGET_RMS (0.15) / clip_rms, clamped to ±12 dB. Near-silent clips left at gain 1.0. Computed gain cached alongside decoded buffer — never recalculated on replay.
Runtime RMS normalization across 2,019 CC-BY-SA clips avoids an offline FFmpeg pass — per-clip gain is computed once on decode, cached with the buffer, and fed through a shared compressor for consistent playback.
Compressor Parameters #
threshold
-24 dB
knee
30 dB
ratio
4:1
attack
3 ms
release
250 ms
Performance Engineering #
Challenge
Audio latency on first interaction
Solution
6-lane concurrent preloading via requestIdleCallback
Challenge
Inconsistent loudness across 2,019 files
Solution
Runtime RMS normalization + shared dynamics compressor
Challenge
Repeated RMS computation on replay
Solution
LRU cache (200 entries) storing decoded buffer + precomputed gain
Challenge
iOS/Safari autoplay restrictions
Solution
unlockAudio() silent-buffer unlock on first user gesture
Key Decisions #
- Runtime normalization over batch — avoids offline FFmpeg pass; flexible for future audio sources
- Custom audio engine over Howler.js — 220-line purpose-built engine leaner than wrapping libraries
- Local state over global state mgmt — three views with minimal shared state do not need Redux/Zustand
- Static JSON over CMS — Mandarin phonology is fixed reference material
Data & Content Sourcing #
Audio recordings
cmguo/PinYinSound
CC-BY-SA
Audio recordings
hugolpz/audio-cmn
CC-BY-SA
Dictionary data
CC-CEDICT
CC-BY-SA 4.0
Outcome #
Live in production as a core feature of Falafel in Hotpot’s learning platform — a single, mobile-optimized reference for the full Mandarin syllable inventory, tone-pair pronunciation patterns, and structured rule explanations, all backed by native-speaker audio with consistent, engineered playback quality.