Click here to Skip to main content
15,881,669 members
Articles / Programming Languages / C#

Fundamentals of Sound: How to Make Music out of Nothing at All

Rate me:
Please Sign up or sign in to vote.
4.89/5 (106 votes)
30 May 2007CPOL10 min read 236K   3.8K   211  
This article describes the basics of sound waves as well as the PCM WAVE format, and illustrates how to create music by writing your own custom wave form.
using System;
using System.IO;
using System.Collections.Generic;

namespace CPI.Audio
{
    /// <summary>
    /// Generates tones from a touch-tone phone.
    /// </summary>
    public class TouchToneGenerator : IDisposable
    {
        # region Static Fields

        private static readonly double[] columnTones = new double[] { 1209, 1336, 1477 };
        private static readonly double[] rowTones = new double[] { 697, 770, 852, 941 };
        private static readonly Dictionary<char, double> columnTone = new Dictionary<char, double>();
        private static readonly Dictionary<char, double> rowTone = new Dictionary<char, double>();
        private static readonly Dictionary<char, char> letterTranslations = new Dictionary<char, char>();

        # endregion

        # region Static Constructors

        static TouchToneGenerator()
        {
            columnTone['1'] = columnTones[0];
            columnTone['2'] = columnTones[1];
            columnTone['3'] = columnTones[2];
            columnTone['4'] = columnTones[0];
            columnTone['5'] = columnTones[1];
            columnTone['6'] = columnTones[2];
            columnTone['7'] = columnTones[0];
            columnTone['8'] = columnTones[1];
            columnTone['9'] = columnTones[2];
            columnTone['*'] = columnTones[0];
            columnTone['0'] = columnTones[1];
            columnTone['#'] = columnTones[2];

            rowTone['1'] = rowTones[0];
            rowTone['2'] = rowTones[0];
            rowTone['3'] = rowTones[0];
            rowTone['4'] = rowTones[1];
            rowTone['5'] = rowTones[1];
            rowTone['6'] = rowTones[1];
            rowTone['7'] = rowTones[2];
            rowTone['8'] = rowTones[2];
            rowTone['9'] = rowTones[2];
            rowTone['*'] = rowTones[3];
            rowTone['0'] = rowTones[3];
            rowTone['#'] = rowTones[3];

            letterTranslations['1'] = '1';
            letterTranslations['2'] = '2';
            letterTranslations['3'] = '3';
            letterTranslations['4'] = '4';
            letterTranslations['5'] = '5';
            letterTranslations['6'] = '6';
            letterTranslations['7'] = '7';
            letterTranslations['8'] = '8';
            letterTranslations['9'] = '9';
            letterTranslations['*'] = '*';
            letterTranslations['0'] = '0';
            letterTranslations['#'] = '#';

            letterTranslations['A'] = '2';
            letterTranslations['B'] = '2';
            letterTranslations['C'] = '2';
            letterTranslations['D'] = '3';
            letterTranslations['E'] = '3';
            letterTranslations['F'] = '3';
            letterTranslations['G'] = '4';
            letterTranslations['H'] = '4';
            letterTranslations['I'] = '4';
            letterTranslations['J'] = '5';
            letterTranslations['K'] = '5';
            letterTranslations['L'] = '5';
            letterTranslations['M'] = '6';
            letterTranslations['N'] = '6';
            letterTranslations['O'] = '6';
            letterTranslations['P'] = '7';
            letterTranslations['R'] = '7';
            letterTranslations['S'] = '7';
            letterTranslations['T'] = '8';
            letterTranslations['U'] = '8';
            letterTranslations['V'] = '8';
            letterTranslations['W'] = '9';
            letterTranslations['X'] = '9';
            letterTranslations['Y'] = '9';
        }

        # endregion

        # region Private Fields

        private readonly WaveWriter8Bit _writer;

        private readonly int _samplesPerTone;

        private readonly int _samplesPerPause;

        # endregion

        # region Constructors

        /// <summary>
        /// Instantiates a new TouchToneGenerator object.
        /// </summary>
        /// <param name="output">The Stream object to write the wave to.</param>
        /// <remarks>
        /// When this object is disposed, it will close its underlying stream.  To change this default behavior,
        /// you can call an overloaded constructor which takes a closeUnderlyingStream parameter.
        /// </remarks>
        public TouchToneGenerator(Stream output)
            : this(output, true) { }

        /// <summary>
        /// Instantiates a new TouchToneGenerator object.
        /// </summary>
        /// <param name="output">The Stream object to write the wave to.</param>
        /// <param name="closeUnderlyingStream">
        /// Determines whether to close the the stream that the TouchToneGenerator 
        /// is writing to when the TouchToneGenerator is closed.
        /// </param>
        public TouchToneGenerator(Stream output, bool closeUnderlyingStream)
        {
            _writer = new WaveWriter8Bit(output, 8000, false, closeUnderlyingStream);

            _samplesPerTone = _writer.SampleRate * 9 / 20;

            _samplesPerPause = _writer.SampleRate / 20;

        }

        # endregion

        # region Methods

        /// <summary>
        /// Generates touchtone phone tones for a string of digits.
        /// </summary>
        /// <param name="phoneNumber">
        /// A string of digits.
        /// Valid digits are: 1234567890*#ABCDEFGHIJKLMNOPRSTUVWXY
        /// Invalid digits will be silently ignored.
        /// </param>
        public void GenerateTones(string phoneNumber)
        {
            foreach (char digit in phoneNumber.ToUpper())
            {
                if (letterTranslations.ContainsKey(digit))
                    GenerateTone(letterTranslations[digit]);
            }
        }

        private void GenerateTone(char digit)
        {
            double columnFrequency = columnTone[digit];
            double rowFrequency = rowTone[digit];

            double samplesPerColumnCycle = _writer.SampleRate / columnFrequency;
            double samplesPerRowCycle = _writer.SampleRate / rowFrequency;


            Sample8Bit sample = new Sample8Bit(false);

            long startPoint = _writer.CurrentSample;

            // Write out the tone associated with the digit's column
            for (int currentSample = 0; currentSample < _samplesPerTone; currentSample++)
            {
                double sampleValue = Math.Sin(currentSample / samplesPerColumnCycle * 2 * Math.PI) * 60;

                sample.LeftChannel = (byte)(sampleValue + 128);

                _writer.Write(sample);
            }

            // go back to the starting point
            _writer.CurrentSample = startPoint;

            // Overlay the tone associated with the digit's row
            for (int currentSample = 0; currentSample < _samplesPerTone; currentSample++)
            {
                // Figure out what the sample was originally set to
                Sample8Bit originalSample = _writer.Read();
                _writer.CurrentSample--;

                double sampleValue = Math.Sin(currentSample / samplesPerRowCycle * 2 * Math.PI) * 60;

                sample.LeftChannel = (byte)(sampleValue + originalSample.LeftChannel);

                _writer.Write(sample);
            }

            // Insert a pause
            sample.LeftChannel = 128;

            for (int currentSample = 0; currentSample < _samplesPerPause; currentSample++)
            {
                _writer.Write(sample);
            }
        }

        /// <summary>
        /// Closes the TouchToneGenerator and saves the underlying stream.
        /// </summary>
        public void Close()
        {
            if (_writer != null)
                _writer.Close();
        }

        /// <summary>
        /// Closes the TouchToneGenerator and the underlying stream.
        /// </summary>
        /// <param name="disposing">true if called by the Dispose() method; false if called by a finalizer.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                Close();
            }
        }

        # endregion

        #region IDisposable Members

        /// <summary>
        /// Disposes this object and cleans up any resources used.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);

            GC.SuppressFinalize(this);
        }

        #endregion
    }
}

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 (Senior)
United States United States
Pete has just recently become a corporate sell-out, working for a wholly-owned subsidiary of "The Man". He counter-balances his soul-crushing professional life by practicing circus acrobatics and watching Phineas and Ferb reruns. Ducky Momo is his friend.

Comments and Discussions