Deep Dive: Bell 212A and V.32bis Handshakes
When 300 baud isn't enough—simulating faster modem handshakes with phase modulation, QAM, and the beautiful screaming of USRobotics Sportsters.
Deep Dive: Bell 212A and V.32bis Handshakes
When 300 baud isn’t enough and you need to simulate the beautiful screaming of faster modems
After getting Bell 103 working at 300 baud (see the Bell 103 deep-dive), I had a problem. The FSK modulation sounded great, but nobody in 1989 was connecting at 300 baud. We had 2400. We had 14.4k. We had USRobotics Sportsters making that glorious screech.
So I had to figure out how Bell 212A and V.32bis actually worked — not just “play a recording,” but generate mathematically accurate handshake audio in real-time. Turns out, once you leave FSK territory, things get… complicated.
The Jump to Phase Modulation
Bell 103 uses FSK: different frequencies for different bits. Simple. Bell 212A at 1200 baud uses something called DPSK — Differential Phase Shift Keying. Instead of changing frequencies, you change the phase of a carrier signal.
// modem-audio.js - Bell 212A carrier generation
function generateBell212ACarrier(role, duration) {
// Bell 212A uses different carriers for each direction
const carrierFreq = role === 'answer' ? 2400 : 1200; // Hz
const samples = duration * this.sampleRate;
const buffer = new Float32Array(samples);
// QPSK: 4 phase states, 2 bits per symbol
// Symbol rate: 600 baud (1200 bps)
const symbolDuration = this.sampleRate / 600;
let phase = 0;
let symbolIndex = 0;
for (let i = 0; i < samples; i++) {
// Phase shifts: 0°, 90°, 180°, 270° (representing 00, 01, 11, 10)
const symbolPhase = (symbolIndex % 4) * (Math.PI / 2);
// Add the carrier with current phase
buffer[i] = Math.sin(2 * Math.PI * carrierFreq * (i / this.sampleRate) + phase + symbolPhase);
// Advance symbol at 600 baud
if (i % symbolDuration < 1) {
// In training sequence, phase rotates predictably
symbolIndex++;
}
}
return buffer;
}
The key insight: Bell 212A achieves 1200 bps at 600 baud by encoding 2 bits per symbol. Four phase states (QPSK) × 600 symbols/sec = 1200 bits/sec. Same bandwidth, twice the throughput.
The Training Dance
Here’s where it gets interesting. Before two Bell 212A modems can exchange data, they have to train. The answering modem sends a specific sequence that lets the originating modem lock onto its carrier and learn its phase reference:
// Bell 212A handshake sequence
const bell212AHandshake = {
// Answer modem: 2400 Hz carrier
answerCarrier: { freq: 2400, duration: 0.5 },
// Training sequence: scrambled DPSK
training: {
// ITU-T defined scrambler polynomial: 1 + x^-14 + x^-17
// Creates pseudo-random phase transitions
scramblerPolynomial: [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1],
duration: 0.256, // 154 symbols at 600 baud
},
// Originate modem responds at 1200 Hz
originateCarrier: { freq: 1200, duration: 0.5 },
// Both sides enter data mode
totalDuration: 2500 // ~2.5 seconds
};
The scrambler polynomial isn’t random — it’s defined in ITU-T V.22bis. This ensures both modems know what sequence to expect, allowing them to calibrate their phase detection.
V.32bis: Welcome to the Jungle
V.32bis at 14,400 bps is where things get really fun. It uses:
- Trellis-coded modulation — error correction built into the modulation scheme
- Echo cancellation — both modems transmit on the same frequency band simultaneously
- Adaptive equalization — compensates for phone line distortion in real-time
// modem-audio.js - V.32bis handshake (simplified)
function generateV32bisHandshake(role) {
const phases = [];
// Phase 1: V.25 answer tone (2100 Hz with phase reversals)
if (role === 'answer') {
phases.push(this.generateAnswerTone2100Hz());
}
// Phase 2: Echo canceller training
// Both modems send frequency sweeps to measure echo characteristics
phases.push(this.generateEchoCancellerTraining());
// Phase 3: Equalizer training
// Dual-tone probing to measure channel response
phases.push(this.generateEqualizerTraining());
// Phase 4: Scrambled data sequence
// TCM (Trellis Coded Modulation) training
phases.push(this.generateTCMTraining());
// Phase 5: Rate negotiation
// Modems agree on actual speed (14.4k, 12k, 9.6k, 7.2k, 4.8k)
phases.push(this.generateRateNegotiation());
return this.concatenatePhases(phases);
}
The V.25 answer tone deserves special attention. It’s 2100 Hz, but with phase reversals every 450ms. This specific pattern tells the phone network “hey, I’m a modem, please disable your echo suppressors.” Phone networks in the 80s and 90s had echo suppressors that would absolutely destroy modem data.
// V.25 answer tone with phase reversals
function generateAnswerTone2100Hz() {
const duration = 3.0; // seconds
const phaseReversalInterval = 0.450; // every 450ms
const samples = duration * this.sampleRate;
const buffer = new Float32Array(samples);
let phase = 0;
let lastReversal = 0;
for (let i = 0; i < samples; i++) {
const t = i / this.sampleRate;
// Phase reversal every 450ms
if (t - lastReversal >= phaseReversalInterval) {
phase += Math.PI; // 180° phase shift
lastReversal = t;
}
buffer[i] = 0.5 * Math.sin(2 * Math.PI * 2100 * t + phase);
}
return buffer;
}
The Echo Canceller Problem
V.32bis modems have to solve a wild problem: they transmit and receive on the same frequencies at the same time. The modem’s own transmitted signal bounces back from the phone line and shows up in the receive path. This “echo” can be 10-20 dB louder than the actual far-end signal.
The training sequence exists to let each modem characterize this echo precisely:
// Echo canceller training: frequency sweep
function generateEchoCancellerTraining() {
const duration = 0.5;
const startFreq = 1200;
const endFreq = 2400;
const samples = duration * this.sampleRate;
const buffer = new Float32Array(samples);
for (let i = 0; i < samples; i++) {
const t = i / this.sampleRate;
const progress = t / duration;
// Linear frequency sweep
const freq = startFreq + (endFreq - startFreq) * progress;
buffer[i] = 0.5 * Math.sin(2 * Math.PI * freq * t);
}
return buffer;
}
By sending a known signal and measuring what comes back, the modem builds a model of the echo path. During data transmission, it generates a predicted echo and subtracts it from the receive signal. Elegant in theory, nightmarish in implementation.
(I’m not actually implementing a real echo canceller. The emulator just needs to sound right.)
The Characteristic V.32bis Screech
That distinctive V.32bis handshake sound — you know the one — comes from the rapid transitions between training phases:
- 2100 Hz answer tone (smooth, pure tone)
- Echo canceller sweep (rising “whoooop”)
- Equalizer training (dual-tone warble)
- TCM training (static-y scrambled data)
- Rate negotiation (brief silence, then data)
// Timing for that characteristic sound
const v32bisProfile = {
name: 'V.32bis',
maxBaud: 14400,
handshakeDuration: 5000, // ~5 seconds
phases: [
{ name: 'answer_tone', start: 0, duration: 2000, generator: 'answerTone2100' },
{ name: 'echo_cancel', start: 2000, duration: 500, generator: 'echoSweep' },
{ name: 'equalizer', start: 2500, duration: 800, generator: 'dualToneQAM' },
{ name: 'tcm_train', start: 3300, duration: 1200, generator: 'scramblerSequence' },
{ name: 'negotiate', start: 4500, duration: 500, generator: 'ratePattern' }
]
};
USRobotics vs Generic Modems
USRobotics Sportsters had a slightly different handshake sound than generic “Rockwell chipset” modems. The difference is in the timing and the specific training sequences they used:
// uart-modem-profiles.js
export const MODEM_PROFILES = {
usrobotics_sportster: {
name: 'USRobotics Sportster 14.4',
handshake: 'v32bis',
quirks: {
// USR used slightly longer training phases
echoTrainingDuration: 600,
// And had a distinctive "chirp" at the end
finishChirp: true
}
},
generic_rockwell: {
name: 'Generic 14.4k (Rockwell)',
handshake: 'v32bis',
quirks: {
// Rockwell chipsets were faster but less distinctive
echoTrainingDuration: 400,
finishChirp: false
}
}
};
(The USRobotics finish chirp is entirely my invention, but it sounds right to my ear. Sometimes emulation is about the feeling as much as the spec.)
Putting It All Together
The final handshake generator switches between profiles based on the modem you’ve selected:
// modem-sequencer.js
async function runHandshake(profile, role) {
const audio = new ModemAudio(this.audioContext);
switch (profile.handshake) {
case 'bell103':
return await audio.generateBell103(role);
case 'bell212a':
return await audio.generateBell212A(role);
case 'v32bis':
const handshake = await audio.generateV32bisHandshake(role);
if (profile.quirks?.finishChirp) {
handshake.push(await audio.generateUSRChirp());
}
return handshake;
}
}
When you dial into the emulator with a V.32bis modem selected, you get approximately 5 seconds of beautiful, mathematically accurate modem screaming. It’s not a recording — it’s generated live every time.
What I Learned
-
Phase modulation is clever — Going from FSK to DPSK/QPSK was the first big leap in modem speeds. Same bandwidth, more bits per symbol.
-
Echo cancellation is why we can’t have nice things — Full-duplex communication over a two-wire phone line is a solved problem, but the solution is horrifically complex.
-
Training sequences aren’t arbitrary — Every chirp, sweep, and warble in a modem handshake serves a specific purpose. Modems were teaching each other.
-
Nostalgia is in the details — Nobody would notice if I used a generic handshake sound. But getting the USRobotics timing right? That’s the difference between “oh, a modem” and “oh man, I had that exact modem.”
The Sound of the Internet
These handshakes — Bell 103, Bell 212A, V.32bis — they’re the sound of the early internet. Every BBS download, every email check, every flame war on Usenet started with these tones. Getting them right felt important.
And yes, I spent way too long on this. But now when you dial into the emulator, you hear your specific modem negotiating with the far end, just like it would have in 1995. The screaming is authentic.
See also: Deep Dive: Bell 103 Audio Modem — where the FSK journey began.
See also: Deep Dive: CTS Flow Control — because what good is a fast modem if you lose data?