1

Does the WaveOut API has some internal limitation of the size for the current piece of buffer played ? I mean if I provide a very small buffer does it affects somehow the sound played to the speakers. I am experiencing very strange noise when I am generating and playing the sinus wave with small buffer. Something like a peak, or "BUMP".

The complete Story:

I made a program that can generate Sinus sound signal in real time. The variable parameters are Frequency and Volume. The project requirement was to have a maximum latency of 50 ms. So the program must be able to produce Sinus signals with manually adjustable frequency of audio signal in real time.

I used Windows WaveOut API, C# and P/invoke to access the API.

Everything works fine when the sound buffer is 1000 ms large. If I minimize the buffer to 50 ms as per latency requirement then for certain frequencies I am experiencing at the end of every buffer, a noise or "BUMP". I do not understand if the sound generated is malformed ( I checked and is not) or something happens with the Audio chip, or some delay in initializing and playing.

When I save the produced audio to .wav file everything is perfect.

This means the must be some bug in my code or the audio subsystem has a limitation to the buffer chunks sent to it.

For those who doesn't know WaveOut must be initialized at first time and then must be prepared with audio headers for each buffer that are containing the number of bytes that needs to be played and the pointer to a memory that contains the audio that needs to be player.

UPDATE

Noise happens with the following combinations 44100 SamplingRate, 16 Bits, 2 channels, 50 ms buffer and generated Sinus audio signal of 201Hz, 202Hz, 203Hz, 204Hz, 205Hz ... 219Hz, 220Hz, 240 Hz, is ok

Why is this difference of 20, I do not know.

Patrik
  • 1,286
  • 1
  • 31
  • 64

2 Answers2

5

There are a few things to keep in mind when you need to output audio smoothly:

  • waveOutXxxx API is a legacy/compatibility layer on top of lower level API and as such it has greater overhead and is not recommended when you are to reach minimal latency. Note that this is unlikely to be your primary problem, but this is a piece of general knowledge helpful for understanding
  • because Windows is not real time OS and its audio subsystem is not realtime either you don't have control over random latency involved between you queue audio data for output and the data is really played back, the key is to keep certain level of buffer fullness which protects you from playback underflows and delivers smooth playback
  • with waveOutXxxx you are no limited to having single buffer, you can allocate multiple reusable buffers and recycle them

All in all, waveOutXxxx, DirectSound, DirectShow APIs work well with latencies 50 ms and up. With WASAPI exclusive mode streams you can get 5 ms latencies and even lower.

EDIT: I seem to have said too early about 20 ms latencies. To compensate for this, here is a simple tool LowLatencyWaveOutPlay (Win32, x64) to estimate the latency you can achieve. With sufficient buffering playback is smooth, otherwise you hear stuttering.

My understanding is that buffers might be returned late and the optimal design in terms of smallest latency lies along the line of having more smaller buffers so that you are given them back as early as possible. For example, 10 buffers 3 ms/buffer rather than 3 buffers 10 ms/buffer.

D:\>LowLatencyWaveOutPlay.exe 48000 10 3
Format: 48000 Hz, 1 channels, 16 bits per sample
Buffer Count: 10
Buffer Length: 3 ms (288 bytes)
Signal Frequency: 1000 Hz
^C
Community
  • 1
  • 1
Roman R.
  • 68,205
  • 6
  • 94
  • 158
  • Thanks Roman. I couldn't explain better but I already have two buffers and they work perfectly. My software has at three main threads. One is Producer, Consumer and the Player thread. Everything that you said is correct but my final goal is to run the program on Windows CE that is real-time OS (at least is said as is). I am using .NET Compact Framework 3.5 on the Windows CE. Firstly I wanted to make a working solution on Windows 7 and then to port it to a Window CE 6.0 R3. To achieve this I need to use libraries supported like WaveOut API. – Patrik Jan 12 '13 at 13:52
  • In addition. I have a buffer that is filled by the producer in one thread. The second buffer created by the consumer to be used by the player thread. The player is nothing more than a wrapper to WaveOut API. WaveOut wrapper works in a thread and takes one by one smaller buffer from a list. So at that level never happens to run out of buffers. When I am saving the buffers to wav file the waver form is perfect. So I think that the problem is somewhere deeper. – Patrik Jan 12 '13 at 13:59
  • BTW. I tried NAudio and I saw that it is using WaveOut too. I tried to initilize it with a small buffer size and the results are even worse. (When I am saying a small buffer it is actuall small buffer x number of chunks) – Patrik Jan 12 '13 at 14:06
  • It's hard to tell about Win CE (you did not mention this on the start), and what I wrote above was about desktop Windows. The primary reason for distortions with small buffers that I would suspect are that by the time filled buffer travels through layers to lowest level driver, it is already too late to play the data. This is where buffer increase makes things better. It's hard to tell whether CE itself, or .NET Compact applies the overhead which is too large. On a regular desktop Windows 50 ms length is completely out of question for being too short for smooth playback. – Roman R. Jan 12 '13 at 14:24
  • Maybe you'd want to start with simple app that generates two buffers (e.g. pure sine wave) to play and you will decrease buffer length until it does not play smooth. And you will see how small can it be. To make sure it's not your threading that lets you down. – Roman R. Jan 12 '13 at 14:29
  • It's interesting situation. Everything plays correctly on and sinus frequency with the buffer of 1000 ms. With the buffer less then 1000 ms the problems are coming out. With 50 ms I can play only frequencies 200Hz, 220, 240, ... 2000 but nothing in the middle. I think that must be two problems. Once is initialization time of the WaveOut api before playing and the other the rounding of the values of the Sinus wave. The fact is that with about freq. 200, 220, ... with 50 ms buffer and 44.1Khz of sampling rate full sinus waves can fit into the buffer for the other freq. is not this case. – Patrik Jan 12 '13 at 15:06
  • @Patrik: Have a look at my edit and the code/app to see effect of various buffer configuration combinations. – Roman R. Jan 12 '13 at 17:18
  • Thanks Roman for the code. It works on my side. But I do not know how it will work in .NET environment with the requirement of real-time changing of frequency. I need to check. Maybe works without too much work. – Patrik Jan 12 '13 at 18:42
  • It does not depend on frequency of the signal. It is only sampling rate that *might* matter (more or less bytes per second), which cannot change in real time. Because the main problem seems to be with buffer restocking delay, the effective latency does not seem to correlate with sampling rate either. It can certainly be different in CE. – Roman R. Jan 12 '13 at 18:50
  • I am trying to make it work on Win CE (ARM) but with the same code I am getting MMSYSERR_INVALIDPARAM in the line for buffer preparation. Win32.MMRESULT hr = Win32.waveOutPrepareHeader(hWaveOut, ref WaveOutHeaders[i], Marshal.SizeOf(WaveOutHeaders[i])); if (hr == Win32.MMRESULT.MMSYSERR_NOERROR) ... I am trying to find some solution or explanation but nothing. – Patrik Jan 12 '13 at 18:58
  • 1
    from what you said about the frequencies, it may be that you are not maintaining phase in your sine wave generator between filling buffers, which will result in a discontinuity. – Mark Heath Jan 13 '13 at 08:06
  • Hello Roman R. Try to call your demo an you will hear what is the problem. LowLatencyWaveOutPlay 44100 8 2 , the same I am hearing with my program with 44100 Sample/s 16bit 205Hz of sinus and 50 ms buffer. Try it and you will hear the noise. Something musr be related to buffers, sampling rate, threads and context switching. – Patrik Jan 14 '13 at 21:27
  • You can even try with 1 ms buffer. But I do not need such small buffer. You can try to change your demo to run 201,202,204,205 Hz of sinus with 50 ms buffer. You will hear exactly the same as I hear. There must be something deeper in the driver, the audio chip, DAC or something else. – Patrik Jan 14 '13 at 21:36
  • As Mark suggested above, you might be generating wrong sine (phase issues at buffer boundaries). I added another command parameter and 205 Hz plays perfectly: `LowLatencyWaveOutPlay.exe 44100 2 50 205`. With 8x 2 ms buffers you only have 16 ms (actually 8 because it's "-1 buffer" at the very least) head time for the internals legacy compatibility layer and mixing, it's just insufficient - hence distortions. It's another story. – Roman R. Jan 14 '13 at 21:40
  • I can accept wrong sine generation at buffer boundaries but why then works good only for some frequencies. The difference is only in one number. Anyway thank you very much for helping me I appreciate very much. I will wait a little to accept the solution. I need to make few more tests. – Patrik Jan 14 '13 at 22:22
1

So I came here because I wanted to find the basic latency of waveoutwrite() as well. I got around 25-26ms of latency before I got to the smooth sine tone.

This is for:

AMD Phenom(tm) 9850 Quad-Core Processor 2.51 GHz 4.00 GB ram 64-bit operating system, x64-based processor Windows 10 Enterprise N

The code follows. It is a modfied version of Petzold's sine wave program, refactored to run on the command line. I also changed the polling of buffers to use of a callback on buffer complete with the idea that this would make the program more efficient, but it didn't make a difference.

It also has a setup for elapsed timing, which I used to probe various timings for operations on the buffers. Using those I get:

Sine wave output program
Channels:         2
Sample rate:      44100
Bytes per second: 176400
Block align:      4
Bits per sample:  16
Time per buffer:  0.025850
Total time prepare header:   87.5000000000 usec
Total time to fill:          327.9000000000 usec
Total time for waveOutWrite: 90.8000000000 usec

Program:

/*******************************************************************************

WaveOut example program

Based on C. Petzold's sine wave example, outputs a sine wave via the waveOut
API in Win32.

*******************************************************************************/

#include <stdio.h>
#include <windows.h>
#include <math.h>
#include <limits.h>
#include <unistd.h>

#define SAMPLE_RATE      44100
#define FREQ_INIT        440
#define OUT_BUFFER_SIZE  570*4
#define PI               3.14159
#define CHANNELS         2
#define BITS             16
#define MAXTIM           1000000000

double        fAngle;
LARGE_INTEGER perffreq;
PWAVEHDR      pWaveHdr1, pWaveHdr2;
int           iFreq = FREQ_INIT;

VOID FillBuffer (short* pBuffer, int iFreq)

{

     int i;
     int c;

     for (i = 0 ; i < OUT_BUFFER_SIZE ; i += CHANNELS) {

          for (c = 0; c < CHANNELS; c++)
            pBuffer[i+c] = (short)(SHRT_MAX*sin (fAngle));
          fAngle += 2*PI*iFreq/SAMPLE_RATE;
          if (fAngle > 2 * PI) fAngle -= 2*PI;

     }

}

double elapsed(LARGE_INTEGER t)

{

    LARGE_INTEGER rt;
    long tt;

    QueryPerformanceCounter(&rt);
    tt = rt.QuadPart-t.QuadPart;

    return (tt*(1.0/(double)perffreq.QuadPart));

}

void CALLBACK waveOutProc(HWAVEOUT hwo, UINT uMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2)

{

    if (uMsg == WOM_DONE) {

        if (pWaveHdr1->dwFlags & WHDR_DONE) {

            FillBuffer((short*)pWaveHdr1->lpData, iFreq);
            waveOutWrite(hwo, pWaveHdr1, sizeof(WAVEHDR));

        }
        if (pWaveHdr2->dwFlags & WHDR_DONE) {

            FillBuffer((short*)pWaveHdr2->lpData, iFreq);
            waveOutWrite(hwo, pWaveHdr2, sizeof(WAVEHDR));

        }

    }

}

int main()

{

    HWAVEOUT     hWaveOut ;

    short*       pBuffer1;
    short*       pBuffer2;
    short*       pBuffer3;

    WAVEFORMATEX waveformat;
    UINT         wReturn;
    int          bytes;
    long         t;
    LARGE_INTEGER rt;
    double       timprep;
    double       filtim;
    double       waveouttim;

    printf("Sine wave output program\n");

    fAngle = 0; /* start sine angle */

    QueryPerformanceFrequency(&perffreq);

    pWaveHdr1 = malloc (sizeof (WAVEHDR));
    pWaveHdr2 = malloc (sizeof (WAVEHDR));
    pBuffer1  = malloc (OUT_BUFFER_SIZE*sizeof(short));
    pBuffer2  = malloc (OUT_BUFFER_SIZE*sizeof(short));
    pBuffer3  = malloc (OUT_BUFFER_SIZE*sizeof(short));

    if (!pWaveHdr1 || !pWaveHdr2 || !pBuffer1 || !pBuffer2) {

        if (!pWaveHdr1) free (pWaveHdr1) ;
        if (!pWaveHdr2) free (pWaveHdr2) ;
        if (!pBuffer1)  free (pBuffer1) ;
        if (!pBuffer2)  free (pBuffer2) ;

        fprintf(stderr, "*** Error: No memory\n");
        exit(1);

    }

    // Load prime parameters to format
    waveformat.wFormatTag      = WAVE_FORMAT_PCM;
    waveformat.nChannels       = CHANNELS;
    waveformat.nSamplesPerSec  = SAMPLE_RATE;
    waveformat.wBitsPerSample  = BITS;
    waveformat.cbSize          = 0;

    // Calculate other parameters
    bytes = waveformat.wBitsPerSample/8; /* find bytes per sample */
    if (waveformat.wBitsPerSample&8) bytes++; /* round  up */
    bytes *= waveformat.nChannels; /* find total channels size */
    waveformat.nBlockAlign = bytes; /* set block align */
    /* find average bytes/sec */
    waveformat.nAvgBytesPerSec = bytes*waveformat.nSamplesPerSec;

    printf("Channels:         %d\n", waveformat.nChannels);
    printf("Sample rate:      %d\n", waveformat.nSamplesPerSec);
    printf("Bytes per second: %d\n", waveformat.nAvgBytesPerSec);
    printf("Block align:      %d\n", waveformat.nBlockAlign);
    printf("Bits per sample:  %d\n", waveformat.wBitsPerSample);
    printf("Time per buffer:  %f\n",
        OUT_BUFFER_SIZE*sizeof(short)/(double)waveformat.nAvgBytesPerSec);

    if (waveOutOpen (&hWaveOut, WAVE_MAPPER, &waveformat, (DWORD_PTR)waveOutProc, 0, CALLBACK_FUNCTION)
        != MMSYSERR_NOERROR) {

        free (pWaveHdr1) ;
        free (pWaveHdr2) ;
        free (pBuffer1) ;
        free (pBuffer2) ;

        hWaveOut = NULL ;
        fprintf(stderr, "*** Error: No memory\n");
        exit(1);

    }

    // Set up headers and prepare them

    pWaveHdr1->lpData          = (LPSTR)pBuffer1;
    pWaveHdr1->dwBufferLength  = OUT_BUFFER_SIZE*sizeof(short);
    pWaveHdr1->dwBytesRecorded = 0;
    pWaveHdr1->dwUser          = 0;
    pWaveHdr1->dwFlags         = WHDR_DONE;
    pWaveHdr1->dwLoops         = 1;
    pWaveHdr1->lpNext          = NULL;
    pWaveHdr1->reserved        = 0;

QueryPerformanceCounter(&rt);
    waveOutPrepareHeader(hWaveOut, pWaveHdr1, sizeof (WAVEHDR));
timprep = elapsed(rt);

    pWaveHdr2->lpData          = (LPSTR)pBuffer2;
    pWaveHdr2->dwBufferLength  = OUT_BUFFER_SIZE*sizeof(short);
    pWaveHdr2->dwBytesRecorded = 0;
    pWaveHdr2->dwUser          = 0;
    pWaveHdr2->dwFlags         = WHDR_DONE;
    pWaveHdr2->dwLoops         = 1;
    pWaveHdr2->lpNext          = NULL;
    pWaveHdr2->reserved        = 0;

    waveOutPrepareHeader(hWaveOut, pWaveHdr2, sizeof (WAVEHDR));

    // Send two buffers to waveform output device

QueryPerformanceCounter(&rt);
    FillBuffer (pBuffer1, iFreq);
filtim = elapsed(rt);

QueryPerformanceCounter(&rt);
    waveOutWrite (hWaveOut, pWaveHdr1, sizeof (WAVEHDR));
waveouttim = elapsed(rt);

    FillBuffer (pBuffer2, iFreq);
    waveOutWrite (hWaveOut, pWaveHdr2, sizeof (WAVEHDR));

    // Run waveform loop
    sleep(10);

printf("Total time prepare header:   %.10f usec\n", timprep*1000000);
printf("Total time to fill:          %.10f usec\n", filtim*1000000);
printf("Total time for waveOutWrite: %.10f usec\n", waveouttim*1000000);

    waveOutUnprepareHeader(hWaveOut, pWaveHdr1, sizeof (WAVEHDR));
    waveOutUnprepareHeader(hWaveOut, pWaveHdr2, sizeof (WAVEHDR));
    // Close waveform file
    free (pWaveHdr1) ;
    free (pWaveHdr2) ;
    free (pBuffer1) ;
    free (pBuffer2) ;

}
Scott Franco
  • 481
  • 2
  • 15