Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 995
jssyjrm Avatar asked Sep 18 '10 21:09

jssyjrm


3 Answers

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);
            }
            */
        }
    }
}
like image 133
arx Avatar answered Nov 05 '22 15:11

arx


FMOD can do sample loads from memory and has a C# wrapper.

like image 42
Kawa Avatar answered Nov 05 '22 16:11

Kawa


How to play from an array below

    PlayerEx pl = new PlayerEx();

    private static void PlayArray(PlayerEx pl)
    {
        double fs = 8000; // sample freq
        double freq = 1000; // desired tone
        short[] mySound = new short[4000];
        for (int i = 0; i < 4000; i++)
        {
            double t = (double)i / fs; // current time
            mySound[i] = (short)(Math.Cos(t * freq) * (short.MaxValue));
        }
        IntPtr format = AudioCompressionManager.GetPcmFormat(1, 16, (int)fs);
        pl.OpenPlayer(format);
        byte[] mySoundByte = new byte[mySound.Length * 2];
        Buffer.BlockCopy(mySound, 0, mySoundByte, 0, mySoundByte.Length);
        pl.AddData(mySoundByte);
        pl.StartPlay();
    }
like image 2
Basil Avatar answered Nov 05 '22 16:11

Basil