Click here to Skip to main content
Click here to Skip to main content

Steganography 15 - Hiding Digital Data in Music on Audio Cassettes

, 4 Jun 2005 CDDL
Rate this:
Please Sign up or sign in to vote.
How to hide data of any kind inside a sound, and keep it alive on an analog medium.

Introduction

Digital information is lossless. You can copy a file from hard disk to floppy to CD to flash stick to ... and it will still be the same file. But if you use analog media, such as good old tape, you can rely on the opposite: The data you read from the medium will definitely be different from what you wrote onto it.

Anyway, there are many ways to hide secret information in the sound on an audio tape. I'll explain a quite simple one. All you need to do, before you can try it, is to take a tape recorder, connect its microphone port to the headphone (or line-out) port of your computer, and the microphone (or line-in) port of the computer to the recorder's headphone port.

This article uses code from A full-duplex audio player in C# using the waveIn/waveOut APIs. The application requires Sound Exchange (SoX).

The Idea

After recording a sound onto an audio cassette, and re-recording it into a Wave file, we cannot rely on the binary data to be the same or in any way similar. So, changing a few bits is not enough anymore. We have to change the sound in a way we can recognize, even behind loads of noise and arbitrary changes.

Your first idea may be to insert a very short beeeep every n seconds, where n is a byte from the secret message. The recipient can extract the noises with a band pass filter, put the intervals into a stream, and just read the message from it. But trying that, you'll soon run out of tape:

A = 65
B = 66
C = 67
ABC = 65 + 66 + 67 = 198

Inserting a recognizable frequency in intervals that stand for the hidden bytes, we would need 198 seconds only for a short message like ABC.

Your second idea may be to split each secret byte into high and low half-byte, so that the maximum interval between two beeps is 15, and no byte can fill more than 30 seconds on the tape. For example, the character "z", that would block 122 seconds by the first try, needs only 17 seconds this way:

z = 122 = 1111010
half bytes = 7 (0111) and 10 (1010)

That is exactly what this article's application does. It allows you to search the carrier wave for an unused or low-volume frequency, and inserts very short, hardly hearable noises of just that frequency. You can then play the result, and record it with the tape recorder. To extract the hidden message, you just play the tape, record the sound with an audio recorder software (such as GoldWave etc.), and remove the silence from beginning and end. Then you can open the file with this application, enter the frequency that had been used for hiding, and watch as a band pass filter isolates the beeps and your message gets re-constructed.

How it Works

Hide a Message

While hiding a message, the user performs five steps:

  1. Select a Wave file, and enter the secret message.
  2. Check if the message fits into the sound, and shorten it, if necessary.
  3. Guess a frequency that occurs only in low volume, or not at all.

    If "Check sound" produces a warning, raise the threshold volume or frequency until the warning disappears.

  4. Write the new sound.
  5. Play the new sound, and record it onto the tape.

Steps three and four are not self-explaining. Let's start with step three: Checking a frequency for existence and maximum volume. The user guesses a frequency and clicks the "Check sound" button. To check if the highest amplitude of such a frequency is lower than the selected volume value (that means, we can use the selected combination), we first have to isolate the frequency with a band pass filter. SoX does that for us. Then we compare the result's samples to the selected maximum amplitude (call it volume, that's nearly the same in this case), and count the samples that are too loud.

//filter for frequency

String outFileName = waveUtility.FindFrequency(
       Path.GetTempPath(),
       soxPath, //contains a path like C:\somewhere\sox.exe
       (int)numFrequency.Value);

//Let another utility read the result file

WaveUtility filterUtility = new WaveUtility(outFileName);
WaveSound waveSound = filterUtility.WaveSound;

//filter for volume, check what is left of the sound

long countLoudSamples = 0;
short minVolumne = (short)(numHideVolume.Value - 100);
short[] samples = waveSound.Samples;
for (int n = 0; n < samples.Length; n++) {
    if (Math.Abs(samples[n]) > minVolumne) {
       countLoudSamples++;
    }
}

if (countLoudSamples > 0) {
   MessageBox.Show(String.Format("The Frequency might be" + 
     " a bad choice, because there are already {0} " + 
     "too loud samples in the sound.", countLoudSamples));
   errorProvider.SetError(numHideFrequency, 
     "Frequency not fitting, oder selected volume too low.");
} else {
   errorProvider.SetError(numHideFrequency, String.Empty);
}

Just for the case that you're interested in how waveUtility.FindFrequency works: it combines the parameters to an argument string, calls SoX, and reads the error output (if any).

/// <summary>Let "Sound Exchange" perform a band pass filter on the sound</summary>
/// <param name="tempDirectory">Path of the directory for temporary files</param>
/// <param name="soxPath">Path and Name of sox.exe</param>
/// <param name="frequency">Frequency that may pass the filter</param>
/// <returns>Path of the output file</returns>
public String FindFrequency(String tempDirectory, String soxPath, int frequency)
{
    String inFileName = Path.Combine(tempDirectory, "in.wav");
    String outFileName = Path.Combine(tempDirectory, "out.wav");
    int fileLength = this.WaveSound.SaveToFile(inFileName);

    String soxArguments = String.Format(
              "-t wav \"{0}\" -t .wav -c 1 -s -w \"{1}\" band {2} 10",
              inFileName,
              outFileName,
              frequency);

    RunSox(soxPath, soxArguments);
    return outFileName;
}

/// <summary>Let "Sound Exchange" convert the sound</summary>
/// <param name="soxPath">Path and Name of sox.exe</param>
/// <param name="soxArguments">Arguments for sox.exe</param>
private void RunSox(String soxPath, String soxArguments)
{
    ProcessStartInfo startInfo = new ProcessStartInfo(
                soxPath,
                soxArguments);
    startInfo.RedirectStandardError = true;
    startInfo.UseShellExecute = false;
    Process sox = Process.Start(startInfo);

    StreamReader errorReader = sox.StandardError;
    String errors = errorReader.ReadLine();
    if (errors != null) {
        throw new ApplicationException("sox failed: " + errors);
    }

    sox.WaitForExit(10000);
}

Once we have found a frequency/amplitude that will be unique in the carrier sound, so that it won't get mixed up with innocent samples when extracting the message, we can hide the secret stream.

/// <summary>Hide a message in the wave</summary>
/// <param name="message">Stream containing the message</param>
/// <param name="frequencyHz">Frequency of the beeps,
///        which will be inserted into the sound</param>
/// <param name="volumne">Maximum sample value of the beeps</param>
public void Hide(Stream message, int frequencyHz, int volumne)
{
    Stream preparedMessage = PrepareMessage(message);
    int messageByte;
    int offset = 0;
    while ((messageByte = preparedMessage.ReadByte()) > -1) {
        offset += messageByte;
        InsertBeep(offset, frequencyHz, volumne);
    }
}

The short piece of code above calls two important methods, PrepareMessage and InsertBeep. PrepareMessage takes the stream with the secret message, and splits each byte in two half-bytes. The interval between two beeps cannot be zero seconds, but some half-bytes may be zero, so every value in the prepared stream will be halfByte + 1. Later on, the extracting method will have to subtract 1 from every interval, to get the original half-bytes.

/// <summary>Split the bytes of a message into four-bit-blocks</summary>
/// <param name="message">Stream containing the message</param>
/// <returns>Stream containing the same
///        message with two bytes per original byte</returns>
private Stream PrepareMessage(Stream message)
{
    message.Position = 0;

    MemoryStream preparedMessage = new MemoryStream();
    int messageByte;
    int highHalfByte;
    int lowHalfByte;

    while ((messageByte = message.ReadByte()) > -1) {
        //split into high and low part
        highHalfByte = (messageByte >> 4);
        lowHalfByte = (messageByte - (highHalfByte << 4));

        //intervals of 0 seconds are not possible -> add 1 to all intervals
        preparedMessage.WriteByte((byte)(highHalfByte + 1));
        preparedMessage.WriteByte((byte)(lowHalfByte + 1));
    }

    preparedMessage.Position = 0;
    return preparedMessage;
}

What are we waiting for? We have the sound, the prepared messages stream, and a useable frequency. That's all we need to insert little noises at the specific seconds, which is the job of CreateBeep and InsertBeep.

/// <summary>Creates sine sound of a specific frequency</summary>
/// <param name="frequencyHz">Frequency in Hertz</param>
/// <param name="volumne">Amplitude</param>
private WaveSound CreateBeep(int frequencyHz, int volumne)
{
    // samples for 1/32 seconds
    short[] samples = new short[waveSound.Format.SamplesPerSec / 32];
    double xValue = 0;
    short yValue;

    double xStep = (2 * Math.PI) / waveSound.Format.SamplesPerSec; // 1 Hz
    xStep = xStep * frequencyHz;

    for (int n = 0; n < samples.Length; n++) {
        xValue += xStep;
        yValue = (short)(Math.Sin(xValue) * volumne);
        samples[n] = yValue;
    }

    WaveSound beep = new WaveSound(waveSound.Format, samples);
    return beep;
}

/// <summary>Replaces a part of the sound with a beep</summary>
/// <param name="insertAtSecond">Where to put the beep</param>
/// <param name="frequencyHz">Frequency of the beep in Hertz</param>
/// <param name="volumne">Maximum sample value of the beep</param>
public void InsertBeep(float insertAtSecond, int frequencyHz, int volumne)
{
    short[] beepWave = CreateBeep(frequencyHz, volumne).Samples;
    int insertAtSample = (int)(waveSound.Format.SamplesPerSec * insertAtSecond);
    int longWaveIndex = insertAtSample;
    for (int index = 0; index < beepWave.Length; index++) {
        waveSound[longWaveIndex] = beepWave[index];
        longWaveIndex++;
    }
}

After Hide has finished the while-loop, the complete message stream has been beeped into the sound. If you chose a fitting frequency an a not too high amplitude, it is hardly hearable (if not, it can still seem as usual noise to the average human ear, when played on average hardware). You can save the result to a .wav file, or play and record it without saving.

Extract a Message

Before reading a hidden message, the user has to filter the recorded sound. If the tape player added very bad noise in just our frequency, so that wrong beeps are detected, those errors can be deselected.

  1. Enter the frequency of the expected beeps, and band pass filter the sound. The second filter button - threshold volume - is not really necessary. You can use it to remove samples from the graphic, that will anyway be treated as silence.
  2. Find the noises. A beep is a group of samples that are greater than the selected threshold volume. In the graphic, the beginning and end of each detected beep are marked with red lines. The CheckBoxes allow you to exclude single beeps from evaluation, if you're sure that they don't belong to the message.
  3. Read the message. The last step lists the intervals between the selected noises, and re-constructs the hidden message.

"Filter sound" applies the same band pass filter we already know. This time, we don't count big samples, but open and display the filtered wave.

String outFileName = waveUtility.FindFrequency(
        Path.GetTempPath(),
        soxPath,
        (int)numExtractFrequency.Value);

waveControl.OpenFromFile(outFileName);

Every beep is 1/32 seconds long. That's not much for human ears, but it's a lot of samples. The groups of samples are separated by silence (due to band pass filter and threshold volume), so we can move a scan window over the samples, and define a piece of silence as all samples in the current time window are below the threshold. Samples above the threshold are treated as parts of one beep, if there is no silence between them.

/// <summary>Find anything but silence in the sound</summary>
/// <remarks>Raises the BeepFound event everytime a sound
///      is detected between two blocks of silence</remarks>
/// <param name="tolerance">
/// Sample values greater than [tolerance] are sound,
/// sample values less than [tolerance] are silence
/// </param>
public void FindAnything(short tolerance) {
    //size of scan window in samples
    int scanWindowSize = waveSound.Format.SamplesPerSec / beepLength;
    //size of scan window in seconds
    float scanWindowSizeSeconds = (float)scanWindowSize / 
                (float)waveSound.Format.SamplesPerSec;

    int startIndex = -1;
    int endIndex = -1;
    int countSilentSamples = 0;
    for (int n = 0; n < waveSound.Count; n++) {
        if (Math.Abs(WaveSound[n]) > tolerance) { //found a sound
            countSilentSamples = 0;
            if(startIndex < 0){
                startIndex = n;
            }
        } else if (startIndex > -1) { //searched and found silence
            countSilentSamples++;
            if (countSilentSamples == scanWindowSize) {
                endIndex = n - scanWindowSize;

                //tell the caller to mark a found beep in the wave
                NotifyOnBeep(startIndex, endIndex, scanWindowSizeSeconds);

                //scan next time window
                countSilentSamples = 0;
                startIndex = -1;
             }
        }
   }

   if (startIndex > -1) { //wave ends with a beep
       NotifyOnBeep(startIndex, waveSound.Count-1, scanWindowSizeSeconds);
   }
}

When the user finally clicks the "Read message" button, all information has already been extracted, we only have to put the half-bytes together.

private void btnExtract_Click(object sender, EventArgs e)
{
    //list the beginning seconds of the selected beeps
    Collection<Beep> selectedItems = waveControl.SelectedItems;
    Collection<float> startSeconds = new Collection<float>();
    foreach (Beep beep in selectedItems) {
        startSeconds.Add(beep.StartSecond);
    }

    //read the hidden message from the seconds
    Stream messageStream = waveUtility.Extract(startSeconds);
    StreamReader messageReader = new StreamReader(messageStream, Encoding.Default);
    String message = messageReader.ReadToEnd();
    messageReader.Close();

    txtExtractMessage.Text = message;
}

Sounds like an Example Wave

If you don't have Wave files at hand, for playing with the application, feel free to use these example records: demoWaves.zip.

  1. 101seconds.wav - 101 seconds long original sound (composed and played by me).
  2. result_f2500_v2000.wav - The wave with "! C# RULES !" hidden at 2500Hz with an amplitude of 2000.
  3. record_f2500_v2000.wav - Recorded by my notebook from the tape player on the photo, via headphone/microphone ports. Just try it, "! C# RULES !" can still be extracted without problems.

License

This article, along with any associated source code and files, is licensed under The Common Development and Distribution License (CDDL)

Share

About the Author

Corinna John
Software Developer
Germany Germany
Corinna lives in Hannover/Germany (CeBIT City) and works as a Delphi developer, though her favorite language is C#.

Comments and Discussions

 
QuestionAodio hiding in an audio file Pinmemberehsan assu18-May-14 18:39 
Generalhide wav Pinmembercameen6-Oct-10 7:16 
Generalmini project Pinmemberrithula16-Jun-10 1:44 
Questionurgent Pinmemberprof.deedee8-Jun-10 10:38 
AnswerRe: urgent PinmemberCorinna John8-Jun-10 11:52 
GeneralRe: urgent Pinmemberprof.deedee9-Jun-10 9:54 
QuestionIt's cool...I have a question, I need help [modified] Pinmemberrockyang8743-Feb-10 15:23 
GeneralHi Pinmembertnla21-Oct-09 9:57 
GeneralEncrypting in mp3 Pinmembersubiya17-Sep-09 22:12 
Generalgeneral Pinmemberatulpawar21-Dec-08 2:42 
GeneralRe: general PinmemberCorinna John21-Dec-08 3:09 
Generalpoject help PinmemberMunwar27-Jul-08 23:02 
QuestionHi Pinmemberrajnaeesh19-Jun-08 16:22 
AnswerRe: Hi PinmemberCorinna John23-Jun-08 21:52 
GeneralFun! PinmemberIlushka18-Sep-07 11:02 
Great Article, very fun Smile | :)
 
Sincerely yours, Ilya Kalujny.

GeneralGood Stuff PinmemberLawrence Botley1-May-07 12:45 
GeneralSome Question.... Pinmembervanleste2-Mar-07 4:17 
JokeRe: Some Question.... PinmemberCorinna John2-Mar-07 14:50 
GeneralHey, PinmemberMartin Welker21-Jun-05 12:24 
GeneralRe: Hey, PinmemberCorinna John23-Jun-05 21:50 
GeneralRe: Hey, PinmemberMartin Welker24-Jun-05 8:43 
Generalplease help my tesis PinsussAnonymous20-Jun-05 5:09 
GeneralNice! PinmemberManuel Wiegemann17-Jun-05 5:54 
GeneralRe: Nice! PinmemberCorinna John17-Jun-05 7:04 
GeneralRe: Nice! Pinmembershriop19-Jul-05 18:17 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.141223.1 | Last Updated 4 Jun 2005
Article Copyright 2005 by Corinna John
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid