/* Copyright (c) 2019 Alex Diener This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. Alex Diener alex@ludobloom.com */ #include "nativeaudio/AudioOut.h" #include "nativeaudio/AudioOut_private.h" #include #include #include #include #define BLOCK_FRAME_COUNT_44100 512 #define BLOCK_SIZE_MAX 4608 #define SAMPLE_RATE_MAX 96000 #define BLOCK_COUNT 12 #define BUFFER_AHEAD_COUNT 7 #define TRANSPORT_BUFFER_SIZE 4096 static bool initialized; static HWAVEOUT device; static HANDLE outputThread; static unsigned int nextFreeBlockIndex, lastPlayedBlockIndex; static WAVEHDR * blocks; static void * transportBuffer; static HANDLE outputThreadDoneSemaphore, outputThreadResumeSemaphore; void CALLBACK waveOutCallback(HWAVEOUT hwo, UINT uMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2) { if (uMsg == WOM_DONE) { lastPlayedBlockIndex = (lastPlayedBlockIndex + 1) % BLOCK_COUNT; } } static DWORD WINAPI outputThreadFunc(LPVOID lpParameter) { WaitForSingleObject(outputThreadResumeSemaphore, INFINITE); for (;;) { unsigned int blockFrameCount = BLOCK_FRAME_COUNT_44100 * g_AudioOut_hostFormat.sampleRate / 44100; unsigned int blockSize = blockFrameCount * g_AudioOut_hostFormat.bytesPerSample * g_AudioOut_hostFormat.channelCount; MMRESULT result; unsigned int blocksBufferedAheadOfPlayback; while (g_AudioOut_outputActive) { if (lastPlayedBlockIndex > nextFreeBlockIndex) { blocksBufferedAheadOfPlayback = (nextFreeBlockIndex + BLOCK_COUNT) - lastPlayedBlockIndex; } else { blocksBufferedAheadOfPlayback = nextFreeBlockIndex - lastPlayedBlockIndex; } while (blocksBufferedAheadOfPlayback < BUFFER_AHEAD_COUNT) { if (blocks[nextFreeBlockIndex].dwFlags & WHDR_PREPARED) { result = waveOutUnprepareHeader(device, &blocks[nextFreeBlockIndex], sizeof(*blocks)); #ifdef DEBUG if (result != MMSYSERR_NOERROR) { fprintf(stderr, "Error: waveOutUnprepareHeader returned %d\n", result); } #endif } blocks[nextFreeBlockIndex].dwBufferLength = blockSize; AudioOut_transferToOutput(blocks[nextFreeBlockIndex].lpData, g_AudioOut_outputCallback, g_AudioOut_outputContext, blockFrameCount, transportBuffer, TRANSPORT_BUFFER_SIZE, g_AudioOut_hostFormat, g_AudioOut_transportFormat, &g_AudioOut_resampleState); result = waveOutPrepareHeader(device, &blocks[nextFreeBlockIndex], sizeof(*blocks)); #ifdef DEBUG if (result != MMSYSERR_NOERROR) { fprintf(stderr, "Error: waveOutPrepareHeader returned %d\n", result); } #endif result = waveOutWrite(device, &blocks[nextFreeBlockIndex], sizeof(*blocks)); #ifdef DEBUG if (result != MMSYSERR_NOERROR) { fprintf(stderr, "Error: waveOutWrite returned %d\n", result); } #endif nextFreeBlockIndex = (nextFreeBlockIndex + 1) % BLOCK_COUNT; blocksBufferedAheadOfPlayback++; } // TODO: Try using a semaphore to signal from waveOutCallback instead of sleeping; might be able to improve both latency and performance Sleep(1); } SignalObjectAndWait(outputThreadDoneSemaphore, outputThreadResumeSemaphore, INFINITE, false); } return 0; } static WAVEFORMATEX createWaveFormat(AudioOut_sampleFormat * sampleFormat) { WAVEFORMATEX waveFormat; if (sampleFormat->bytesPerSample > 2) { sampleFormat->bytesPerSample = 2; } if (sampleFormat->channelCount > 2) { sampleFormat->channelCount = 2; } if (sampleFormat->sampleRate > SAMPLE_RATE_MAX) { sampleFormat->sampleRate = SAMPLE_RATE_MAX; } waveFormat.wFormatTag = WAVE_FORMAT_PCM; waveFormat.nChannels = sampleFormat->channelCount; waveFormat.nSamplesPerSec = sampleFormat->sampleRate; waveFormat.nAvgBytesPerSec = sampleFormat->sampleRate * sampleFormat->bytesPerSample * sampleFormat->channelCount; waveFormat.nBlockAlign = sampleFormat->bytesPerSample * sampleFormat->channelCount; waveFormat.wBitsPerSample = sampleFormat->bytesPerSample * 8; waveFormat.cbSize = 0; return waveFormat; } void AudioOut_init(const char * processName) { if (initialized) { return; } WAVEFORMATEX format = createWaveFormat(&g_AudioOut_hostFormat); MMRESULT result = waveOutOpen(&device, WAVE_MAPPER, &format, (DWORD_PTR) waveOutCallback, (DWORD_PTR) NULL, CALLBACK_FUNCTION); if (result != MMSYSERR_NOERROR) { #ifdef DEBUG fprintf(stderr, "Error: waveOutOpen returned %d\n", result); #endif return; } g_AudioOut_transportFormat = g_AudioOut_hostFormat = AudioOut_getHostFormat(); transportBuffer = malloc(TRANSPORT_BUFFER_SIZE); unsigned int blockFrameCount = BLOCK_FRAME_COUNT_44100 * g_AudioOut_hostFormat.sampleRate / 44100; unsigned int blockSize = blockFrameCount * g_AudioOut_hostFormat.bytesPerSample * g_AudioOut_hostFormat.channelCount; blocks = calloc(BLOCK_SIZE_MAX + sizeof(*blocks), BLOCK_COUNT); for (unsigned int blockIndex = 0; blockIndex < BLOCK_COUNT; blockIndex++) { blocks[blockIndex].dwBufferLength = blockSize; blocks[blockIndex].lpData = (void *) blocks + sizeof(*blocks) * BLOCK_COUNT + BLOCK_SIZE_MAX * blockIndex; } lastPlayedBlockIndex = 0; nextFreeBlockIndex = 0; outputThreadDoneSemaphore = CreateSemaphore(NULL, 0, 1, NULL); outputThreadResumeSemaphore = CreateSemaphore(NULL, 0, 1, NULL); outputThread = CreateThread(NULL, 0, outputThreadFunc, NULL, 0, NULL); initialized = true; } void AudioOut_shutdown(void) { } void AudioOut_setHostFormat(AudioOut_sampleFormat sampleFormat) { if (sampleFormat.channelCount == g_AudioOut_hostFormat.channelCount && sampleFormat.bytesPerSample == g_AudioOut_hostFormat.bytesPerSample && sampleFormat.sampleRate == g_AudioOut_hostFormat.sampleRate) { return; } if (initialized) { bool wasPlaying = g_AudioOut_outputActive; if (wasPlaying) { AudioOut_stopOutput(); } waveOutClose(device); WAVEFORMATEX waveFormat = createWaveFormat(&sampleFormat); MMRESULT result = waveOutOpen(&device, WAVE_MAPPER, &waveFormat, (DWORD_PTR) waveOutCallback, (DWORD_PTR) NULL, CALLBACK_FUNCTION); if (result != MMSYSERR_NOERROR) { waveFormat = createWaveFormat(&g_AudioOut_hostFormat); result = waveOutOpen(&device, WAVE_MAPPER, &waveFormat, (DWORD_PTR) waveOutCallback, (DWORD_PTR) NULL, CALLBACK_FUNCTION); } else { g_AudioOut_hostFormat = sampleFormat; } if (wasPlaying) { AudioOut_startOutput(g_AudioOut_outputCallback, g_AudioOut_outputContext); } } else { g_AudioOut_hostFormat = sampleFormat; } } void AudioOut_startOutput(AudioOutCallback callback, void * context) { if (!g_AudioOut_outputActive && device != NULL) { g_AudioOut_outputActive = true; waveOutReset(device); g_AudioOut_outputCallback = callback; g_AudioOut_outputContext = context; ReleaseSemaphore(outputThreadResumeSemaphore, 1, NULL); } } void AudioOut_stopOutput(void) { if (g_AudioOut_outputActive && device != NULL) { g_AudioOut_outputActive = false; WaitForSingleObject(outputThreadDoneSemaphore, INFINITE); } }