New AssemblyAI-TTS-1: Text-to-speech for voice agents. Learn more
Insights & Use Cases

μ-law vs A-law: getting text-to-speech right on phone calls

Why a 24 kHz text-to-speech voice loses detail on an 8 kHz phone line, how μ-law and A-law differ, and how to request carrier-ready G.711 audio straight from the TTS API instead of transcoding it yourself.

Abstract green cylinder illustration

Written by

Kelsey Foster

Published on

3 November 2026

You pick a voice in the dashboard. It sounds warm, clear and close-miked. Then you put it on a real phone call and it sounds like it's talking through a paper towel tube.

Nothing is wrong with the voice. The phone network is doing exactly what it was designed to do in 1972: carry 8,000 samples a second, 8 bits each, squeezed through a logarithmic curve called μ-law or A-law. Every textbook page on the subject explains the math. Almost none of them tell you what that means for a voice agent that has to talk on that line.

So that's what this post covers: what G.711 actually carries, how μ-law and A-law differ, why a great voice loses detail on the wire, and how to ask your text-to-speech API for carrier-ready audio at the source instead of transcoding it yourself. There's also one asymmetry most teams find out about in production: on an A-law trunk, the audio you send and the audio you receive don't have to use the same handling.

What a phone line actually carries: 8 kHz G.711

Most phone calls that touch the public network still carry audio as ITU-T G.711, the pulse code modulation standard for voice frequencies. The numbers are small on purpose:

  • 8,000 samples per second. By the Nyquist limit, that caps the audio at 4 kHz. In practice the classic telephone band runs roughly 300 Hz to 3.4 kHz.
  • 8 bits per sample. One byte per sample, so 64 kbit/s per direction.
  • Logarithmic companding. Those 8 bits aren't spread evenly. Quiet sounds get finer steps and loud sounds get coarser ones, which gives 8 bits the useful range of roughly 13 to 14 bits of linear audio.

In SIP and RTP you'll see the same two codecs under different names. PCMU is μ-law and PCMA is A-law. Twilio calls its format audio/x-mulaw. Same bytes, different labels.

μ-law vs A-law: same idea, two companding curves

μ-law and A-law solve the same problem with slightly different curves. Both compress 16-bit-ish samples into 8 bits so that speech stays intelligible at low volume.

μ-law (PCMU) A-law (PCMA)
Where it's used North America and Japan Europe and most other regions
Curve μ = 255 A = 87.6
Linear range it encodes About 14 bits About 13 bits
Trade-off A little more dynamic range Slightly less distortion on quiet signals
Silence byte 0xFF 0xD5
RTP name PCMU PCMA

For a voice agent, the quality difference between the two is negligible. What matters is that you send the one the line expects. If you don't, something in the path transcodes it for you, and every transcode costs you a little fidelity and a little time.

That silence byte row is the one that bites people. In μ-law, a byte of 0x00 isn't silence. It decodes to almost full-scale negative amplitude. Pad a μ-law buffer with zero bytes and the caller hears a loud click or a burst of noise. Pad with 0xFF for μ-law and 0xD5 for A-law.

Why a great 24 kHz voice sounds worse on the line

Modern TTS models generate audio at a much higher rate than the phone network carries. AssemblyAI-TTS-1 produces 24 kHz, 16-bit PCM natively, which means it can represent frequencies up to 12 kHz. The phone line keeps everything below 4 kHz and throws the rest away.

What lives above 4 kHz? A lot of what makes speech crisp:

  • Fricatives. The hiss of "s", "f" and "th" sits mostly above 4 kHz. On the line, "fifty" and "sixty" start to blur, which is a problem when your agent reads back an amount.
  • Breath and air. The small, natural sounds that make a voice feel close and human.
  • Presence. The brightness that makes a voice cut through background noise on the caller's end.

Then companding adds quantization noise, and the caller's handset, cell codec and speakerphone each take their own cut.

You can't add that detail back later. A team we worked with was upsampling inbound 8 kHz μ-law to 16 kHz PCM before sending it to speech-to-text, assuming more samples meant better audio. It didn't add fidelity. The resampling added a faint hiss that pushed their voice activity detection into dropping turns. They switched to sending 8 kHz μ-law as-is and the problem went away. The same rule holds on the output side: once a signal has been to 8 kHz, it stays an 8 kHz signal.

So what can you control? Two things. Choose a voice that stays clear in the telephone band, and make sure the 8 kHz conversion happens exactly once, with a good resampler, as close to the model as possible. Our guide on how to evaluate TTS for voice agents covers how to test voices over a telephony path; this post explains why that test matters.

Build a phone agent that sounds right on the line

AssemblyAI streaming speech-to-text and text-to-speech both speak 8 kHz μ-law, so phone audio goes in and out without a transcoding step. Start with a free account and clear docs.

Sign up free

Ask for telephony audio at the source

The usual pipeline looks like this: request MP3 or 24 kHz PCM from the TTS API, decode it, resample to 8 kHz, encode to μ-law, then send. That's three steps of your own code, each with its own bugs, plus extra latency on every turn.

The better pipeline asks the TTS API for the wire format directly. The AssemblyAI Text-to-Speech API takes an encoding and a sample_rate on both its HTTP and WebSocket interfaces. Set encoding to pcm_mulaw or pcm_alaw and sample_rate to 8000, and the server resamples from the native 24 kHz and then encodes. According to the connect parameters docs, that server-side conversion added a few milliseconds at most in AssemblyAI's tests.

Here's the HTTP version with the Python SDK, which returns one complete clip. It's handy for prompts you cache, like greetings and hold messages:

import os
import assemblyai as aai

tts = aai.TextToSpeech(api_key=os.environ["ASSEMBLYAI_API_KEY"])

response = tts.synthesize(
    "Your appointment is confirmed for Tuesday at 3:30pm.",
    aai.TTSConfig(
        voice_id="49c443a4-8a1d-5f7e-b70b-98bdbd9823c3",  # Audrey
        encoding="pcm_mulaw",   # use "pcm_alaw" for an A-law trunk
        sample_rate=8000,
    ),
)

# Raw G.711 mu-law bytes, 1 byte per sample, no header: ready for the carrier
open("confirm.ulaw", "wb").write(response.audio)
print(response.sample_rate, response.encoding, response.audio_duration_ms)

The raw response comes back as audio/PCMU (or audio/PCMA for A-law), with no header. That's exactly what a carrier media stream wants.

One format to avoid on calls: MP3. According to the HTTP API docs, the MP3 encoder puts about 1,100 samples of silence before the speech, which is roughly 138 ms at 8 kHz. On a streamed response, that silence plays before every reply. For a live agent, raw G.711 starts sooner and needs no decoder.

The model is the same either way: AssemblyAI-TTS-1 was trained on millions of hours of audio, including real conversational data, and it's built for conversational voice agents. Asking for μ-law changes only the packaging; the voice stays the same.

Pacing and the 8,000 bytes-per-second math

The byte math for G.711 is refreshingly simple. One byte per sample, 8,000 samples per second, so one second of audio is 8,000 bytes. A 20 ms frame is 160 bytes.

Compare that with the native format: 24 kHz pcm_s16le is 48,000 bytes per second. If you're sizing buffers or bandwidth, telephony audio is a sixth of the bytes.

Here's the part that trips people up. Synthesis runs several times faster than real time, so your TTS stream will hand you a second of audio in well under a second. If you forward it to the carrier as fast as it arrives, you build a big buffer on the carrier side. Twilio, for example, queues outbound audio and gives you a clear message to empty that queue on barge-in, per its Media Streams docs. The bigger that queue, the longer your agent keeps talking after the caller interrupts.

So pace the bytes out at the wire rate. Here's a minimal sender for a Twilio Media Stream, using a streaming TTS session:

import base64
import json
import os
import time

import assemblyai as aai

FRAME_BYTES = 160          # 20 ms of 8 kHz mu-law (8,000 bytes per second)
FRAME_SECONDS = FRAME_BYTES / 8000

config = aai.TTSConfig(
    voice_id="49c443a4-8a1d-5f7e-b70b-98bdbd9823c3",  # Audrey
    language="english",
    encoding="pcm_mulaw",
    sample_rate=8000,
)


def speak_to_twilio(twilio_ws, stream_sid, text):
    """Synthesize one reply and send it to a Twilio Media Stream at wire rate."""
    buffer = b""
    next_send = time.monotonic()
    with aai.TTSSession(config, api_key=os.environ["ASSEMBLYAI_API_KEY"]) as session:
        for chunk in session.speak(text):
            buffer += chunk
            while len(buffer) >= FRAME_BYTES:
                frame, buffer = buffer[:FRAME_BYTES], buffer[FRAME_BYTES:]
                twilio_ws.send(json.dumps({
                    "event": "media",
                    "streamSid": stream_sid,
                    "media": {"payload": base64.b64encode(frame).decode("ascii")},
                }))
                next_send += FRAME_SECONDS
                time.sleep(max(0.0, next_send - time.monotonic()))
    if buffer:  # the last partial frame
        twilio_ws.send(json.dumps({
            "event": "media",
            "streamSid": stream_sid,
            "media": {"payload": base64.b64encode(buffer).decode("ascii")},
        }))

In production you'd keep one TTS session open per call rather than one per reply, and run this in an async loop next to your speech-to-text stream. For the full agent, with Universal-3.6 Pro Realtime transcribing the caller and turn detection driving the conversation, follow our Twilio voice agent build. If your agent places calls rather than answering them, the outbound call tutorial uses the same audio path. And TTS latency explains where the rest of your response time goes.

A cheap integrity check: the FlushDone frame on the WebSocket API reports audio_duration_ms. Divide the bytes you received by 8 and you should land within a few milliseconds of it. If you don't, something dropped audio.

WAV headers and the Python wave gotcha

Sooner or later you'll want to listen to what your agent sent. Raw μ-law bytes won't play on their own, because nothing tells the player what they are. You need a WAV header, and G.711 needs a specific one:

  • Format code 7 for μ-law, or 6 for A-law, with 8 bits per sample.
  • A fact chunk, which makes the header 58 bytes instead of the 44 you might be used to.

Two mistakes show up again and again. First, writing G.711 bytes under a standard 16-bit PCM header. The docs describe the result well: loud noise at half the real duration. Second, reaching for Python's wave module, which only handles plain PCM and won't open a G.711 WAV file at all. Players and ffmpeg open these files fine.

If you want to keep a raw capture and listen to it later, this writes a correct header:

import struct


def g711_wav_header(num_bytes, sample_rate=8000, alaw=False):
    """58-byte WAV header for mono 8-bit G.711 (format 7 = mu-law, 6 = A-law)."""
    fmt_code = 6 if alaw else 7
    fmt = struct.pack("<HHIIHHH", fmt_code, 1, sample_rate, sample_rate, 1, 8, 0)
    fact = struct.pack("<I", num_bytes)  # sample count = byte count at 1 byte/sample
    return (
        b"RIFF" + struct.pack("<I", 50 + num_bytes) + b"WAVE"
        + b"fmt " + struct.pack("<I", len(fmt)) + fmt
        + b"fact" + struct.pack("<I", len(fact)) + fact
        + b"data" + struct.pack("<I", num_bytes)
    )


ulaw = open("confirm.ulaw", "rb").read()
with open("confirm.wav", "wb") as f:
    f.write(g711_wav_header(len(ulaw)) + ulaw)

Or skip the header code entirely: the HTTP API returns a ready-made G.711 WAV file when you set the container to wav. And if you plan to analyze the audio with Python's wave module, ask for pcm_s16le at 8 kHz instead of G.711 for that debugging run.

Hear the difference on your own audio

Try AssemblyAI streaming speech-to-text in the browser and see how it handles real call audio before you wire up a carrier.

Try playground

Which encoding for Twilio, Telnyx and SIP trunks

Here's the short version for the carriers voice agent builders hit most. Carrier details come from each provider's own docs.

Carrier or path What it carries TTS request STT request
Twilio Media Streams Always audio/x-mulaw at 8000 Hz, base64 pcm_mulaw, 8000 pcm_mulaw, 8000
Telnyx media streaming, PCMU (default) μ-law at 8 kHz pcm_mulaw, 8000 pcm_mulaw, 8000
Telnyx media streaming, PCMA A-law at 8 kHz pcm_alaw, 8000 Decode to pcm_s16le, 8000
SIP trunk negotiating PCMU μ-law at 8 kHz pcm_mulaw, 8000 pcm_mulaw, 8000
SIP trunk negotiating PCMA A-law at 8 kHz pcm_alaw, 8000 Decode to pcm_s16le, 8000

A few notes on that table:

  • Twilio is the easy case. Its Media Streams docs say the inbound encoding is always audio/x-mulaw at 8000 Hz, and the audio you send back must be μ-law at 8000 Hz, base64-encoded.
  • Telnyx supports several codecs on bidirectional media streams, with PCMU at 8 kHz as the default. If the audio you send doesn't match the call's codec, Telnyx transcodes it, and its docs warn that this may degrade quality. Match the call.
  • SIP trunks use whatever the SDP negotiation settles on. Check the codec in the answer, then request that encoding.

The inbound A-law caveat

Look at the last column again. On an A-law call, the two directions aren't symmetric.

AssemblyAI TTS can produce A-law directly. Streaming speech-to-text, per the streaming API reference, accepts pcm_s16le, pcm_mulaw, opus, ogg_opus and aac, but not A-law. So on an A-law trunk, decode the caller's audio to 16-bit PCM before you send it to streaming speech-to-text, and keep it at 8 kHz. Don't upsample; as covered above, it adds nothing.

A-law decoding is a simple per-byte lookup with no resampling involved. Python's audioop module used to do it, but it was removed in Python 3.13, so here's a dependency-free decoder:

import struct


def alaw_to_pcm16(alaw_bytes):
    """Decode G.711 A-law to 16-bit little-endian PCM (same sample rate)."""
    out = bytearray()
    for byte in alaw_bytes:
        a = byte ^ 0x55
        exponent = (a & 0x70) >> 4
        mantissa = a & 0x0F
        if exponent == 0:
            sample = (mantissa << 4) + 8
        else:
            sample = ((mantissa << 4) + 0x108) << (exponent - 1)
        out += struct.pack("<h", sample if a & 0x80 else -sample)
    return bytes(out)


# Then stream it to Universal-3.6 Pro Realtime as 8 kHz pcm_s16le:
# StreamingParameters(speech_model="universal-3-6-pro", sample_rate=8000, encoding="pcm_s16le")

At call volume you'd precompute the 256 output values once and index into a table, but the math is the same. The result doubles your byte rate to 16,000 bytes per second, which is still tiny.

Universal-3.6 Pro Realtime transcribes 8 kHz phone audio directly, so you don't need to "improve" the signal before it arrives. If you're curious how different formats affect accuracy on the input side, see the best audio file formats for speech-to-text.

Treat the phone line as a target format

It's tempting to treat 8 kHz as a quality problem to engineer around. It's better to treat it as a format to target. The network won't change, and every transcode you add between the model and the wire is another place for clicks, drift and latency to sneak in.

The cleanest phone agents do three things. They ask for the line's own encoding at the source. They pace audio at 8,000 bytes per second so barge-in stays sharp. And on A-law trunks, they handle the inbound direction separately instead of assuming it mirrors the outbound one. If you're starting from scratch, building a phone-based voice agent walks through the whole loop, and the text-to-speech product page covers voices, languages and interfaces.

Put a voice on the phone today

Get an API key and request 8 kHz μ-law or A-law from AssemblyAI text-to-speech in a few lines of code. Your first call is a short script away.

Sign up free

Frequently Asked Questions (FAQs)

What is μ-law and A-law?

μ-law and A-law are the two companding curves defined in the ITU-T G.711 standard for carrying voice over telephone networks. Both compress each audio sample into 8 bits at 8,000 samples per second, giving quiet sounds finer steps than loud ones. μ-law is used mainly in North America and Japan, and A-law in Europe and most other regions.

What is companding and how is it used in digital communication?

Companding means compressing a signal's dynamic range before encoding it and expanding it again after decoding. In telephony, it lets 8-bit samples cover roughly the range of 13 to 14 bits of linear audio, so soft speech stays intelligible without doubling the bandwidth. G.711 μ-law and A-law are the most common companding schemes in voice networks.

Is PCMU the same as μ-law, and PCMA the same as A-law?

Yes. PCMU is the RTP and SIP name for G.711 μ-law, and PCMA is the name for G.711 A-law. Twilio labels the same μ-law format audio/x-mulaw, and AssemblyAI's APIs call them pcm_mulaw and pcm_alaw.

What is the best text-to-speech setup for a Twilio phone voice agent?

The best setup requests 8 kHz μ-law straight from the TTS API, so the audio needs no resampling or encoding before it goes to Twilio. Twilio Media Streams always carry audio/x-mulaw at 8000 Hz, and AssemblyAI text-to-speech returns exactly that when you set encoding to pcm_mulaw and sample_rate to 8000. Pair it with Universal-3.6 Pro Realtime, which accepts the same μ-law audio from the caller.

Why does my text-to-speech voice sound muffled on phone calls?

The phone line keeps only frequencies below about 4 kHz, so the sibilants, breath and brightness a 24 kHz voice carries above that are cut off. Companding then adds a little quantization noise, and the caller's handset and network add their own losses. You can't restore that detail, but you can avoid extra damage by converting to 8 kHz exactly once, at the source.

Can AssemblyAI speech-to-text accept A-law audio?

Not directly today. AssemblyAI streaming speech-to-text accepts pcm_s16le, pcm_mulaw, opus, ogg_opus and aac, so A-law calls need to be decoded to 16-bit PCM at 8 kHz before streaming. AssemblyAI text-to-speech can still produce A-law output directly with encoding set to pcm_alaw.