Audio Steganography

Hide secret messages or binary data within audio signals so that casual listeners hear only the original sound, while decoders can extract the hidden payload.

Three methods

Method

Capacity

Audibility

Robustness

Requirement

LSB

~1 bit/sample (~16 KB / 3 s @ 44.1 kHz)

Inaudible (≈ −90 dB)

Fragile (destroyed by lossy compression, resampling)

Any sample rate

Frequency-band

~2.6 kbit/s (~121 bytes / 3 s @ 44.1 kHz)

Above most listeners’ hearing

Moderate (survives mild noise)

sample_rate ≥ 40 kHz

Spectrogram text

~1 bit/sample (same as LSB)

Audible as buzzy tones; visually readable in spectrogram

Fragile (same as LSB)

Any sample rate

LSB flips the least-significant bit of a 16-bit PCM representation — distortion ≈ −90 dB. Best for lossless pipelines (WAV, FLAC).

Frequency-band encodes data as brief BFSK tone bursts at 18.5 kHz (bit 0) or 19.5 kHz (bit 1). Choose this when light interference is expected.

Spectrogram text is a hybrid method that hides data via LSB encoding and renders the message as readable text in a spectrogram view. The message is rasterised with a built-in bitmap font, and sine waves at corresponding frequencies produce visible characters when viewed with a spectrogram analyser.

See also

Spectrogram Text Art

Detailed guide on the spectrogram text art synthesis function.

miniDSP C library — Audio Steganography

Upstream C library documentation with algorithm details and C-level examples.

Message structure

All three methods prepend a 32-bit little-endian header: bits 0–30 hold the byte count, bit 31 indicates payload type (0 = text, 1 = binary). This lets the decoder recover messages without prior knowledge of length.

Hiding text

import pyminidsp as md

host = md.sine_wave(44100, amplitude=0.8, freq=440.0, sample_rate=44100.0)
stego, n = md.steg_encode(host, "secret message",
                           sample_rate=44100.0, method=md.STEG_LSB)
print(f"Encoded {n} bytes")

Listen — compare the host signal and the stego outputs:

Original host (440 Hz sine):

LSB-encoded (sounds identical):

Frequency-band encoded (faint high-frequency tones):

Spectrogram text encoding works the same way — just pass method=md.STEG_SPECTEXT:

stego_st, n = md.steg_encode(host, "HELLO",
                              sample_rate=44100.0, method=md.STEG_SPECTEXT)
print(f"Encoded {n} bytes (visible in spectrogram)")

Recovering text

recovered = md.steg_decode(stego, sample_rate=44100.0, method=md.STEG_LSB)
print(recovered)  # "secret message"

# Recover from spectrogram-text encoded signal
recovered_st = md.steg_decode(stego_st, sample_rate=44100.0, method=md.STEG_SPECTEXT)
print(recovered_st)  # "HELLO"

Binary data

data = b"\x00\x01\x02\xff\xfe\xfd"
stego, n = md.steg_encode_bytes(host, data, sample_rate=44100.0)
recovered = md.steg_decode_bytes(stego, sample_rate=44100.0)
assert recovered == data

Automatic detection

method, payload_type = md.steg_detect(stego, sample_rate=44100.0)
if method is not None:
    names = {md.STEG_LSB: "LSB", md.STEG_FREQ_BAND: "Freq-band",
             md.STEG_SPECTEXT: "Spectrogram-text"}
    print(f"Method: {names[method]}")
    print(f"Type: {'text' if payload_type == md.STEG_TYPE_TEXT else 'binary'}")

Capacity check

cap = md.steg_capacity(44100, sample_rate=44100.0, method=md.STEG_LSB)
print(f"Can hide up to {cap} bytes")

md.shutdown()