Skip to main content
Back to articles
2026 / 01
| 5 min read

Deep Dive: Bell 212A and V.32bis Handshakes

Simulating Bell 212A and V.32bis handshakes: phase modulation, training sweeps, and why the negotiation audio has the shape it does.

emulator modem audio signal-processing v32bis
On this page

The first time I got the V.32bis handshake working, it was too quiet to hear. The timing was right and the frequencies matched the config, but the speaker simulation attenuated everything until the screech was a whisper. I spent an afternoon adjusting volume curves before admitting the real problem: I had no model of why this particular sequence of tones existed, so I was generating sounds rather than simulating a negotiation.

That turned out to matter for debugging, not just for authenticity. Each phase of a handshake exists because the previous phase proved something about the connection. Once I could name what each phase proves, a handshake that sounded wrong told me which phase was misbehaving.

The Jump from FSK to PSK

Bell 103 at 300 baud uses frequency-shift keying: the data is in which frequency is playing. That works, but it spends bandwidth keeping the originate and answer bands apart. Bell 212A moves to phase-shift keying, where the carrier stays at a fixed frequency and the data lives in phase changes between symbols. Same wire, four times the throughput.

The sequence for Bell 212A:

  • Answer modem emits a 2400 Hz carrier to announce itself.
  • Originate modem responds with a 1200 Hz carrier, offset in time.
  • Both sides run a training sequence of regular phase shifts so the receiver can lock its timing reference.
  • Once phase lock is confirmed, data mode begins.

In the emulator’s HandshakeGenerator, that training sequence is explicit:

// Bell 212A training: 10 cycles, 80ms each, ±12 Hz phase modulation
for (let i = 0; i < cfg.trainingCycles; i++) {
  const oFreq = cfg.originateHz + (i % 2 === 0 ? cfg.phaseModulationHz : -cfg.phaseModulationHz);
  const aFreq = cfg.answerHz + (i % 2 === 0 ? -cfg.phaseModulationHz : cfg.phaseModulationHz);
  // ... create overlapping oscillators for both carriers
  time += cfg.trainingCycleMs / 1000;
}

The numbers (1200 Hz originate, 2400 Hz answer, 10 training cycles at 80 ms each, ±12 Hz modulation) come from the config. They produce a handshake that steps through its phases the way a Bell 212A lock-in does instead of warbling at random. The ±12 Hz modulation is too small to hear as a pitch change, but it is enough that the training phases are visible in the emulator’s timing.

V.32bis: Training as a Diagnostic Stack

V.32bis carried 14,400 bits per second over the same phone lines that Bell 103 used for 300, at a symbol rate of 2400 baud with a 128-point constellation. Getting there required solving problems Bell 212A could ignore: echo cancellation, line equalization, and rate negotiation. Each problem gets its own training phase, and each phase produces a distinct sound.

The answer tone comes first: a 2100 Hz carrier with periodic phase reversals. ITU-T V.25 specifies the tone so the network knows an automatic answering device has picked up; the 180° reversals every 450 ms exist to disable network echo cancellers, which would otherwise chew up a full-duplex data signal. The emulator runs three cycles at 450 ms each:

// V.25 answer tone with phase reversals
for (let i = 0; i < cfg.answerToneCycles; i++) {
  const freq = cfg.answerToneHz + (i % 2 === 0 ? 0 : cfg.answerTonePhaseShiftHz);
  const tone = this.createOscillator(freq, cfg.baseVolumeDb);
  tone.start(time).stop(time + cfg.answerToneCycleMs / 1000);
  time += cfg.answerToneCycleMs / 1000;
}

Next comes the echo canceller training. Real V.32bis modems are full duplex on one channel, so each end has to measure how much of its own signal comes back off the line and subtract it. The emulator has no physical line to echo, so it does not implement cancellation at all; it only generates the training sweep so the handshake has the right shape. Twelve steps from 1200 Hz upward, 100 Hz per step, 100 ms per step:

// Echo canceller training sweep
for (let i = 0; i < cfg.echoTrainingSweepSteps; i++) {
  const freq = cfg.echoTrainingSweepStartHz + i * cfg.echoTrainingSweepStepHz;
  const sweep = this.createOscillator(freq, cfg.baseVolumeDb - 1);
  sweep.start(time).stop(time + cfg.echoTrainingSweepStepMs / 1000);
  time += cfg.echoTrainingSweepStepMs / 1000;
}

The equalizer training follows: ten dual-tone bursts at 150 ms each, starting at 1600 Hz and 2000 Hz and stepping in opposite directions. On real hardware this is where the modem measures how the line distorts different frequencies. Last is the QAM training block, six cycles of three overlapping tones, standing in for the constellation the data phase would use.

The full V.32bis profile in the emulator’s config:

V32bis: {
  baudRate: 14400,
  answerToneHz: 2100,
  answerToneCycles: 3,
  answerToneCycleMs: 450,
  answerTonePhaseShiftHz: 3,
  echoTrainingSweepStartHz: 1200,
  echoTrainingSweepSteps: 12,
  echoTrainingSweepStepHz: 100,
  echoTrainingSweepStepMs: 100,
  equalizerBursts: 10,
  equalizerBurstMs: 150,
  equalizerFreq1StartHz: 1600,
  equalizerFreq1StepHz: 40,
  equalizerFreq2StartHz: 2000,
  equalizerFreq2StepHz: -30,
  qamTrainingCycles: 6,
  qamTrainingCycleMs: 130,
  qamFreq1Hz: 1750,
  qamFreq2Hz: 1850,
  qamFreq3Hz: 1950,
  qamFreqStepHz: 50,
  baseVolumeDb: -18,
}

Those numbers produce a handshake of about 4.5 seconds, long enough to feel like a negotiation and short enough not to stall the emulator. The -18 dB base volume was the fix for the “too quiet” problem I started with: the speaker simulation path has its own gain structure, so the generators have to output at a consistent level for anything downstream to mix properly.

The Tempo Constraint

The hardest part of getting the handshakes right was not the frequencies. It was the tempo.

A short answer tone sounds like a glitch rather than a greeting. Long training phases make the emulator feel stalled. The numbers in the config were tuned by ear against recordings of real modems, then adjusted until the sequence felt right in the emulator.

The target is not conformance to the ITU-T timing tables; nothing here has to interoperate with real hardware. The target is believable causality, where each phase sounds like it finished something before the next one starts. Adding a 200 ms gap between echo training and equalizer training did nothing computationally and made the whole sequence more convincing, because the ear expects a pause between topics.

What the Timing Tables Imply

Faster modems are not simply more data. They are more training phases, tighter timing contracts, and stronger assumptions about the line. Bell 212A assumes the line is stable enough to hold phase lock. V.32bis assumes it can measure echo and frequency response in real time. Each jump in speed shows up as a longer, more structured handshake.

The simulation only has to match the system’s own model. There is no echo to cancel here, so the echo training sweep is cosmetic. It stays because the audio is part of what the emulator promises the visitor: the screech is how you know a modem is connecting, and skipping phases would break that.

Explicit timing tables beat clever generation. I could have derived the handshakes procedurally from a smaller set of rules, but the flat config with explicit durations means I can read the numbers and predict the audio, and identify a failing phase by its timestamp.


See also: Deep Dive: Bell 103 Audio Modem See also: Deep Dive: CTS Flow Control