5

[The mystery has been solved; for those looking for an explanation, it is at the bottom of this post]

Below is a Windows tone generator I am trying to write using Windows waveOut*() functions.

Despite doing literally everything according to MSDN (e.g. callback events that should be reset manually), I cannot get smooth square-wave playback from the damn thing — any smooth playback, actually, but for the sake of simplicity I demonstrate squares. Buffer borders always greet me with clicks! Looks like Windows just ignores the fact that I use double buffering.

The generator itself is independent of the buffer size, and if I take a larger buffer the seamless playback continues for a longer period of time — but when the buffer finally ends there is a click.

Help.

#define _WIN32_IE 0x0500
#define _WIN32_WINNT 0x0501
#define WINVER _WIN32_WINNT

#include <windows.h>
#include <mmsystem.h>
#include <commctrl.h>

#include <stdint.h>
#include <stdio.h>
#include <math.h>



short freq, ampl;



typedef struct {
    long chnl, smpl, bits, size, swiz;
    void *sink, *data[2];
} WAVE;



LRESULT APIENTRY WndProc(HWND hWnd, UINT uMsg, WPARAM wPrm, LPARAM lPrm) {
    switch (uMsg) {
        case WM_DESTROY:
            PostQuitMessage(0);
            return 0;

        case WM_NOTIFY:
            switch (((NMHDR*)lPrm)->idFrom) {
                case 2:
                    freq = ((NMUPDOWN*)lPrm)->iPos;
                    break;

                case 4:
                    ampl = ((NMUPDOWN*)lPrm)->iPos;
                    break;
            }
            return 0;

        default:
            break;
    }
    return DefWindowProc(hWnd, uMsg, wPrm, lPrm);
}



void FillBuf(WAVE *wave, short freq, short ampl, long *phaz) {
    int16_t *data = wave->data[wave->swiz ^= 1];
    float tone = 1.0 * freq / wave->smpl;
    long iter;

    for(iter = 0; iter < wave->size; iter++)
        data[iter] = ((long)(tone * (iter + *phaz)) & 1)? ampl : -ampl;

    *phaz = *phaz + iter;//2.0 * frac(0.5 * tone * (iter + *phaz)) / tone;
}



DWORD APIENTRY WaveFunc(LPVOID data) {
    WAVEHDR *whdr;
    WAVE *wave;
    intptr_t *sink;
    long size, phaz = 0;

    wave = (WAVE*)data;
    whdr = (WAVEHDR*)(sink = wave->sink)[1];
    size = wave->chnl * wave->size * (wave->bits >> 3);
    wave->data[0] = calloc(1, size);
    wave->data[1] = calloc(1, size);
    do {
        waveOutUnprepareHeader((HWAVEOUT)sink[0], whdr, sizeof(WAVEHDR));
        whdr->dwBufferLength = size;
        whdr->dwFlags = 0;
        whdr->dwLoops = 0;
        whdr->lpData = (LPSTR)wave->data[wave->swiz];
        waveOutPrepareHeader((HWAVEOUT)sink[0], whdr, sizeof(WAVEHDR));
        ResetEvent((HANDLE)sink[2]);
        waveOutWrite((HWAVEOUT)sink[0], whdr, sizeof(WAVEHDR));
        FillBuf(wave, freq, ampl, &phaz);
    } while (!WaitForSingleObject((HANDLE)sink[2], INFINITE));
    return 0;
}



int APIENTRY WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR args, int show) {
    WNDCLASSEX wndc = {sizeof(wndc), CS_HREDRAW | CS_VREDRAW, WndProc, 0, 0,
                       inst, LoadIcon(0, IDI_HAND), LoadCursor(0, IDC_ARROW),
                       (HBRUSH)(COLOR_BTNFACE + 1), 0, "-", 0};
    INITCOMMONCONTROLSEX icct = {sizeof(icct), ICC_STANDARD_CLASSES};
    MSG pmsg;

    HWND mwnd, cwnd, spin;
    DWORD thrd;
    WAVEFORMATEX wfmt;
    intptr_t data[3];
    WAVE wave = {1, 44100, 16, 4096, 0, data};

//    AllocConsole();
//    freopen("CONOUT$", "wb", stdout);

    InitCommonControlsEx(&icct);
    RegisterClassEx(&wndc);
    mwnd = CreateWindowEx(0, wndc.lpszClassName, " ",
                          WS_OVERLAPPEDWINDOW | WS_VISIBLE,
                          CW_USEDEFAULT, CW_USEDEFAULT, 320, 240,
                          HWND_DESKTOP, 0, wndc.hInstance, 0);


    cwnd = CreateWindowEx(WS_EX_CLIENTEDGE, WC_EDIT, 0, ES_AUTOHSCROLL
                        | ES_WANTRETURN | ES_MULTILINE | ES_NUMBER | WS_CHILD
                        | WS_VISIBLE, 10, 10, 100, 24, mwnd, (HMENU)1, 0, 0);
    SendMessage(cwnd, EM_LIMITTEXT, 9, 0);
    spin = CreateWindowEx(0, UPDOWN_CLASS, 0, UDS_HOTTRACK | UDS_NOTHOUSANDS
                        | UDS_ALIGNRIGHT | UDS_SETBUDDYINT | UDS_ARROWKEYS
                        | WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, mwnd, (HMENU)2, 0, 0);
    SendMessage(spin, UDM_SETBUDDY, (WPARAM)cwnd, 0);
    SendMessage(spin, UDM_SETRANGE32, (WPARAM)20, (LPARAM)22050);
    SendMessage(spin, UDM_SETPOS32, 0, (LPARAM)(freq = 400));


    cwnd = CreateWindowEx(WS_EX_CLIENTEDGE, WC_EDIT, 0, ES_AUTOHSCROLL
                        | ES_WANTRETURN | ES_MULTILINE | ES_NUMBER | WS_CHILD
                        | WS_VISIBLE, 10, 44, 100, 24, mwnd, (HMENU)3, 0, 0);
    SendMessage(cwnd, EM_LIMITTEXT, 9, 0);
    spin = CreateWindowEx(0, UPDOWN_CLASS, 0, UDS_HOTTRACK | UDS_NOTHOUSANDS
                        | UDS_ALIGNRIGHT | UDS_SETBUDDYINT | UDS_ARROWKEYS
                        | WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, mwnd, (HMENU)4, 0, 0);
    SendMessage(spin, UDM_SETBUDDY, (WPARAM)cwnd, 0);
    SendMessage(spin, UDM_SETRANGE32, (WPARAM)0, (LPARAM)32767);
    SendMessage(spin, UDM_SETPOS32, 0, (LPARAM)(ampl = 32767));


    wfmt = (WAVEFORMATEX){WAVE_FORMAT_PCM, wave.chnl, wave.smpl,
                         ((wave.chnl * wave.bits) >> 3) * wave.smpl,
                          (wave.chnl * wave.bits) >> 3, wave.bits};
    data[1] = (intptr_t)calloc(1, sizeof(WAVEHDR));
    waveOutOpen((LPHWAVEOUT)&data[0], WAVE_MAPPER, &wfmt,
                 data[2] = (intptr_t)CreateEvent(0, 1, 0, 0), 0,
                 CALLBACK_EVENT);
    SetThreadPriority(CreateThread(0, 0, WaveFunc, &wave, 0, &thrd),
                      THREAD_PRIORITY_TIME_CRITICAL);

    while (pmsg.message != WM_QUIT) {
        if (PeekMessage(&pmsg, 0, 0, 0, PM_REMOVE)) {
            TranslateMessage(&pmsg);
            DispatchMessage(&pmsg);
            continue;
        }
        Sleep(1);
    }
    waveOutClose((HWAVEOUT)data[0]);
    fclose(stdout);
    FreeConsole();

    exit(pmsg.wParam);
    return 0;
}

[UPDATE:]

Duplicated the header as I`ve been told, to no avail:

#define _WIN32_IE 0x0500
#define _WIN32_WINNT 0x0501
#define WINVER _WIN32_WINNT

#include <windows.h>
#include <mmsystem.h>
#include <commctrl.h>

#include <stdint.h>
#include <stdio.h>
#include <math.h>



short freq, ampl;



typedef struct {
    long chnl, smpl, bits, size, swiz;
    void *sink, *data[2];
} WAVE;



LRESULT APIENTRY WndProc(HWND hWnd, UINT uMsg, WPARAM wPrm, LPARAM lPrm) {
    switch (uMsg) {
        case WM_DESTROY:
            PostQuitMessage(0);
            return 0;

        case WM_NOTIFY:
            switch (((NMHDR*)lPrm)->idFrom) {
                case 2:
                    freq = ((NMUPDOWN*)lPrm)->iPos;
                    break;

                case 4:
                    ampl = ((NMUPDOWN*)lPrm)->iPos;
                    break;
            }
            return 0;

        default:
            break;
    }
    return DefWindowProc(hWnd, uMsg, wPrm, lPrm);
}



void FillBuf(WAVE *wave, short freq, short ampl, long *phaz) {
    int16_t *data = wave->data[wave->swiz ^= 1];
    float tone = 1.0 * freq / wave->smpl;
    long iter;

    for(iter = 0; iter < wave->size; iter++)
        data[iter] = ((long)(tone * (iter + *phaz)) & 1)? ampl : -ampl;

    *phaz = *phaz + iter;//2.0 * frac(0.5 * tone * (iter + *phaz)) / tone;
}



DWORD APIENTRY WaveFunc(LPVOID data) {
    WAVEHDR *whdr;
    WAVE *wave;
    intptr_t *sink;
    long size, phaz = 0;

    wave = (WAVE*)data;
    whdr = (WAVEHDR*)(sink = wave->sink)[1];
    size = wave->chnl * wave->size * (wave->bits >> 3);

    whdr[0].dwBufferLength = whdr[1].dwBufferLength = size;
    whdr[0].dwFlags        = whdr[1].dwFlags        = 0;
    whdr[0].dwLoops        = whdr[1].dwLoops        = 0;
    whdr[0].lpData = (LPSTR)(wave->data[0] = calloc(1, size));
    whdr[1].lpData = (LPSTR)(wave->data[1] = calloc(1, size));

    do {
        waveOutUnprepareHeader((HWAVEOUT)sink[0], &whdr[wave->swiz], sizeof(WAVEHDR));
        waveOutPrepareHeader((HWAVEOUT)sink[0], &whdr[wave->swiz], sizeof(WAVEHDR));
        ResetEvent((HANDLE)sink[2]);
        waveOutWrite((HWAVEOUT)sink[0], &whdr[wave->swiz], sizeof(WAVEHDR));
        FillBuf(wave, freq, ampl, &phaz);
    } while (!WaitForSingleObject((HANDLE)sink[2], INFINITE));
    return 0;
}



int APIENTRY WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR args, int show) {
    WNDCLASSEX wndc = {sizeof(wndc), CS_HREDRAW | CS_VREDRAW, WndProc, 0, 0,
                       inst, LoadIcon(0, IDI_HAND), LoadCursor(0, IDC_ARROW),
                       (HBRUSH)(COLOR_BTNFACE + 1), 0, "-", 0};
    INITCOMMONCONTROLSEX icct = {sizeof(icct), ICC_STANDARD_CLASSES};
    MSG pmsg;

    HWND mwnd, cwnd, spin;
    DWORD thrd;
    WAVEFORMATEX wfmt;
    intptr_t sink[3];
    WAVE wave = {1, 44100, 16, 4096, 0, sink};

//    AllocConsole();
//    freopen("CONOUT$", "wb", stdout);

    InitCommonControlsEx(&icct);
    RegisterClassEx(&wndc);
    mwnd = CreateWindowEx(0, wndc.lpszClassName, " ",
                          WS_OVERLAPPEDWINDOW | WS_VISIBLE,
                          CW_USEDEFAULT, CW_USEDEFAULT, 320, 240,
                          HWND_DESKTOP, 0, wndc.hInstance, 0);


    cwnd = CreateWindowEx(WS_EX_CLIENTEDGE, WC_EDIT, 0, ES_AUTOHSCROLL
                        | ES_WANTRETURN | ES_MULTILINE | ES_NUMBER | WS_CHILD
                        | WS_VISIBLE, 10, 10, 100, 24, mwnd, (HMENU)1, 0, 0);
    SendMessage(cwnd, EM_LIMITTEXT, 9, 0);
    spin = CreateWindowEx(0, UPDOWN_CLASS, 0, UDS_HOTTRACK | UDS_NOTHOUSANDS
                        | UDS_ALIGNRIGHT | UDS_SETBUDDYINT | UDS_ARROWKEYS
                        | WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, mwnd, (HMENU)2, 0, 0);
    SendMessage(spin, UDM_SETBUDDY, (WPARAM)cwnd, 0);
    SendMessage(spin, UDM_SETRANGE32, (WPARAM)20, (LPARAM)22050);
    SendMessage(spin, UDM_SETPOS32, 0, (LPARAM)(freq = 400));


    cwnd = CreateWindowEx(WS_EX_CLIENTEDGE, WC_EDIT, 0, ES_AUTOHSCROLL
                        | ES_WANTRETURN | ES_MULTILINE | ES_NUMBER | WS_CHILD
                        | WS_VISIBLE, 10, 44, 100, 24, mwnd, (HMENU)3, 0, 0);
    SendMessage(cwnd, EM_LIMITTEXT, 9, 0);
    spin = CreateWindowEx(0, UPDOWN_CLASS, 0, UDS_HOTTRACK | UDS_NOTHOUSANDS
                        | UDS_ALIGNRIGHT | UDS_SETBUDDYINT | UDS_ARROWKEYS
                        | WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, mwnd, (HMENU)4, 0, 0);
    SendMessage(spin, UDM_SETBUDDY, (WPARAM)cwnd, 0);
    SendMessage(spin, UDM_SETRANGE32, (WPARAM)0, (LPARAM)32767);
    SendMessage(spin, UDM_SETPOS32, 0, (LPARAM)(ampl = 32767));


    wfmt = (WAVEFORMATEX){WAVE_FORMAT_PCM, wave.chnl, wave.smpl,
                         ((wave.chnl * wave.bits) >> 3) * wave.smpl,
                          (wave.chnl * wave.bits) >> 3, wave.bits};
    sink[1] = (intptr_t)calloc(2, sizeof(WAVEHDR));
    waveOutOpen((LPHWAVEOUT)&sink[0], WAVE_MAPPER, &wfmt,
                 sink[2] = (intptr_t)CreateEvent(0, 1, 0, 0), 0,
                 CALLBACK_EVENT);
    SetThreadPriority(CreateThread(0, 0, WaveFunc, &wave, 0, &thrd),
                      THREAD_PRIORITY_TIME_CRITICAL);

    while (pmsg.message != WM_QUIT) {
        if (PeekMessage(&pmsg, 0, 0, 0, PM_REMOVE)) {
            TranslateMessage(&pmsg);
            DispatchMessage(&pmsg);
            continue;
        }
        Sleep(1);
    }
    waveOutClose((HWAVEOUT)sink[0]);
    fclose(stdout);
    FreeConsole();

    exit(pmsg.wParam);
    return 0;
}

[WHAT ACTUALLY HAPPENED:]

The playback has been stuttering due to the fact that Windows ran out of data the very moment I switched the buffers. To avoid that, you have to provide BOTH buffers to the system BEFORE the feedback loop begins, so that when one of the buffers is done playing there was the next one already prepared and sent, while you refill the one that just retired.

And may the lost souls (like me two days prior) finally find clarity here =)

Seriously, for the time being this is the sole page on the Internet where an actual working solution has been proposed, which doesn`t employ timers or whatever the kludge instead of the correct approach.

hidefromkgb
  • 5,834
  • 1
  • 13
  • 44
  • 2
    The phrase "double-buffering" in the MSDN docs is very misleading. What it is trying to say is that you need more than one buffer so the driver won't run out of data. The event that is signaled merely means "something happened to the buffers". You are then supposed to inspect the dwFlags of your buffers and detect WHDR_DONE. Rejigger the code so you start off with 2 waveOutWrite() calls, that way there is always at least one buffer that the driver can play back. – Hans Passant Apr 05 '18 at 13:52
  • @HansPassant Finally! The missing link I\`ve been looking for! It works now, thank you so much! – hidefromkgb Apr 05 '18 at 15:23
  • Yay, congratulations. – Hans Passant Apr 05 '18 at 15:25
  • 1
    Huh, just stumbled of your question )) What I don't like when use this site is they usually set as the answer the one that is not the answer )) Actually, Double Buffering must use two threads and one same AutoReset Event handle. Each thread will compete to writeOut with own header and data. And I suggest you to use a one common Queue with data and Lock (Muttex) when access to it. – Vitali Petrov Dec 06 '19 at 11:46

3 Answers3

5

While the code is mostly okay (in terms of functionality but not readability and clarity), the thread function is not good.

You are supposed to fill while unprepared and route for playback afterwards.

Here you go (also the thread does not need to have a priority above normal):

DWORD APIENTRY WaveFunc(LPVOID data) 
{
    WAVEHDR *whdr;
    WAVE *wave;
    intptr_t *sink;
    long size, phaz = 0;

    wave = (WAVE*)data;
    whdr = (WAVEHDR*)(sink = (intptr_t*) wave->sink)[1];
    size = wave->chnl * wave->size * (wave->bits >> 3);

    HWAVEOUT hWaveOut = (HWAVEOUT) sink[0];
    HANDLE hEvent = (HANDLE)sink[2];

    whdr[0].dwBufferLength = whdr[1].dwBufferLength = size;
    whdr[0].dwFlags        = whdr[1].dwFlags        = 0;
    whdr[0].dwLoops        = whdr[1].dwLoops        = 0;
    whdr[0].lpData = (LPSTR)(wave->data[0] = calloc(1, size));
    whdr[1].lpData = (LPSTR)(wave->data[1] = calloc(1, size));

    ResetEvent(hEvent);

    assert(wave->swiz == 0);
    FillBuf(wave, freq, ampl, &phaz);
    waveOutPrepareHeader(hWaveOut, &whdr[1], sizeof (WAVEHDR));
    waveOutWrite(hWaveOut, &whdr[1], sizeof (WAVEHDR));

    assert(wave->swiz == 1);
    FillBuf(wave, freq, ampl, &phaz);
    waveOutPrepareHeader(hWaveOut, &whdr[0], sizeof (WAVEHDR));
    waveOutWrite(hWaveOut, &whdr[0], sizeof (WAVEHDR));

    for(; ; )
    {
        WaitForSingleObject(hEvent, INFINITE);
        ResetEvent(hEvent);
        for(long index = 0; index < 2; index++)
            if(whdr[index].dwFlags & WHDR_DONE)
            {
                wave->swiz = index ^ 1;
                // NOTE: See comment from Paul Sanders: the headers have to be
                //       prepared before writing, however there is no need to
                //       re-prepare to upload new data
                //waveOutUnprepareHeader(hWaveOut, &whdr[wave->swiz], sizeof (WAVEHDR));
                FillBuf(wave, freq, ampl, &phaz);
                //waveOutPrepareHeader(hWaveOut, &whdr[wave->swiz], sizeof (WAVEHDR));
                waveOutWrite(hWaveOut, &whdr[wave->swiz], sizeof (WAVEHDR));
            }
    }
    return 0;
}
Roman R.
  • 68,205
  • 6
  • 94
  • 158
  • Woohoo, it`s working! Thank you. Bounty well earned. – hidefromkgb Apr 05 '18 at 15:40
  • Sorry, at the last moment I have decided to award the bounty to @PaulSanders, as he needs extra points more and also gave a useful answer, further explaining the right solution. I hope you don\`t despise me for breaking my promise given in the comment above. – hidefromkgb Apr 12 '18 at 22:57
  • Lol, I don't deserve it! BTW A good way to recycle your WAVEHDRs is in a callback function as you are passed the WAVEHDR that has completed and don't need to walk through however many you originally submitted to see which is / are done. Google `waveOutProc` for more. – Paul Sanders Apr 15 '18 at 17:58
  • Code with care though - you're very limited as to what you can do in there. – Paul Sanders Apr 15 '18 at 18:05
  • this is good, but you know, what if the wave data comes to you unstable and with pauses? so you have 10 packs and then while WOM_DONE has fired you have no incoming data, but it will come, later, because of some delay. – Vitali Petrov Dec 03 '19 at 00:44
  • @VitaliPetrov `1` This API plays data continuously until it plays out everything you supplied. If at some times you are late to supply new buffers, there will be a silence gap and then it resumes playback as new data appears. `2` Note that as of now this API is mostly a legacy code integration layer and converts calls into WASAPI (or close WASAPI internals) calls. That is, you should probably be interested in using WASAPI directly instead. – Roman R. Dec 03 '19 at 10:21
  • Also, you can't just write wave data two times without WOM_DONE as you shown in the example. It will overlap data. Anyway in the for;;; you'll stillbe waiting for WOMDATA and it will not scrab the sound – Vitali Petrov Dec 04 '19 at 08:25
  • @VitaliPetrov: as far as I remember the example worked, and the check for WOM_DONE is present in code above. – Roman R. Dec 04 '19 at 08:29
  • the first WOM_DONE will start making pauses in the resulting sound – Vitali Petrov Dec 04 '19 at 08:31
  • @VitaliPetrov: I just run the code and it works. I suppose you can witness this yourself too. If you want to improve it - go ahead, the original code snippet is not really perfect. – Roman R. Dec 04 '19 at 08:40
  • what you can do to make your code look closer to DBT is to move down WaitForSingleObject right before the second writeOutwave. Then you will get that effect.. because you'll fill 2 buffer while 1-st is on the worktable. – Vitali Petrov Dec 04 '19 at 11:25
  • @VitaliPetrov: It's a QA site. If you don't understand how it works, Help section here is brilliantly written. You would not ever write "shown in the example" "not double-buffering tech" if you ever took a trouble to realize what is going on here. – Roman R. Dec 04 '19 at 11:32
  • :) Don't teach me how to use a site, be consistent and follow your mistakes – Vitali Petrov Dec 06 '19 at 03:08
3

I don't have enough rep to comment so this has to be an answer, but I just wanted to add that you don't have to Unprepare and Reprepare the WAVEHDR each time, you can just re-use it.

Also, if you need to associate additional data with a WAVEHDR you can allocate a larger structure and tack it on the end - waveoutWrite won't care. This can be handy (for input buffers, mostly) if the buffer passes through some kind of processing chain before being reused. I use this trick when converting DSD to PCM.

Wave APIs rock!

Paul Sanders
  • 24,133
  • 4
  • 26
  • 48
1

I don't think you're doing double-buffering as intended. For one, I can only see that one WAVEHDR is instantiated.

In your setup, create 2 WAVEHDRs.

In your thread do the following (in pseudocode)

waveOutPrepareHeader(hdr[0]);
waveOutPrepareHeader(hdr[1]);
FillBuffer(hdr[0]->lpData);
FillBuffer(hdr[1]->lpData);
waveOutWrite(hdr[0]);
waveOutWrite(hdr[1]);
int nextBuf = 0;
while (!WaitForSingleObject(....)))
{
    waveOutUnprepareHeader(hdr[nextBuf]);
    waveOutPrepareHeader(hdr[nextBuf]);
    FillBuffer(hdr[nextBuf]);
    waveOutWrite(hdr[nextBuf]);
    nextBuf = (nextBuf+1) % 2;
}
jaket
  • 9,140
  • 2
  • 25
  • 44