Salah Thabet
0% read
Back

Project 4 min read

Interactive Pinyin Chart

An interactive Mandarin pronunciation tool with 814 syllable cells, runtime audio normalization.

AstroReact 18ViteRadix UIWeb Audio APIAudio Engineering

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.

Pinyin Chart — desktop and mobile UI showing the interactive sound table
Feature demo — Chart mechanics
0:00 / 0:00
Click cells, filter tones, and hear pronunciation across the full initial–final grid.

Product

Falafel in Hotpot — Mandarin Pronunciation Learning Tool

Role

Sole developer, architect, content designer

Stack

Astro, React 18, Vite, Radix UI, Web Audio API

Scale

814 cells, ~3,256 tonal slots, 2,019 audio files, 240 HSK words

Type

Fully static, zero-backend, client-side audio processing


Problem #

Mandarin pronunciation is one of the biggest barriers for beginner learners, especially underserved for Arabic-speaking students. Two specific gaps needed solving:

  1. No single reference existed showing every valid initial-final-tone combination in Mandarin (814 total) with on-demand audio, filterable by tone.
  2. 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.

01
Chart
Every Chinese initial laid out against all finals in a grid. Click any cell to hear pronunciation across all 4 tones. Tone filter isolates one tone across the entire grid.
02
Sound Map
Covers all 20 tone-pair combinations. Each cell surfaces 8 real HSK words recorded by native speakers.
03
Learn Section
Seven structured guides covering rules, exceptions, and quick-reference summaries — each bundled with inline audio.
Feature demo — Tone pairs
0:00 / 0:00
Sound Map: drill all 20 tone-pair combinations with native-speaker HSK examples.
Feature demo — Learn tab
0:00 / 0:00
Structured guides with rules, exceptions, and inline audio for quick reference.

Architecture #

Page shell

Astro — Static output, fast initial load, minimal JS for non-interactive content

Interactive layer

React 18 — Component-driven state for three independent views

Audio

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.