Click here to Skip to main content
15,897,273 members
Articles / Programming Languages / C#

Steganography 15 - Hiding Digital Data in Music on Audio Cassettes

Rate me:
Please Sign up or sign in to vote.
4.95/5 (41 votes)
4 Jun 2005CDDL6 min read 225.4K   6.7K   68  
How to hide data of any kind inside a sound, and keep it alive on an analog medium.
using System;
using System.IO;
using System.Diagnostics;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Windows.Forms;

namespace SteganoTape {
	public class WaveUtility {

		/// <summary>Called by FindBeeps for every Beep detected</summary>
		public event BeepFoundHandler BeepFound;

		private WaveSound waveSound;
		private const int beepLength = 32; // 1/32 second

		public WaveSound WaveSound
		{
			get { return waveSound; }
		}

		public WaveUtility(WaveSound waveSound)
		{
			this.waveSound = waveSound;
		}
		
		public WaveUtility(String fileName)
		{
			this.waveSound = ReadFile(fileName);
		}

		/// <summary>Creates sine sound of a specific frequency</summary>
		/// <param name="frequencyHz">Frequency in Hertz</param>
		/// <param name="volumne">Amplitude</param>
		/// <returns></returns>
		private WaveSound CreateBeep(int frequencyHz, int volumne)
		{
			// samples for 1/beepLength seconds
			short[] samples = new short[waveSound.Format.SamplesPerSec / beepLength];
			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>Read a wave file</summary>
		/// <param name="fileName"></param>
		/// <returns></returns>
		private WaveSound ReadFile(String fileName)
		{
			BinaryReader reader = new BinaryReader(new FileStream(fileName, FileMode.Open));

			WaveFormat format = ReadHeader(reader);
			int dataLength = reader.ReadInt32();
			
			//Test
			dataLength = (int)(reader.BaseStream.Length - reader.BaseStream.Position);

			int maxSampleValue = 0;
			int bytesPerSample = format.BitsPerSample / 8;
			int countSamples = dataLength / bytesPerSample;

			sbyte[] channelSamples8 = new sbyte[countSamples];
			short[] channelSamples16 = new short[countSamples];

			int channelSamplesIndex = 0;
			int absValue;
			for (int sampleIndex = 0; sampleIndex < countSamples; sampleIndex++) {

				if (format.BitsPerSample == 8) {
					channelSamples8[channelSamplesIndex] = reader.ReadSByte();
					if (Math.Abs((int)channelSamples8[channelSamplesIndex]) > maxSampleValue) {
						maxSampleValue = Math.Abs((int)channelSamples8[channelSamplesIndex]);
					}
				} else {
					channelSamples16[channelSamplesIndex] = reader.ReadInt16();
					absValue = Math.Abs((int)channelSamples16[channelSamplesIndex]);
					if (absValue > maxSampleValue) {
						maxSampleValue = (absValue > short.MaxValue) ? absValue - 1 : absValue;
					}
				}

				channelSamplesIndex++;
			}

			reader.Close();

			if (format.BitsPerSample == 8) {
				for (int n = 0; n < channelSamples8.Length; n++) {
					channelSamples16[n] = (short)channelSamples8[n];
				}
			}

			return new WaveSound(format, channelSamples16);
		}

		/// <summary>Read a chunk of four bytes from a wave file</summary>
		/// <param name="reader"></param>
		/// <returns></returns>
		private string ReadChunk(BinaryReader reader)
		{
			byte[] ch = new byte[4];
			reader.Read(ch, 0, ch.Length);
			return System.Text.Encoding.ASCII.GetString(ch);
		}

		/// <summary>
		/// Read the header from a wave file, and move the
		/// reader's position to the beginning of the data chunk
		/// </summary>
		/// <param name="reader"></param>
		/// <returns></returns>
		private WaveFormat ReadHeader(BinaryReader reader)
		{
			WaveFormat format = new WaveFormat();

			if (ReadChunk(reader) != "RIFF") {
				throw new Exception("Invalid file format");
			}

			reader.BaseStream.Seek(4, SeekOrigin.Current); // .ReadInt32();

			if (ReadChunk(reader) != "WAVE") {
				throw new ArgumentException("Kein WAVE-Stream", "reader");
			}

			if (ReadChunk(reader) != "fmt ") {
				throw new ArgumentException("Format-Chunk nicht gefunden", "reader");
			}

			int len = reader.ReadInt32();
			if (len < 16) {
				throw new ArgumentException("Format-Chunk hat eine ung�ltige L�nge", "reader");
			}

			format.FormatTag = reader.ReadInt16();
			format.Channels = reader.ReadInt16();
			format.SamplesPerSec = reader.ReadInt32();
			format.AvgBytesPerSec = reader.ReadInt32();
			format.BlockAlign = reader.ReadInt16();
			format.BitsPerSample = reader.ReadInt16();

			// go to beginning of wave samples
			while (reader.BaseStream.Position < reader.BaseStream.Length && ReadChunk(reader) != "data")
				;

			return format;
		}

		/// <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;
						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);
            }
        }

		/// <summary>Raise the BeepFound event</summary>
		/// <param name="startIndex">Index of the sound's first sample</param>
		/// <param name="endIndex">Index of the sound's last sample</param>
		/// <param name="silentSeconds"></param>
		private void NotifyOnBeep(int startIndex, int endIndex, float silentSeconds) {
			if (BeepFound != null) {
				//get the second in the wave at which the sound stops
				float second = (float)endIndex / (float)waveSound.Format.SamplesPerSec;

				//notify
				BeepFound(this, new BeepFoundEventArgs(
					new Beep(startIndex, endIndex,
					second - silentSeconds, second)));
			}
		}

		/// <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++;
			}
		}

		/// <summary>Get the minimum duration a sound must have in order to hide [message]</summary>
		/// <param name="message"></param>
		/// <returns></returns>
		public long CountRequiredSeconds(Stream message)
		{
			message.Position = 0;

			long countSeconds = 0;
			int messageByte;
			byte highHalfByte;
			byte lowHalfByte;

			while ((messageByte = message.ReadByte()) > -1) {
				highHalfByte = (byte)(messageByte >> 4);
				lowHalfByte = (byte)(messageByte - (highHalfByte << 4));

				//intervals of 0 seconds are not possible -> add 1 to all intervals
				countSeconds += highHalfByte + 1;
				countSeconds += lowHalfByte + 1;
			}

			return countSeconds;
		}

		/// <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;
		}

		/// <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);
			}
		}

		/// <summary>Read a message from a series of seconds</summary>
		/// <param name="seconds">Seconds at which beeps have been found in the sound</param>
		/// <returns>Stream containing the decoded message</returns>
		public Stream Extract(Collection<float> seconds)
		{
			MemoryStream message = new MemoryStream();

			byte interval;
			int second;
			int previousSecond = 0;
			byte messageByte = 0;
			bool isLowPart = false;
			foreach(float floatSecond in seconds) {
				second = (int)Math.Round(floatSecond);
				try {
					interval = (byte)(second - previousSecond);
					if (interval > 16) { interval = 16; }
				} catch (OverflowException ex) {
					Console.WriteLine(ex);
					interval = 16; //highest possible value for a half byte + 1
				}
				
				if (isLowPart) {
					messageByte += (byte)(interval - 1);
					message.WriteByte(messageByte);
				}else{
					messageByte = (byte)((interval-1) << 4);
				}

				isLowPart = !isLowPart;
				previousSecond = second;
			}

			message.Position = 0;
			return message;
		}

		/// <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)
		{
			if (Application.OpenForms.Count > 0) { //there is a visible form
				Application.OpenForms[0].Cursor = Cursors.WaitCursor;
			}

			try {

				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);

			} finally {
				if (Application.OpenForms.Count > 0) { //reset cursor for the visible form
					Application.OpenForms[0].Cursor = Cursors.Default;
				}
			}
		}

		/// <summary>Convert an 8 bit sound to 16 bit.</summary>
		/// <param name="tempDirectory">Path of the directory for temporary files</param>
		/// <param name="soxPath">Path and Name of sox.exe</param>
		public void ConvertToDefaultFormat(String tempDirectory, String soxPath)
		{
			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}\"",
				inFileName,
				outFileName);

			RunSox(soxPath, soxArguments);

			this.waveSound = ReadFile(outFileName);
		}

		/// <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;
		}

	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

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


Written By
Software Developer
Germany Germany
Corinna lives in Hanover/Germany and works as a C# developer.

Comments and Discussions