Search code examples
c#audiowavlow-level

Real low level sound generation in C#?


Anyone knows of a sensible way to create an ARBITRARY sound wave in C# and play it back from the speakers?

This issue has been coming back to every now and then for years, I always end up giving it up after a lot of failure without finding a solution.

What I want to do is like a reverse-visualizer, that is, I don't want to generate "numbers" from sound, I want to generate sound from numbers.

Like get a function that I provide with sample rate, sample size, and the sound data (an array of integers for example), and it would generate the appropriate wav file from it (real-time sound playback would be ideal, but I'd be more than pleased with this too).

I know the wav file specifications are all over the interweb, and did make several attempts creating the above function, had some success for low frequencies, but once I start messing with bits per sample etc... it becomes a HUGE, uncontrollable mess.

Is this not already done in any way? I wouldn't mind what it uses, as long as there's a .NET managed wrapper for it (and I can access it from the most recent VS to time). XNA doesn't support low level audio this way. Also found several examples that claim to achieve something similar, but they either don't work at all, or do something entirely different.

Thank you.


Solution

  • This looked interesting so I've knocked up a simple app that:

    • Creates the samples for two seconds of a pure tone (440Hz A).
    • Converts them into a byte array in WAV file format.
    • Plays the sound by passing the byte array to the PlaySound API.
    • Also includes code to save the WAV data to a WAV file.

    You can easily change the sample rate, tone frequency and sample duration. The code is very ugly and space-inefficient but it works. The following is a complete command-line app:

    using System;
    using System.Diagnostics;
    using System.IO;
    using System.Runtime.InteropServices;
    
    namespace playwav
    {
        class Program
        {
            [DllImport("winmm.dll", EntryPoint = "PlaySound", SetLastError = true)]
            private extern static int PlaySound(byte[] wavData, IntPtr hModule, PlaySoundFlags flags);
    
            //#define SND_SYNC            0x0000  /* play synchronously (default) */
            //#define SND_ASYNC           0x0001  /* play asynchronously */
            //#define SND_NODEFAULT       0x0002  /* silence (!default) if sound not found */
            //#define SND_MEMORY          0x0004  /* pszSound points to a memory file */
            //#define SND_LOOP            0x0008  /* loop the sound until next sndPlaySound */
            //#define SND_NOSTOP          0x0010  /* don't stop any currently playing sound */
    
            //#define SND_NOWAIT      0x00002000L /* don't wait if the driver is busy */
            //#define SND_ALIAS       0x00010000L /* name is a registry alias */
            //#define SND_ALIAS_ID    0x00110000L /* alias is a predefined ID */
            //#define SND_FILENAME    0x00020000L /* name is file name */
            //#define SND_RESOURCE    0x00040004L /* name is resource name or atom */
    
            enum PlaySoundFlags
            {
                SND_SYNC = 0x0000,
                SND_ASYNC = 0x0001,
                SND_MEMORY = 0x0004
            }
    
            // Play a wav file appearing in a byte array
            static void PlayWav(byte[] wav)
            {
                PlaySound(wav, System.IntPtr.Zero, PlaySoundFlags.SND_MEMORY | PlaySoundFlags.SND_SYNC);
            }
    
            static byte[] ConvertSamplesToWavFileFormat(short[] left, short[] right, int sampleRate)
            {
                Debug.Assert(left.Length == right.Length);
    
                const int channelCount = 2;
                int sampleSize = sizeof(short) * channelCount * left.Length;
                int totalSize = 12 + 24 + 8 + sampleSize;
    
                byte[] wav = new byte[totalSize];
                int b = 0;
    
                // RIFF header
                wav[b++] = (byte)'R';
                wav[b++] = (byte)'I';
                wav[b++] = (byte)'F';
                wav[b++] = (byte)'F';
                int chunkSize = totalSize - 8;
                wav[b++] = (byte)(chunkSize & 0xff);
                wav[b++] = (byte)((chunkSize >> 8) & 0xff);
                wav[b++] = (byte)((chunkSize >> 16) & 0xff);
                wav[b++] = (byte)((chunkSize >> 24) & 0xff);
                wav[b++] = (byte)'W';
                wav[b++] = (byte)'A';
                wav[b++] = (byte)'V';
                wav[b++] = (byte)'E';
    
                // Format header
                wav[b++] = (byte)'f';
                wav[b++] = (byte)'m';
                wav[b++] = (byte)'t';
                wav[b++] = (byte)' ';
                wav[b++] = 16;
                wav[b++] = 0;
                wav[b++] = 0;
                wav[b++] = 0; // Chunk size
                wav[b++] = 1;
                wav[b++] = 0; // Compression code
                wav[b++] = channelCount;
                wav[b++] = 0; // Number of channels
                wav[b++] = (byte)(sampleRate & 0xff);
                wav[b++] = (byte)((sampleRate >> 8) & 0xff);
                wav[b++] = (byte)((sampleRate >> 16) & 0xff);
                wav[b++] = (byte)((sampleRate >> 24) & 0xff);
                int byteRate = sampleRate * channelCount * sizeof(short); // byte rate for all channels
                wav[b++] = (byte)(byteRate & 0xff);
                wav[b++] = (byte)((byteRate >> 8) & 0xff);
                wav[b++] = (byte)((byteRate >> 16) & 0xff);
                wav[b++] = (byte)((byteRate >> 24) & 0xff);
                wav[b++] = channelCount * sizeof(short);
                wav[b++] = 0; // Block align (bytes per sample)
                wav[b++] = sizeof(short) * 8;
                wav[b++] = 0; // Bits per sample
    
                // Data chunk header
                wav[b++] = (byte)'d';
                wav[b++] = (byte)'a';
                wav[b++] = (byte)'t';
                wav[b++] = (byte)'a';
                wav[b++] = (byte)(sampleSize & 0xff);
                wav[b++] = (byte)((sampleSize >> 8) & 0xff);
                wav[b++] = (byte)((sampleSize >> 16) & 0xff);
                wav[b++] = (byte)((sampleSize >> 24) & 0xff);
    
                Debug.Assert(b == 44);
    
                for (int s = 0; s != left.Length; ++s)
                {
                    wav[b++] = (byte)(left[s] & 0xff);
                    wav[b++] = (byte)(((ushort)left[s] >> 8) & 0xff);
                    wav[b++] = (byte)(right[s] & 0xff);
                    wav[b++] = (byte)(((ushort)right[s] >> 8) & 0xff);
                }
    
                Debug.Assert(b == totalSize);
    
                return wav;
            }
    
            // Create a simple sine wave
            static void CreateSamples(out short[] left, out short[] right, int sampleRate)
            {
                const double middleC = 261.626;
                const double standardA = 440;
    
                const double frequency = standardA;
    
                int count = sampleRate * 2; // Two seconds
                left = new short[count];
                right = new short[count];
    
                for (int i = 0; i != count; ++i)
                {
                    double t = (double)i / sampleRate; // Time of this sample in seconds
                    short s = (short)Math.Floor(Math.Sin(t * 2 * Math.PI * frequency) * short.MaxValue);
                    left[i] = s;
                    right[i] = s;
                }
            }
    
            static void Main(string[] args)
            {
                short[] left;
                short[] right;
                int sampleRate = 44100;
                CreateSamples(out left, out right, sampleRate);
                byte[] wav = ConvertSamplesToWavFileFormat(left, right, sampleRate);
                PlayWav(wav);
    
                /*
                // Write the data to a wav file
                using (FileStream fs = new FileStream(@"C:\documents and settings\carlos\desktop\a440stereo.wav", FileMode.Create))
                {
                    fs.Write(wav, 0, wav.Length);
                }
                */
            }
        }
    }