by Wojciech Sura

How to write a tone generator?

Let’s continue our adventure with FMOD. Today we’ll write a simple sine tone generator.

FMOD supports live streaming of the sound – it periodically asks program for new block of data. We’ll use it to generate sine wave of a specific frequency.

First of all, we need to instantiate and initialize a System object. Nothing new here.

[csharp]public Form1()
{
InitializeComponent();

FMOD.Factory.System_Create(ref system);
system.init(1, FMOD.INITFLAGS.NORMAL, IntPtr.Zero);
}[/csharp]

Now we have to prepare special version of Sound class – a stream. This process is a little bit more complicated than creating a sound from mp3, so let’s take a closer look on the following piece of source code:

[csharp]private const int sampleRate = 44100;
private const int channels = 1;
private const int lengthInSec = 1;
private const int freq = 440;

private void bPlay_Click(object sender, EventArgs e)
{
var exInfo = new FMOD.CREATESOUNDEXINFO();
exInfo.cbsize = Marshal.SizeOf(exInfo);
exInfo.decodebuffersize = sampleRate; // 1 sec
exInfo.length = sampleRate * sizeof(short) * lengthInSec;
exInfo.numchannels = channels;
exInfo.defaultfrequency = sampleRate;
exInfo.format = FMOD.SOUND_FORMAT.PCM16;
exInfo.pcmreadcallback = new FMOD.SOUND_PCMREADCALLBACK(DoReadData);
exInfo.pcmsetposcallback = new FMOD.SOUND_PCMSETPOSCALLBACK(DoSetPos);

current = 0;

system.createStream(String.Empty, FMOD.MODE.OPENUSER | FMOD.MODE.LOOP_NORMAL, ref exInfo, ref sound);
system.playSound(FMOD.CHANNELINDEX.FREE, sound, false, ref channel);
}[/csharp]

A special structure is used to describe, what kind of stream do we want to create. Inside, we specify:

  • cbsize – size of the structure in bytes. Remember, FMOD is natively a C/C++ library.
  • decodebuffersize – size of the buffer, for which FMOD will be asking us periodically, during the playback. We fill it with sampleRate – 44100 samples, which will represent a second of playback (since there are exactly 44100 per second)
  • length – how long is the sound, which we want to stream. In this case it is irrelevant, since sound will be looped.
  • numchannels – how many channels will we use (in this case only a mono sound)
  • defaultfrequency – frequency of sampling
  • format – format of data, which will be sent to FMOD. PCM16 means, that we will send raw data composed of 16-bit signed integer (short).
  • pcmreadcallback – a delegate to method, which will be called whenever FMOD requires data for playback.
  • pcmsetposcallback – a delegate to method, which will be called if someone tries to change position of playback.

Notice, that we pass FMOD.MODE.LOOP_NORMAL to ensure, that sound will be looped. Now we can implement both callbacks.

[csharp]private FMOD.RESULT DoSetPos(IntPtr soundraw, int subsound, uint position, FMOD.TIMEUNIT postype)
{
return FMOD.RESULT.OK;
}[/csharp]

Since we actually don’t plan allowing changing position of the playback, we may simply ignore this callback, returning FMOD.RESULT.OK.

The data read callback is a little bit more complicated.

[csharp]private FMOD.RESULT DoReadData(IntPtr soundraw, IntPtr data, uint datalen)
{
int dataCount = (int)(datalen / sizeof(short));
short[] rawData = new short[dataCount];

double multiplier = ((double)sampleRate / (double)freq) / (2 * Math.PI);
for (int i = 0; i < rawData.Length; i++)
rawData[i] = (short)(Math.Sin((current + i) / multiplier) * short.MaxValue);

Marshal.Copy(rawData, 0, data, rawData.Length);

current += rawData.Length;

return FMOD.RESULT.OK;
}[/csharp]

FMOD gives us two key information. First is data: pointer to buffer in which we will place the actual wave data. Second is datalen, which is size of the buffer (in bytes, so count of samples is actually datalen / sizeof(short)).

Now some maths. We want to achieve sound of frequency 440 Hz. We know, that one second contains 44100 samples, and we want one second to contain 440 complete sine waves, so single wave will be (44100/440) ~= 100.22 samples long. Since sine loops at 2π, we have to multiply the actual data passed to Math.Sin by 2π (such that it will loop at 100.22 samples instead).

The rest is simple – we copy the data to given buffer using the Marshal.Copy method and return FMOD.RESULT.OK. The current field is used in order to provide fluent playback.

Stopping code is identical as in previous example.

[csharp]private void bStop_Click(object sender, EventArgs e)
{
if (channel != null)
channel.stop();
}[/csharp]