Here is a program I wrote that uses waveOutOpen
and waveOutWrite
to generate 48 kHz stereo audio in real time. It is possible to generate the audio using the main thread, but this program launches a separate thread so that it can provide the nice kind of API you wanted.
#include <windows.h>
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <math.h>
#define BUFFER_COUNT 4
#define BUFFER_SAMPLE_COUNT 1000
#define SAMPLES_PER_SECOND 48000
HWAVEOUT waveOut;
HANDLE waveEvent;
WAVEHDR headers[BUFFER_COUNT];
int16_t buffers[BUFFER_COUNT][BUFFER_SAMPLE_COUNT * 2];
WAVEHDR * currentHeader;
double volume = 6000;
double phase;
double phase_increment;
double audio_value;
void sound(double frequency) {
if (frequency == 0) {
phase_increment = 0;
return;
}
phase_increment = 2 * M_PI / SAMPLES_PER_SECOND * frequency;
}
void fill_buffer(int16_t * buffer) {
for (size_t i = 0; i < BUFFER_SAMPLE_COUNT * 2; i += 2) {
if (phase_increment == 0) {
phase = 0;
audio_value *= 0.9;
}
else {
phase += phase_increment;
if (phase > 0) { phase -= 2 * M_PI; }
audio_value = sin(phase) * volume;
}
buffer[i + 0] = audio_value; // Left channel
buffer[i + 1] = audio_value; // Right channel
}
}
__stdcall DWORD audio_thread(LPVOID param) {
while (1) {
DWORD waitResult = WaitForSingleObject(waveEvent, INFINITE);
if (waitResult) {
fprintf(stderr, "Failed to wait for event.\n");
return 1;
}
BOOL success = ResetEvent(waveEvent);
if (!success) {
fprintf(stderr, "Failed to reset event.\n");
return 1;
}
while (currentHeader->dwFlags & WHDR_DONE) {
fill_buffer((int16_t *)currentHeader->lpData);
MMRESULT result = waveOutWrite(waveOut, currentHeader, sizeof(WAVEHDR));
if (result) {
fprintf(stderr, "Failed to write wave data. Error code %u.\n", result);
return 1;
}
currentHeader++;
if (currentHeader == headers + BUFFER_COUNT) { currentHeader = headers; }
}
}
}
int audio_init() {
WAVEFORMATEX format = { 0 };
format.wFormatTag = WAVE_FORMAT_PCM;
format.nChannels = 2;
format.nSamplesPerSec = SAMPLES_PER_SECOND;
format.wBitsPerSample = 16;
format.nBlockAlign = format.nChannels * format.wBitsPerSample / 8;
format.nAvgBytesPerSec = format.nBlockAlign * format.nSamplesPerSec;
waveEvent = CreateEvent(NULL, true, false, NULL);
if (waveEvent == NULL) {
fprintf(stderr, "Failed to create event.");
return 1;
}
MMRESULT result = waveOutOpen(&waveOut, WAVE_MAPPER, &format,
(DWORD_PTR)waveEvent, 0, CALLBACK_EVENT);
if (result) {
fprintf(stderr, "Failed to start audio output. Error code %u.\n", result);
return 1;
}
for (size_t i = 0; i < BUFFER_COUNT; i++) {
headers[i] = (WAVEHDR) {
.lpData = (char *)buffers[i],
.dwBufferLength = BUFFER_SAMPLE_COUNT * 4,
};
result = waveOutPrepareHeader(waveOut, &headers[i], sizeof(WAVEHDR));
if (result) {
fprintf(stderr, "Failed to prepare header. Error code %u.\n", result);
return 1;
}
headers[i].dwFlags |= WHDR_DONE;
}
currentHeader = headers;
HANDLE thread = CreateThread(NULL, 0, audio_thread, NULL, 0, NULL);
if (thread == NULL) {
fprintf(stderr, "Failed to start thread");
return 1;
}
return 0;
}
int main() {
if (audio_init()) { return 1; }
sound(400); Sleep(500);
sound(300); Sleep(500);
sound(600); Sleep(500);
for (int i = 0; i < 3; i++) {
sound(200); Sleep(50);
sound(0); Sleep(200);
sound(300); Sleep(50);
sound(0); Sleep(200);
sound(400); Sleep(50);
sound(0); Sleep(200);
sound(500); Sleep(50);
sound(0); Sleep(200);
sound(600); Sleep(50);
sound(0); Sleep(200);
}
}
I compiled in the MinGW 64-bit environment of MSYS2 using this command:
gcc -g -O2 -Wall -Wextra -Wno-unused-parameter wave.c -lwinmm -owave
Note the extra library I linked in: -lwinmm
.
This code uses 4 buffers with 1000 samples each, so at any given time there will be no more than 83 ms worth of sound queued up to be played. This works fine on my system, but perhaps on other systems you might need to increase these parameters to ensure smooth playback. The buffer count is probably the first one I would try increasing if there are problems.
There might be some multithreading things I am getting wrong in the code. The phase_increment
variable is written in the main thread and read in the audio thread. Perhaps I should explicitly make that be an atomic variable, and insert a memory barrier after I write to it.