/* Copyright (c) 2020 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" #define _GNU_SOURCE #include #include #include #include #include #include #define TRANSPORT_BUFFER_SIZE 4096 #define CHUNK_FRAME_COUNT_44100 256 //#define MIN_DELAY_FRAMES 128 static bool initialized; static void * transportBuffer; static snd_pcm_t * pcmOutput; static pthread_t outputThread; static void msleep(unsigned int ms) { struct timespec sleepTimeSpec = {.tv_sec = ms / 1000, .tv_nsec = (ms % 1000) * 1000000}; struct timespec elapsedTimeSpec = sleepTimeSpec; nanosleep(&sleepTimeSpec, &elapsedTimeSpec); } /*static void closePCM(void) { if (g_AudioOut_outputActive) { g_AudioOut_outputActive = false; msleep(CHUNK_FRAME_COUNT * 2000 / g_AudioOut_hostFormat.sampleRate); } snd_pcm_close(pcmOutput); }*/ #ifdef DEBUG #define checkError(function, ...) \ error = function(__VA_ARGS__); \ if (error < 0) { \ fprintf(stderr, #function " returned %d (%s)\n", error, snd_strerror(error)); \ return; \ } #else #define checkError(function, ...) \ error = function(__VA_ARGS__); \ if (error < 0) { \ return; \ } #endif static void setPCMFormat(AudioOut_sampleFormat sampleFormat) { snd_pcm_hw_params_t * hwParams; snd_pcm_sw_params_t * swParams; snd_pcm_uframes_t periodSize = CHUNK_FRAME_COUNT_44100 * sampleFormat.sampleRate / 44100; snd_pcm_uframes_t bufferSize = periodSize * 2; snd_pcm_uframes_t boundary = 0; int dir = 0, error; snd_pcm_hw_params_alloca(&hwParams); checkError(snd_pcm_hw_params_any, pcmOutput, hwParams); checkError(snd_pcm_hw_params_set_rate_resample, pcmOutput, hwParams, true); checkError(snd_pcm_hw_params_set_access, pcmOutput, hwParams, SND_PCM_ACCESS_RW_INTERLEAVED); snd_pcm_format_t format = SND_PCM_FORMAT_S16; switch (sampleFormat.bytesPerSample) { case 1: format = SND_PCM_FORMAT_S8; break; case 2: format = SND_PCM_FORMAT_S16; break; case 3: format = SND_PCM_FORMAT_S24; break; case 4: format = SND_PCM_FORMAT_FLOAT; break; default: #ifdef DEBUG fprintf(stderr, "setPCMHardwareFormat: Unknown bytesPerSample: %u\n", sampleFormat.bytesPerSample); #endif return; } checkError(snd_pcm_hw_params_set_format, pcmOutput, hwParams, format); checkError(snd_pcm_hw_params_set_channels, pcmOutput, hwParams, sampleFormat.channelCount); checkError(snd_pcm_hw_params_set_rate_near, pcmOutput, hwParams, &sampleFormat.sampleRate, &dir); checkError(snd_pcm_hw_params_set_buffer_size_near, pcmOutput, hwParams, &bufferSize); checkError(snd_pcm_hw_params_set_period_size_near, pcmOutput, hwParams, &periodSize, &dir); g_AudioOut_hostFormat = sampleFormat; checkError(snd_pcm_hw_params, pcmOutput, hwParams); //checkError(snd_pcm_hw_params_get_buffer_size, hwParams, &bufferSize); //checkError(snd_pcm_hw_params_get_period_size, hwParams, &periodSize, &dir); //printf("Got buffer and period sizes: %u, %u\n", (unsigned int) bufferSize, (unsigned int) periodSize); snd_pcm_sw_params_alloca(&swParams); checkError(snd_pcm_sw_params_current, pcmOutput, swParams); checkError(snd_pcm_sw_params_set_start_threshold, pcmOutput, swParams, 1); checkError(snd_pcm_sw_params_get_boundary, swParams, &boundary); checkError(snd_pcm_sw_params_set_stop_threshold, pcmOutput, swParams, boundary); checkError(snd_pcm_sw_params_set_avail_min, pcmOutput, swParams, periodSize); checkError(snd_pcm_sw_params, pcmOutput, swParams); } void AudioOut_init(const char * processName) { if (initialized) { return; } int error; checkError(snd_pcm_open, &pcmOutput, "default", SND_PCM_STREAM_PLAYBACK, 0); setPCMFormat(g_AudioOut_hostFormat); checkError(snd_pcm_nonblock, pcmOutput, 0); g_AudioOut_transportFormat = g_AudioOut_hostFormat = AudioOut_getHostFormat(); transportBuffer = malloc(TRANSPORT_BUFFER_SIZE); initialized = true; //atexit(closePCM); } void AudioOut_shutdown(void) { } void AudioOut_setHostFormat(AudioOut_sampleFormat format) { if (format.channelCount == g_AudioOut_hostFormat.channelCount && format.bytesPerSample == g_AudioOut_hostFormat.bytesPerSample && format.sampleRate == g_AudioOut_hostFormat.sampleRate) { return; } if (initialized) { bool wasPlaying = g_AudioOut_outputActive; if (wasPlaying) { AudioOut_stopOutput(); } setPCMFormat(format); if (wasPlaying) { AudioOut_startOutput(g_AudioOut_outputCallback, g_AudioOut_outputContext); } } else { g_AudioOut_hostFormat = format; } } #define INITIAL_SILENCE_WRITE_COUNT 9 static void * outputThreadFunc(void * context) { int error; snd_pcm_sframes_t writeiReturn, framesWritten = 0, framesRemaining = 0, framesToWrite; unsigned int channelCount = g_AudioOut_hostFormat.channelCount; unsigned int bytesPerSample = g_AudioOut_hostFormat.bytesPerSample; unsigned int writeiCallCount = 0; //unsigned int sampleRate = g_AudioOut_hostFormat.sampleRate; //snd_pcm_sframes_t delay; //setpriority(PRIO_PROCESS, gettid(), -20); size_t chunkFrameCount = CHUNK_FRAME_COUNT_44100 * g_AudioOut_hostFormat.sampleRate / 44100; void * outputBuffer = calloc(g_AudioOut_hostFormat.channelCount * g_AudioOut_hostFormat.bytesPerSample, chunkFrameCount); framesToWrite = chunkFrameCount; while (g_AudioOut_outputActive) { if (framesRemaining == 0) { if (writeiCallCount > INITIAL_SILENCE_WRITE_COUNT) { AudioOut_transferToOutput(outputBuffer, g_AudioOut_outputCallback, g_AudioOut_outputContext, framesToWrite, transportBuffer, TRANSPORT_BUFFER_SIZE, g_AudioOut_hostFormat, g_AudioOut_transportFormat, &g_AudioOut_resampleState); } writeiReturn = snd_pcm_writei(pcmOutput, outputBuffer, framesToWrite); if (writeiReturn >= 0) { framesWritten = writeiReturn; } } else { writeiReturn = snd_pcm_writei(pcmOutput, outputBuffer + framesWritten * channelCount * bytesPerSample, framesRemaining); if (writeiReturn > 0) { framesWritten += writeiReturn; } } writeiCallCount++; if (writeiReturn < 0) { if (!g_AudioOut_outputActive) { break; } if (writeiReturn == -EAGAIN) { framesWritten = 0; framesRemaining = framesToWrite; msleep(1); continue; } if (writeiReturn == -EINTR || writeiReturn == -EPIPE || writeiReturn == -ESTRPIPE) { if (writeiReturn == -EPIPE) { msleep(1); if (!g_AudioOut_outputActive) { break; } } error = snd_pcm_recover(pcmOutput, writeiReturn, true); if (error < 0) { if (!g_AudioOut_outputActive) { break; } #ifdef DEBUG fprintf(stderr, "Error: snd_pcm_recover returned %d (%s)\n", error, snd_strerror(error)); #endif //return (void *) 1; } } framesWritten = 0; //} else if (writeiReturn == 0) { // SDL sleeps here; not sure if necessary? } if (framesWritten >= 0) { framesRemaining = framesToWrite - framesWritten; } //framesToWrite = CHUNK_FRAME_COUNT; //error = snd_pcm_delay(pcmOutput, &delay); //if (error == 0 && delay > MIN_DELAY_FRAMES) { // printf("delay = %d; usleeping for %d microseconds\n", (int) delay, (int) (delay - MIN_DELAY_FRAMES) * 1000000 / sampleRate); // usleep((delay - MIN_DELAY_FRAMES) * 1000000 / g_AudioOut_hostFormat.sampleRate); //usleep(100); //} } free(outputBuffer); return NULL; } void AudioOut_startOutput(AudioOutCallback callback, void * context) { if (!g_AudioOut_outputActive) { //int error = snd_pcm_prepare(pcmOutput); //if (error < 0) { // fprintf(stderr, "snd_pcm_prepare returned %d (%s)\n", error, snd_strerror(error)); // return; //} /*error = snd_pcm_start(pcmOutput); if (error < 0) { fprintf(stderr, "snd_pcm_start returned %d (%s)\n", error, snd_strerror(error)); return; }*/ g_AudioOut_outputCallback = callback; g_AudioOut_outputContext = context; pthread_attr_t attributes; pthread_attr_init(&attributes); pthread_attr_setschedpolicy(&attributes, SCHED_FIFO); struct sched_param priority = {-20}; pthread_attr_setschedparam(&attributes, &priority); g_AudioOut_outputActive = true; pthread_create(&outputThread, &attributes, outputThreadFunc, NULL); } } void AudioOut_stopOutput(void) { if (g_AudioOut_outputActive) { void * returnValue; g_AudioOut_outputActive = false; pthread_join(outputThread, &returnValue); //snd_pcm_drain(pcmOutput); } }