Click here to Skip to main content
15,891,248 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 236.6K   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;

namespace CPI.Audio
{
    /// <summary>
    /// Generates musical tones at specified frequencies, and saves them as a 16-bit wave file.
    /// </summary>
    public class SongWriter : IDisposable
    {
        # region Private Fields

        private readonly double _tempo;

        private readonly WaveWriter16Bit _writer;

        private short _defaultVolume;

        # endregion

        # region Constructors

        /// <summary>
        /// Instantiates a new SongWriter object.
        /// </summary>
        /// <param name="output">The Stream object to write the wave to.</param>
        /// <param name="tempo">The speed (in beats-per-minute) of playback.</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 SongWriter(Stream output, double tempo) : this(output, tempo, true) {}

        /// <summary>
        /// Instantiates a new SongWriter object.
        /// </summary>
        /// <param name="output">The Stream object to write the wave to.</param>
        /// <param name="tempo">The speed (in beats-per-minute) of playback.</param>
        /// <param name="closeUnderlyingStream">
        /// Determines whether to close the the stream that the SongWriter is writing to when the SongWriter is closed.
        /// </param>
        public SongWriter(Stream output, double tempo, bool closeUnderlyingStream)
            : this(output, tempo, 8000, closeUnderlyingStream) { }

        /// <summary>
        /// Instantiates a new SongWriter object.
        /// </summary>
        /// <param name="output">The Stream object to write the wave to.</param>
        /// <param name="tempo">The speed (in beats-per-minute) of playback.</param>
        /// <param name="defaultVolume">The default volume of the notes generated, ranging from 0 to short.MaxValue.</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 SongWriter(Stream output, double tempo, short defaultVolume)
            : this(output, tempo, defaultVolume, true) { }

        /// <summary>
        /// Instantiates a new SongWriter object.
        /// </summary>
        /// <param name="output">The Stream object to write the wave to.</param>
        /// <param name="tempo">The speed (in beats-per-minute) of playback.</param>
        /// <param name="defaultVolume">The default volume of the notes generated, ranging from 0 to short.MaxValue.</param>
        /// <param name="closeUnderlyingStream">
        /// Determines whether to close the the stream that the SongWriter is writing to when the SongWriter is closed.
        /// </param>
        public SongWriter(Stream output, double tempo, short defaultVolume, bool closeUnderlyingStream)
        {
            this._tempo = tempo;

            _writer = new WaveWriter16Bit(output, 44100, false, closeUnderlyingStream);

            _defaultVolume = defaultVolume;
        }

        # endregion

        # region Properties

        /// <summary>
        /// Gets the tempo (in beats-per-minute) of the song.
        /// </summary>
        public double Tempo
        {
            get
            {
                return _tempo;
            }
        }

        /// <summary>
        /// Gets or sets the current beat of the song.
        /// </summary>
        public double CurrentBeat
        {
            get
            {
                return (double)_writer.CurrentSample * Tempo / 60 / _writer.SampleRate;
            }
            set
            {
                _writer.CurrentSample = (long)(value * _writer.SampleRate * 60 / Tempo);
            }
        }

        /// <summary>
        /// Gets or sets the default volume of the song, ranging from 0 to short.MaxValue.
        /// </summary>
        public short DefaultVolume
        {
            get
            {
                return _defaultVolume;
            }
            set
            {
                if (value < 0)
                    throw new ArgumentOutOfRangeException("value", "Volume must be greater than or equal to zero.");

                _defaultVolume = value;
            }
        }

        # endregion

        # region Methods

        /// <summary>
        /// Adds a note to the song.
        /// </summary>
        /// <param name="frequency">The frequency of the note to add.</param>
        /// <param name="length">The length, in beats, of the note.</param>
        /// <remarks>
        /// The frequencies of all the notes on a piano keyboard have been defined in the Tones class.
        /// You can use those constants for the frequency parameter if you want.
        /// </remarks>
        public void AddNote(float frequency, double length)
        {
            AddNote(frequency, length, _defaultVolume);
        }

        /// <summary>
        /// Adds a note to the song.
        /// </summary>
        /// <param name="frequency">The frequency of the note to add.</param>
        /// <param name="length">The length, in beats, of the note.</param>
        /// <param name="volume">The volume of the note, ranging from 0 to short.MaxValue.</param>
        /// <remarks>
        /// The frequencies of all the notes on a piano keyboard have been defined in the Tones class.
        /// You can use those constants for the frequency parameter if you want.
        /// </remarks>
        public void AddNote(float frequency, double length, short volume)
        {
            if (volume < 0)
                throw new ArgumentOutOfRangeException("volume", "Volume must be greater than or equal to zero.");

            double samplesPerCycle = _writer.SampleRate / frequency;

            int samplesForNote = (int)(_writer.SampleRate * length * 60 / Tempo);

            
            Sample16Bit sample = new Sample16Bit();

            for (int currentSample = 0; currentSample < samplesForNote; currentSample++)
            {
                bool endOfStream = true;

                if (_writer.CurrentSample < _writer.NumberOfSamples)
                {
                    sample = _writer.Read();
                    endOfStream = false;
                }
                else
                    sample.LeftChannel = 0;

                // If we're at the end of the note, fade out linearly.
                // This causes two back-to-back notes with the same frequency
                // to have a break between them, rather than being one
                // continuous tone.
                int distanceFromEnd = samplesForNote - currentSample;
                short finalVolume = distanceFromEnd < 1000 ? ((short)(volume * distanceFromEnd / 1000)) : volume;

                double sampleValue = Math.Sin(currentSample / samplesPerCycle * 2 * Math.PI) * finalVolume + sample.LeftChannel;

                if (sampleValue > short.MaxValue)
                    sampleValue = short.MaxValue;
                else if (sampleValue < short.MinValue)
                    sampleValue = short.MinValue;

                sample.LeftChannel = (short)(sampleValue);

                if (endOfStream == false) 
                    _writer.CurrentSample--;

                _writer.Write(sample);
            }
        }

        /// <summary>
        /// Adds a rest in the song, essentially advancing the CurrentBeat property
        /// without adding any sound.
        /// </summary>
        /// <param name="length">The length, in beats, of the rest.</param>
        public void AddRest(double length)
        {
            CurrentBeat += length;
        }

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

        # endregion

        #region IDisposable Members

        /// <summary>
        /// Closes the SongWriter 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();
            }
        }

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