Click here to Skip to main content
15,886,797 members
Articles / Programming Languages / C#

Compose sounds from frequencies and visualize them

Rate me:
Please Sign up or sign in to vote.
4.87/5 (53 votes)
17 Apr 2006CPOL6 min read 190.9K   3.5K   162  
What you never wanted to know about PCM.
/* This class has been written by
 * Corinna John (Hannover, Germany)
 * cj@binary-universe.net
 * 
 * The GPL (GNU General public license)
 * is valid for this file. Other files of the project
 * may be free of specific licenses, but this one will
 * be used in other GPL projects.
 * 
 * Please send me a little feedback about what you're
 * using the code for and what changes you'd like to
 * see in later versions.
 */

using System;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Diagnostics;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Windows.Forms;

namespace WaveMixer
{

	/// <summary>Encapsulates the colour flags for visualizing a wave channel.</summary>
	public class ChannelColorFlags
	{
		private bool isRed;
		private bool isGreen;
		private bool isBlue;

		public bool IsRed
		{
			get { return isRed; }
			set { isRed = value; }
		}

		public bool IsBlue
		{
			get { return isBlue; }
			set { isBlue = value; }
		}

		public bool IsGreen
		{
			get { return isGreen; }
			set { isGreen = value; }
		}
	}

	/// <summary>Stores the colours of a pixel.</summary>
	public struct PixelData
	{
		public byte Blue;
		public byte Green;
		public byte Red;
	}

    /// <summary>Edits a PCM wave sound.</summary>
    public class WaveUtility
	{
		/// <summary>Adds a sine sound of a specific frequency.</summary>
        /// <param name="waveSound">The sound that's being edited</param>
        /// <param name="frequencyHz">Frequency of the new wave in Hertz.</param>
		/// <param name="offsetSeconds">Starting second of the new wave.</param>
		/// <param name="lengthSeconds">Length of the new wave in seconds.</param>
        /// <param name="amplitude">Maximum amplitude of the new wave.</param>
		public void AddWave(WaveSound waveSound, float frequencyHz, float offsetSeconds, float lengthSeconds, int amplitude)
		{
			//existing wave samples
			short[] samples = waveSound.Samples;

			//interval for 1 Hz
			double xStep = (2 * Math.PI) / waveSound.Format.SamplesPerSec;
			//interval for the requested frequency = 1Hz * frequencyHz
			xStep = xStep * frequencyHz;

			long lastSample;
			double xValue = 0;
			short yValue;
			short channelSample;
			
			long offsetSamples = (long)(waveSound.Format.Channels * waveSound.Format.SamplesPerSec * offsetSeconds);
			if (offsetSamples < samples.Length) //if the beginning sample exists
			{
				lastSample = (long)(waveSound.Format.Channels * waveSound.Format.SamplesPerSec * (offsetSeconds + lengthSeconds));
				if (lastSample > samples.Length)
				{	//last sample does not exist - shorten the new wave
					lastSample = samples.Length - waveSound.Format.Channels + 1;
				}

				for (long n = offsetSamples; n < lastSample; n += waveSound.Format.Channels)
				{
					xValue += xStep;
					yValue = (short)(Math.Sin(xValue) * amplitude);

					for (int channelIndex = 0; channelIndex < waveSound.Format.Channels; channelIndex++)
					{	
						channelSample = samples[n + channelIndex];
						channelSample = (short)((channelSample + yValue) / 2);
						samples[n + channelIndex] = channelSample;
					}
				}
			}
		}

		/// <summary>Changes the volume of a sound.</summary>
		/// <param name="waveSound">The sound that's being edited.</param>
		/// <param name="maxSampleValue">Current maximum amplitude.</param>
		/// <param name="newMaximum">New maximum amplitude. May not be greater than 127, otherwise the result will be useless.</param>
		public void ChangeAmplitude(WaveSound waveSound, short maxSampleValue, short newMaximum)
		{
			float scale = (float)newMaximum / maxSampleValue;

			for (int waveIndex = 0; waveIndex < waveSound.Count; waveIndex++)
			{
				waveSound[waveIndex] = (short)(waveSound[waveIndex] * scale);
			}
		}

		/// <summary>Paints a bitmap for a wave.</summary>
		/// <param name="waveSound">The sound you want to paint.</param>
		/// <param name="width">Width of the bitmap.</param>
		/// <param name="height">Height of the bitmap.</param>
		/// <param name="firstChannelColor">Colors to use for the first channel.</param>
		/// <param name="secondChannelColor">Colors to use for the seconds channel.</param>
		/// <param name="maximizeVolume">Whether or not to paint the sound with maximized amplitude == best contrast.</param>
		/// <param name="maxSampleValue">Value of the loudest sample. Can be zero if  [maximizeVolume] is false.</param>
		/// <returns>Picture of the sound.</returns>
		public unsafe Bitmap PaintWave(WaveSound waveSound, int width, int height, ChannelColorFlags firstChannelColor, ChannelColorFlags secondChannelColor, bool convertSignedToAbsoluteValues, bool maximizeVolume, short maxSampleValue)
		{
			Bitmap bmp = new Bitmap(width, height, PixelFormat.Format24bppRgb);

			BitmapData bitmapData = bmp.LockBits(
				new Rectangle(0, 0, width, height),
				ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);

			int stride = bitmapData.Stride;
			int wastedPixels = stride - width*3;
			PixelData* pPixel = (PixelData*)bitmapData.Scan0.ToPointer();
			
			try
			{
				ChannelColorFlags[] channelColors = new ChannelColorFlags[2] { firstChannelColor, secondChannelColor };
				Color color;
				int pixelColumnIndex = 0;
				short currentSample;
				short currentChannelSample;
				short[] channelPartMaximum = new short[waveSound.Format.Channels];
				short[] channelPartMinimum = new short[waveSound.Format.Channels];
				float maximizeScale = 127f / maxSampleValue;

				if (maximizeVolume)
				{
					maximizeScale = maximizeScale * short.MaxValue / maxSampleValue;
				}

				int countPixels = width * height;
				int samplesPerPixel = 1;
				if (countPixels < waveSound.Count)
				{
					samplesPerPixel = (int)Math.Ceiling((double)waveSound.Count / countPixels);
				}

				int maxIndex = waveSound.Count - waveSound.Format.Channels;
				int maxCountChannels = (waveSound.Format.Channels == 1) ? 1 : 2;

				for (int waveIndex = 0; waveIndex < maxIndex; waveIndex += (samplesPerPixel * waveSound.Format.Channels))
				{
					//find the loudest sample that will be painted in the current pixel
					for (int sampleIndex = waveIndex; sampleIndex < (waveIndex + samplesPerPixel) && sampleIndex < waveSound.Count; sampleIndex += waveSound.Format.Channels)
					{
						currentSample = waveSound[sampleIndex];

						for (int channelIndex = 0; channelIndex < waveSound.Format.Channels; channelIndex++)
						{
							channelPartMaximum[channelIndex] = 0;
							channelPartMinimum[channelIndex] = 0;
						}
						for (int channelIndex = 0; channelIndex < waveSound.Format.Channels; channelIndex++)
						{
							if (currentSample > channelPartMaximum[channelIndex]) { channelPartMaximum[channelIndex] = currentSample; }
							if (currentSample < channelPartMinimum[channelIndex]) { channelPartMinimum[channelIndex] = currentSample; }
						}
					}


					for (int channelIndex = 0; channelIndex < maxCountChannels; channelIndex++)
					{
						currentChannelSample = (channelPartMaximum[channelIndex] > channelPartMinimum[channelIndex] * -1)
							? channelPartMaximum[channelIndex]
							: channelPartMinimum[channelIndex];

						currentChannelSample = (sbyte)(currentChannelSample * maximizeScale);

						if (convertSignedToAbsoluteValues)
						{
							color = Color.FromArgb(
								channelColors[channelIndex].IsRed ? (int)Math.Abs((int)currentChannelSample) : 0,
								channelColors[channelIndex].IsGreen ? (int)Math.Abs((int)currentChannelSample) : 0,
								channelColors[channelIndex].IsBlue ? (int)Math.Abs((int)currentChannelSample) : 0);

						}
						else
						{
							color = Color.FromArgb(
								channelColors[channelIndex].IsRed ? (int)Math.Abs(currentChannelSample + 127) : 0,
								channelColors[channelIndex].IsGreen ? (int)Math.Abs(currentChannelSample + 127) : 0,
								channelColors[channelIndex].IsBlue ? (int)Math.Abs(currentChannelSample + 127) : 0);

						}
						
						SetPixel(pPixel, color);
						pPixel += 1;
						pixelColumnIndex++;

						if (pixelColumnIndex == width && wastedPixels > 0)
						{	//skip the invisible bytes and enter the next row
							pixelColumnIndex = 0;
							IntPtr pointer = new IntPtr(pPixel);
							int newAddress = pointer.ToInt32() + wastedPixels;
							pPixel = (PixelData*)newAddress;
						}
					}
				}
			}
			finally
			{
				bmp.UnlockBits(bitmapData);
			}

			return bmp;
		}

		private unsafe void SetPixel(PixelData* pPixel, Color newColor)
		{
			pPixel->Red = (byte)newColor.R;
			pPixel->Green = (byte)newColor.G;
			pPixel->Blue = (byte)newColor.B;
		}
	}
}

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 Code Project Open License (CPOL)


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

Comments and Discussions