0

I'm currently working on a (plain) C project, on Windows 7, and I need help.

The program is supposed to start playing a sound of given frequency when I press a key, and then stop it when I press another key. Once it starts playing, it shouldn't stop until the user presses the second key, so I can't specify a duration.

Is there a way to do it with WinApi? I'm searching for a function like:

sound(frequency);

that would start playing a sound at frequency frequency. If frequency is 0, then all sounds should stop.

I searched all over Stackoverflow, but I couldn't find any solution except Beep(); that needs a duration, and PlaySound(); that needs a wav file. I'd like to keep it simple. I don't want a big program that generates wav files, only a built-in function.

Ah, I did found such a function on Stack a few days ago, but I can't find it again.

(P.S: this question has no answer with WAV file synthesis from scratch. It's another question. So please do not tell me the close question has not been answered, because it's FALSE.)

  • 1
    Basically, create a WAV file in memory and then use the `PlaySound` API with the SND_MEMORY flag. – selbie May 18 '22 at 23:03
  • More details here: https://stackoverflow.com/questions/2302841/win32-playsound-how-to-control-the-volume/2303612#2303612 – selbie May 18 '22 at 23:04
  • 1
    This question is not a duplicate of [WAV File Synthesis From Scratch - C](https://stackoverflow.com/questions/10844122/wav-file-synthesis-from-scratch-c). That question is about how to create a WAV file. The question here does not concern the WAV format nor writing sound to a file. The question here is about how to generate sound and play it in real time on Microsoft Windows. Generating a WAV file is inappropriate for this question because we don't know how long the sound will need to be played. – David Grayson May 19 '22 at 16:22
  • Please take a look at my answer below and let me know if it answers your question :-) – David Grayson May 29 '22 at 01:28

1 Answers1

1

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.

David Grayson
  • 84,103
  • 24
  • 152
  • 189
  • Thanks for your answer! It answers my question in some way, but there's still a small issue, I was searching for a built-in function. But what matters is that it works so don't mind what I said. – Desktop Firework May 29 '22 at 11:11