VTX Voice Matching

Making an AY chip imitate a voice

This is an experiment that follows on from VTX Speech. That project builds speech from text: words become phonemes, and phonemes become AY register settings using the chip's envelope as the buzz of a voice. Here the starting point is a recording instead. For every 50th of a second, the tool searches for the AY register settings that sound closest to the recording, then plays the result through an AY emulator so you can compare.

Everything runs in your browser. Load a WAV file, record your own voice or try the examples; nothing is uploaded. For noisy recordings, clean them first with NVIDIA RE-USE (section 4). There is no cartridge or Spectrum player yet: the output is register data, checked only in emulation.

Open the tool Source on GitHub

Listen

The openings of five public-domain LibriVox readings (clean recordings, voices from about 86 to 200 Hz) and two unaccompanied songs, one in a high voice, each beside the AY version made from it: 50 updates a second on a Vectrex's AY clock, free search with the current defaults (exact squares and noise × tone, section 3b), no gate; denoise only on the nursery rhyme (2). The tool's example menu loads them.

ReadingOriginalAY, 50 HzAY STOI4-band vocoder8-band vocoder
“Kearney at Seven Pines”
Edmund Clarence Stedman; read for LibriVox; voice about 200 Hz
0.7640.6930.757
The Gettysburg Address
Abraham Lincoln; read by John Greenman for LibriVox; about 120 Hz
0.7050.7520.808
John Adams to John Quincy Adams, 27 July 1777
read for LibriVox; about 170 Hz
0.6860.6720.776
“The Seven Ages of Man”
William Shakespeare; read for LibriVox; about 145 Hz
0.6930.6850.767
The Swiss Family Robinson, opening
Johann David Wyss; read by Mark F. Smith for LibriVox; a deep voice, about 86 Hz
0.6780.7180.806
“Twinkle, Twinkle, Little Star”, sung
unaccompanied, by Wikimedia Commons user Dcoetzee (CC0); about 180 Hz, pitch followed every update
0.6650.6340.692
“A chantar m’er”, sung
12th-century song by the Comtessa de Dia, unaccompanied; recorded by Wikimedia Commons user Makemi (public domain); a high voice, about 350 Hz
0.4750.4270.490

The vocoder columns are the same readings through 4- and 8-band noise vocoders, for scale (section 2): the AY versions sit around the 4-band level, the highest speaking voice beats 8 bands, and both songs sit between 4 and 8 bands. The deep voice does worst against the references although its pitch is the most accurate (7 cents): at 86 Hz there are about 70 harmonics under 6 kHz, too dense a spectrum for three channels to follow, and the bare envelope buzz does much of the work. Pitch is followed every update: in the nursery rhyme the envelope's steps land 14 cents from the sung notes on average (40 at worst); in the high voice, where each envelope step is about 6%, 29 cents (62 at worst). STOI rates the long sustained notes of the second song lower overall, vocoders included. Noisy sources, such as the Apollo radio recordings, score far lower: clean them first, ideally with NVIDIA RE-USE (section 4). Sources and licences are under Credits.

1. How it works

The AY-3-8910 has three square-wave channels, one noise source and one envelope generator. VTX Speech showed that the envelope, run as a fast sawtooth, makes a good voice buzz: it is rich in harmonics, like the vocal folds, and its pitch is set by the envelope period. Tones locked to exact harmonics of that buzz then shape it into vowels. Voice matching uses the same parts, but decides them from the recording, one update at a time:

  1. Pitch. The recording's pitch is found for each update (the YIN method), and the envelope period is set to match: EP = clock ÷ (256 × pitch). Pitch follows the recording every update.
  2. Harmonic slots. The recording's energy is measured around each harmonic of that pitch, up to 6 kHz.
  3. Candidates. Each channel can be off; the envelope buzz with its tone off, or gated by a tone at harmonic n (tone period = 16 × EP ÷ n, so it stays locked to the buzz); a square at harmonic n at one of 15 fixed volumes; or noise at a fixed volume, with one noise period shared between channels.
  4. Search. The three channels are chosen together, to bring the AY's harmonic energies as close as possible to the recording's in decibels. The search fits each frame's peaks, as LPC speech coders do, and ignores slots more than 30 dB below the loudest. It also tries several overall levels, because the envelope only ever plays at full volume.
  5. Render and measure. The chosen registers are played through Ayumi, an accurate AY emulator, and the result is measured against the recording the same way.

A second mode, Speech, adds speech-coder rules: LPC formants place channel A near the first formant and B near the second, quiet high-frequency frames become noise only, and voiced frames get no noise. It places formants more accurately, but it measures as less intelligible than the free search (section 3).

The tool also shows each update's harmonic energies (recording, model and rendered side by side) and its 14 register values, and exports them for a player.

2. Measuring intelligibility

A small spectral error is not the same as speech you can understand, so every result is scored with STOI (short-time objective intelligibility, Taal et al., 2011). It is a standard measure of whether speech survives processing: it compares how the loudness of 15 frequency bands rises and falls over about 0.4 seconds in the original and the copy. It runs from 0 to 1, where 1 means identical.

To give the numbers a scale, the same clips are also put through noise vocoders. A noise vocoder keeps only the loudness of a few frequency bands and rebuilds the sound from noise. It is a standard way to simulate cochlear implants, and a few bands are known to be enough for much speech to be understood. They are references, not rivals: a vocoder controls many bands independently and updates over three times as often as 50 Hz.

Two cautions. STOI assumes its reference is clean, so measuring against a noisy recording penalises any cleaning (section 4). And it is not a listening test: the tool has an A/B button (the whole recording, then the whole AY version) for the final judgement.

3. Results so far

Four test recordings, none of which are included here: a spoken film line over a music bed, an a cappella singing clip, and two short game voice clips (8-bit, 22 and 11 kHz). All at 50 Hz with no smoothing between updates; gates set per clip.

STOIfilm linesinginggame voice 1game voice 2
noise vocoder, 16 bands (reference)0.7770.7650.6150.722
noise vocoder, 8 bands (reference)0.6880.6830.5210.636
noise vocoder, 4 bands (reference)0.6040.5950.4250.563
AY, free search0.5500.6240.3820.686
AY, built-in denoise first (best of strength 2 or 4)0.6220.6430.4800.679
AY, speech mode0.5030.5870.3400.537

Full tables: results/sweep.json, made by tools/sweep.js. These tables, and those in sections 4 and 5, were measured before the two defaults chosen in section 3b; those changed the figures by about ±0.02.

3b. Choosing by ear

STOI is a good guide, but it is not a listener. The later experiments were decided by blind A/B tests: each pair of AY versions played in random order, the listener picking the better one or “can't tell”, with the answers revealed only at the end.

ExperimentBlind resultSTOI changeNow
Exact squares: square tones tuned to the recording's own pitch instead of the envelope's rounded pitchpreferred on the high voice; a tie on speech and the lower song−0.02 to +0.02default
Noise × tone: noise ANDed with a 1–6 kHz tone, moving the hiss up the spectrum for consonantspreferred 4 of 4 on noisy and cleaned speech; a tie on clean speech−0.02 to +0.05default
Triangle envelope (shape 10) as the buzzthe usual sawtooth preferred 3 to 1+0.002 to +0.010rejected
Pitch dithering: alternating envelope periods so the average pitch lands on the notenever preferred−0.03 to +0.01option
Formant pairs: two channels on neighbouring harmonics placing the first formant between themcan't tellup to +0.018 on the high voiceoption
Phase lock: only harmonics that keep a fixed phase against the envelopecan't tell−0.007 to −0.020rejected

The triangle envelope is the lesson: STOI scored it better on every clip, and it sounded worse. Anything that changes the timbre needs listening, not only a score. Each default costs some data: together they add about 10–60 register writes a second.

4. Clean the audio first

Anything in the recording that is not the voice (music, room noise, hiss) is something the AY will try to imitate with its three channels. Removing it first helps more than any other setting tried.

Built-in denoise. The tool can subtract the background's spectrum before matching. It estimates the background from the quietest tenth of the recording. On the film line it lifts STOI from 0.550 to 0.622, and on the noisier game voice from 0.382 to 0.480. On a clean recording it costs a little, so leave it at 0 there. A gate silences frames below a set level; it helps on top of denoise.

Recommended: clean noisy recordings with NVIDIA RE-USE first. RE-USE is a universal speech-enhancement model: it repairs noise, reverberation, clipping, narrow bandwidth and codec damage while keeping the speaker's voice, and accepts recordings at 8 to 48 kHz. It runs on Hugging Face or on your own machine. Neural enhancement like this separates a voice from its background far better than the tool's spectral subtraction. One noisy game clip, matched before and after cleaning with RE-USE:

AY matched toSTOI vs originalSTOI vs cleaned
the original0.3820.563
the original, built-in denoise0.4800.570
the RE-USE cleaned copy0.3540.725
(the cleaned copy itself)0.5221

Read the columns with care. The cleaned copy itself only scores 0.522 against the noisy original, because STOI counts the removed noise as an error. When a clean copy exists it is the better yardstick, and against it the AY version made from the cleaned copy is the best result so far. Each version naturally scores best against the file it was matched to, so listen to decide.

How: run the recording through RE-USE, load the cleaned file in the tool with denoise at 0 and the gate off (60), and use score against to compare with the original if you like. Other tools for the same job include DeepFilterNet (open-source noise suppression), Demucs (separating vocals from music) and Adobe Podcast's Enhance Speech. Clean recordings, such as the examples above, need no cleaning.

5. Is 50 Hz enough?

One update per 50 Hz frame is what a game can afford: the register writes happen once per frame, between the drawing. Faster updates were measured to see what they would buy (free search, film line with its gate; a clean game clip):

updates a secondfilm line STOIregister writes a secondclean clip STOI
500.5501780.686
1000.5583070.652
2000.5885140.678
4000.6116150.697
8000.6188190.694

The film line gains 0.06 by 400 Hz and then levels off; the clean clip shows no trend. Faster rates mean writing registers in the middle of a frame from a timer, and 3–5 times the data, so 50 Hz stays the target. Cleaning the audio gains more than any update rate.

6. Playing it back on a Vectrex or a Spectrum

Vectrex: a player exists. In the tool, match at 50 Hz with machine Vectrex, then Save Vectrex .asm (the whole match) or drag across the spectrogram and save just that part. vectrex/vtx_voice_player.asm (193 bytes of 6809 code, 4 bytes of RAM) plays it: each update is a 2-byte mask of the registers that changed, then their values, about 250–350 bytes a second of speech. vectrex/voice_cart.asm is a complete example cartridge. The player matches the tool update by update in MAME, and MAME's sound matches the tool's Ayumi render; nobody has heard it on a real Vectrex yet.

For other machines, Save registers .bin writes 14 bytes per update, R0 to R13 in order. R13 (the envelope shape) is $FF when it must not be written, because writing it restarts the envelope; it is written once, when the buzz starts. A player only has to copy one 14-byte record to the AY every frame.

Match for the right machine. Every period register depends on the chip's clock, so choose the machine in the tool before matching. The same file played on a machine with a different clock would come out at the wrong pitch.

MachineAY clockWhat the player does each frame
Vectrex1.5 MHzCopy the record into the BIOS sound buffer (Vec_Music_Work) and call Do_Sound, which writes the registers, once per Wait_Recal. VTX Speech's cartridge player already works this way.
ZX Spectrum 128, +2, +31.7734 MHzIn the 50 Hz interrupt, for each register: select it on port $FFFD, write the value on $BFFD. The 48K Spectrum has no AY.
Amstrad CPC1 MHzWrites go through the PPI, so each register costs more instructions; still small at 50 Hz.
MSX1.79 MHzAs above through the PSG ports. 60 Hz machines would need matching at 60 updates a second, which the tool does not offer yet.
Atari ST2 MHz (YM2149)The YM's envelope has 32 steps; the model assumes the AY's 16, so expect small differences.

Data size. At 50 Hz the raw stream is 700 bytes a second, so a 32 KB cartridge holds about 45 seconds. Most registers do not change every update: storing a 2-byte mask of the changed registers plus their values takes 300–350 bytes a second on the test clips. A real format would compress further. The tool shows both figures for each match.

CPU time is small: writing 14 registers is a few hundred cycles, a percent or two of a 50 Hz frame.

For the phoneme-based speech player, which stores words instead of frames and is much smaller, see VTX Speech.

7. How it differs from other projects

Nick Bild's AY-3-8910 speech synthesis (2021, GPL-3.0) is the closest idea. He recorded every tone and noise setting the chip can make on one channel, without envelopes, then split the target speech ("Greetings, Professor Falken") into 30 ms pieces. Each piece got the recorded sound whose spectrogram was closest (mean squared error), played from a Raspberry Pi; he describes the result as rough but distinguishable. Voice matching is the same "find the closest chip sound" approach, but with a much larger set of sounds to choose from:

audio2ay by Jari Komppa (2025, zlib licence) converts audio to AY data with a short-time FFT, giving up to three tone channels the strongest peaks between 300 and 2,000 Hz, with an optional arpeggio on the last channel. It writes 2 bytes per channel per frame and comes with a ZX Spectrum player. Its author says speech does not really work with it; it is aimed at melodic sounds. Its documentation does not mention noise or the envelope, which section 3 shows matter most for speech.

Neither project has been run here, so their quality is described in their authors' own words, not measured with STOI.

8. Limits and next steps

Credits and licences

VTX Voice Matching © 2026 Michael Simonds. MIT licence (LICENSE); the files in vendor/ keep their own licences. The only recordings included are the public-domain examples above.