Skip to content

Instantly share code, notes, and snippets.

@Flix01
Last active August 22, 2024 14:23
Show Gist options
  • Save Flix01/157e8dafd9bef766092264ce6c1abbdb to your computer and use it in GitHub Desktop.
Save Flix01/157e8dafd9bef766092264ce6c1abbdb to your computer and use it in GitHub Desktop.
Very basic single-file, plain C, openAL mp3 radio decoder
// gist made after this issue: https://github.com/mackron/dr_libs/issues/142
/*
The license refers to this single file.
Every included or linked library comes with its own license
===============================================================================
Public Domain (www.unlicense.org)
===============================================================================
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this
software dedicate any and all copyright interest in the software to the public
domain. We make this dedication for the benefit of the public at large and to
the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to
this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
*/
/* VERY BASIC SINGLE FILE OPENAL MP3 RADIO DECODER
Dependencies [tested version on Ubuntu 20.04 and 24.04]:
OpenAL [libopenal-dev 1:1.19.1-1] https://openal-soft.org [https://github.com/kcat/openal-soft]
[libopenal-dev 1:1.23.1-4build] [modified code to make it work with this version. See: (***)]
libcurl4 [libcurl4-openssl-dev 7.68.0] https://curl.haxx.se
[libcurl4-openssl-dev 8.5.0-2ubuntu1]
one of the following:
dr_mp3.h [v0.6.13 - 2020-07-06] https://github.com/mackron/dr_libs/blob/master/dr_mp3.h
[v0.6.39 - 2024-02-27]
minimp3.h [?] https://github.com/lieff/minimp3/blob/master/minimp3.h
USAGE: when run with an argument it tries to play the mp3 stream url, otherwise it just plays a default one.
The program NEVER quits! Unless you try the STOP_STREAM_USING_GETCH definition below.
WARNING: an error output: "curl_easy_perform() failed: Couldn't connect to server [res=7]" means
that the stream server is currently offline (not all stations broadcast all the time. Try running
again with another argument.
COMPILATION:
// Linux (tested)
gcc -O3 -no-pie -fno-pie mini_mp3_radio_decoder.c -o mini_mp3_radio_decoder -lopenal -lcurl
// Windows (incorrect, but just to have something to get started)
// Mingw attempt (never tested)
x86_64-w64-mingw32-gcc -O3 -no-pie -fno-pie -mconsole mini_mp3_radio_decoder.c -o mini_mp3_radio_decoder.exe -DWINVER=0x0800 -D_WIN32 -D_WIN64 -luser32 -lkernel32 -lOpenAL32 -lcurl
// cl attempt (never tested)
cl /TC /O2 /ML mini_mp3_radio_decoder.c /link /out:mini_mp3_radio_decoder.exe Shell32.lib user32.lib kernel32.lib OpenAL32.lib curl.lib
// Emscripten (?)
Not sure it's possible/convenient to convert this code for emscripten. Some facts:
OpenAL works in emscripten
curl does NOT work AFAIK, but usually people replaces it using emscripten_wget(...) or emscripten_wget2(...) compiling with -s ASYNCIFY=1.
dr_mp3.h or minimp3.h could work (there are definitions to remove SIMD/NEON)
In any case I guess that by using some kind of Web API maybe we can perform the same task in a much better and easy way.
*/
//========================================================================================
// OPTIONAL DEFINITIONS (uncomment here, o use -D DEFINITION in the compiler command line)
//========================================================================================
//#define USE_MINIMP3 // default uses "dr_mp3.h"
//#define USE_FLOAT_SAMPLES // otherwise short samples are used
//#define STOP_STREAM_USING_GETCH // experimental (pressing any key quits program, and 'c' changes radio station)
//========================================================
// common headers
#include <stdio.h> // printf
#include <assert.h>
#include <string.h> // memset, memmove and other stuff
// curl header
#include <curl/curl.h> /* License inspired by MIT/X, but not identical (usable in commercial projects): https://curl.haxx.se/docs/copyright.html */
// openal headers
//#define ALEXT_H_IS_PRESENT // optional (well, if you have 'AL/alext.h', define it!)
#ifdef ALEXT_H_IS_PRESENT
# include <AL/alext.h>
#else //ALEXT_H_IS_PRESENT
# ifndef AL_EXT_float32 // we assume AL_EXT_float32 is implemented
# define AL_EXT_float32 1
# define AL_FORMAT_MONO_FLOAT32 0x10010
# define AL_FORMAT_STEREO_FLOAT32 0x10011
# endif //AL_EXT_float32
#endif //__EMSCRIPTEN__
#include <AL/al.h>
#include <AL/alc.h>
#ifdef USE_FLOAT_SAMPLES
typedef ALfloat ALsampletype;
const ALenum formats[2] = {AL_FORMAT_MONO_FLOAT32,AL_FORMAT_STEREO_FLOAT32};
#else
typedef ALshort ALsampletype;
const ALenum formats[2] = {AL_FORMAT_MONO16,AL_FORMAT_STEREO16};
#endif
#if (!defined(_NDEBUG) && !defined(NDEBUG) && !defined(NO_AL_CHECKS))
static void al_checkError() {
ALenum error = alGetError();
if (error!=AL_NO_ERROR) fprintf(stderr,"AL_ERROR: %s\n",alGetString(error));
assert(error==AL_NO_ERROR);
}
#define AL_CHECKERROR al_checkError()
#else
#define AL_CHECKERROR /*no-op*/
#endif
#ifdef STOP_STREAM_USING_GETCH
#ifdef _WIN32
# include <conio.h>
#else // tested only on Linux
# include <unistd.h>
# include <termios.h>
char getch(void) {
/* https://stackoverflow.com/questions/7469139/what-is-the-equivalent-to-getch-getche-in-linux */
char buf = 0;
struct termios old = {0};
fflush(stdout);
if(tcgetattr(0, &old) < 0)
perror("tcsetattr()");
old.c_lflag &= ~ICANON;
old.c_lflag &= ~ECHO;
old.c_cc[VMIN] = 1;
old.c_cc[VTIME] = 0;
if(tcsetattr(0, TCSANOW, &old) < 0)
perror("tcsetattr ICANON");
if(read(0, &buf, 1) < 0)
perror("read()");
old.c_lflag |= ICANON;
old.c_lflag |= ECHO;
if(tcsetattr(0, TCSADRAIN, &old) < 0)
perror("tcsetattr ~ICANON");
//printf("%c\n", buf);
return buf;
}
# include <sys/ioctl.h>
int kbhit() {
/* https://stackoverflow.com/questions/29335758/using-kbhit-and-getch-on-linux?rq=1 */
struct termios term,term2;int byteswaiting;
tcgetattr(0, &term);
term2 = term;
term2.c_lflag &= ~ICANON;
tcsetattr(0, TCSANOW, &term2);
ioctl(0, FIONREAD, &byteswaiting);
tcsetattr(0, TCSANOW, &term);
return (byteswaiting > 0) ? 1 : 0;
}
#endif // _WIN32
__inline static char async_getch(void) {return kbhit() ? getch() : 0;}
#endif /* STOP_STREAM_USING_GETCH */
//========================================================
// DEFINITIONS TO TWEAK:
//========================================================
#define ENCODED_BUFFER_DECODING_STEP (16384) // Decoding chunk size step to pass to drmp3dec_decode_frame(...). Better leave it at 16384 (or maybe a multiple?). minimp3 docs say: «We recommend having as many as 10 consecutive MP3 frames (~16KB) in the input buffer at a time.»
#define ENCODED_BUFFER_INIT_CHUNK_SIZE (ENCODED_BUFFER_DECODING_STEP*2) // Before starting decoding an internet stream, wait until ENCODED_BUFFER_INIT_CHUNK_SIZE bytes are available
#define ENCODED_BUFFER_UPDATE_CHUNK_SIZE (ENCODED_BUFFER_DECODING_STEP) // And then update as soon as ENCODED_BUFFER_UPDATE_CHUNK_SIZE are available
#define ENCODED_BUFFER_SIZE (ENCODED_BUFFER_DECODING_STEP*32)
static unsigned char encodedBuffer[ENCODED_BUFFER_SIZE]; // Do we really need this? Can't we just decode the stream directly? Even if size<DRMP3_DATA_CHUNK_SIZE?
static size_t encodedBufferSize=0;
#if ENCODED_BUFFER_DECODING_STEP>=ENCODED_BUFFER_SIZE || ENCODED_BUFFER_INIT_CHUNK_SIZE>=ENCODED_BUFFER_SIZE || ENCODED_BUFFER_UPDATE_CHUNK_SIZE>=ENCODED_BUFFER_SIZE
# error
#endif
#if ENCODED_BUFFER_INIT_CHUNK_SIZE<ENCODED_BUFFER_DECODING_STEP || ENCODED_BUFFER_UPDATE_CHUNK_SIZE<ENCODED_BUFFER_DECODING_STEP || ENCODED_BUFFER_SIZE<ENCODED_BUFFER_DECODING_STEP
# error
#endif
#define DECODED_BUFFER_SIZE (ENCODED_BUFFER_SIZE*8)
static ALsampletype pcm[DECODED_BUFFER_SIZE]; // This can be moved inside openal_update_buffers(...)
//#define NUM_STREAMING_BUFFERS 2 // (optional) OpenAL-related
//========================================================
#ifndef USE_MINIMP3
# ifdef USE_FLOAT_SAMPLES
# define DR_MP3_FLOAT_OUTPUT
# endif
//# define DR_MP3_NO_STDIO
//# define DR_MP3_NO_SIMD
# define DR_MP3_IMPLEMENTATION
# include "dr_mp3.h" // https://github.com/mackron/dr_libs
#else //USE_MINIMP3
# ifdef USE_FLOAT_SAMPLES
# define MINIMP3_FLOAT_OUTPUT
# endif
//# define MINIMP3_ONLY_MP3
//# define MINIMP3_ONLY_SIMD
//# define MINIMP3_NO_SIMD
//# define MINIMP3_NONSTANDARD_BUT_LOGICAL
# define MINIMP3_IMPLEMENTATION
# include "minimp3.h" // https://github.com/lieff/minimp3
typedef mp3dec_frame_info_t drmp3dec_frame_info;
typedef mp3dec_t drmp3dec;
# define drmp3dec_init(A) mp3dec_init(A)
# define drmp3dec_decode_frame(A,B,C,D,E) mp3dec_decode_frame(A,B,C,D,E)
#endif //USE_MINIMP3
#ifndef NUM_STREAMING_BUFFERS
# define NUM_STREAMING_BUFFERS 3 // not sure what's better here
#elif NUM_STREAMING_BUFFERS<2
# undef NUM_STREAMING_BUFFERS
# define NUM_STREAMING_BUFFERS 2
#endif
typedef struct openal_t {
ALCdevice *playbackDevice;
ALCcontext * playbackContext;
ALuint buffers[NUM_STREAMING_BUFFERS];
ALuint source;
volatile int stop_streaming;
volatile char pressed_char;
drmp3dec mp3;
drmp3dec_frame_info mp3info;
} openal_t;
void openal_init(openal_t* al) {
memset(al,0,sizeof(*al));
al->playbackDevice = alcOpenDevice(NULL);
if (al->playbackDevice) {
al->playbackContext = alcCreateContext(al->playbackDevice, NULL);
if (al->playbackContext) {
// (***) Hack part 1: with newer version of OpenAL we need a starting dummy
// (blank) buffer to set up out streaming system, otherwise program won't start
// We don't care much about the format here... it's just to make the system start.
ALsizei size = 8; // 1 second of silence at 44100 Hz should be 44100. We use 8.
ALenum format = AL_FORMAT_MONO16; // Mono 16-bit
ALshort data[size*sizeof(ALshort)] = {};
int j;
// End Hack Part 1
alcMakeContextCurrent(al->playbackContext);
alGenSources(1, &al->source);
alGenBuffers(NUM_STREAMING_BUFFERS, al->buffers);
alSourcei(al->source, AL_SOURCE_RELATIVE, AL_TRUE);
alSource3f(al->source, AL_POSITION, 0.0f, 0.0f, 0.0f);
alSource3f(al->source, AL_VELOCITY, 0.0f, 0.0f, 0.0f);
# ifdef USE_FLOAT_SAMPLES
{
ALboolean isFloatExtensionPresent = alIsExtensionPresent("AL_EXT_float32");
assert(isFloatExtensionPresent==AL_TRUE);
}
# endif
AL_CHECKERROR;
// (***) Hack part 2: we fill all the buffers with blank data
for (j=0;j<NUM_STREAMING_BUFFERS;j++) alBufferData(al->buffers[j],format,data,size*sizeof(ALshort), 44100);
// End Hack Part 2
alSourceQueueBuffers(al->source, NUM_STREAMING_BUFFERS, al->buffers);AL_CHECKERROR;
alSourcePlay(al->source);AL_CHECKERROR; // Might seem dumb, but without this line, nothing works... even if source state becomes AL_STOPPED soon
}
}
AL_CHECKERROR;
}
void openal_destroy(openal_t* al) {
AL_CHECKERROR;
if (al->playbackContext) {
if (al->source && al->buffers) {alSourceUnqueueBuffers(al->source, NUM_STREAMING_BUFFERS, al->buffers);AL_CHECKERROR;}
alDeleteSources(1, &al->source);
alDeleteBuffers(NUM_STREAMING_BUFFERS, al->buffers);
alcMakeContextCurrent(NULL);
alcDestroyContext(al->playbackContext);al->playbackContext=NULL;
}
if (al->playbackDevice) {alcCloseDevice(al->playbackDevice);al->playbackDevice=NULL;}
memset(al,0,sizeof(*al));
}
size_t openal_update_buffers(openal_t* al) {
size_t decoded_size = 0;ALint toprocess = NUM_STREAMING_BUFFERS;
AL_CHECKERROR;
alGetSourcei(al->source, AL_BUFFERS_PROCESSED, &toprocess);AL_CHECKERROR;
assert(toprocess<=NUM_STREAMING_BUFFERS);
if (toprocess>0) {
size_t sum = 0, average = 0, split;
assert(encodedBufferSize>=ENCODED_BUFFER_DECODING_STEP);
while (encodedBufferSize>=ENCODED_BUFFER_DECODING_STEP) {
// decode ENCODED_BUFFER_STEP steps
size_t samples_read = drmp3dec_decode_frame(&al->mp3,&encodedBuffer[0],ENCODED_BUFFER_DECODING_STEP,&pcm[decoded_size], &al->mp3info);
samples_read*=al->mp3info.channels; // == al->num_channels
assert(encodedBufferSize>=(size_t)al->mp3info.frame_bytes);
memmove(&encodedBuffer[0],&encodedBuffer[al->mp3info.frame_bytes],encodedBufferSize-al->mp3info.frame_bytes); // slow...
encodedBufferSize-=al->mp3info.frame_bytes;
decoded_size+=samples_read;
assert(decoded_size<=DECODED_BUFFER_SIZE);
if (decoded_size+samples_read>DECODED_BUFFER_SIZE) break;
}
if (decoded_size>0) {
// we just split 'decoded_size' in each 'processed' buffer
ALint processed = toprocess;
average=decoded_size/toprocess;if (average>0 && average%2==1) --average; // maybe stereo signals need even samples
sum = 0;
assert(al->mp3info.channels>0 && al->mp3info.channels<3 && al->mp3info.hz>0);
if (toprocess<NUM_STREAMING_BUFFERS) {
while (processed--) {
ALuint albuffer=0;
split = processed==0 ? (decoded_size-sum) : average;
AL_CHECKERROR;
alSourceUnqueueBuffers(al->source, 1, &albuffer);AL_CHECKERROR;
alBufferData(albuffer,formats[al->mp3info.channels-1], &pcm[sum], split*sizeof(pcm[0]), al->mp3info.hz);AL_CHECKERROR;
alSourceQueueBuffers(al->source, 1, &albuffer);AL_CHECKERROR;
sum+=split;
}
}
else {
// when all buffers are empty, OpenAL stops the stream AFAICS
alSourceUnqueueBuffers(al->source, NUM_STREAMING_BUFFERS, al->buffers);
AL_CHECKERROR;
for (processed=0;processed<toprocess;processed++) {
split = processed==toprocess-1 ? (decoded_size-sum) : average;
alBufferData(al->buffers[processed],formats[al->mp3info.channels-1], &pcm[sum], split*sizeof(pcm[0]), al->mp3info.hz);AL_CHECKERROR;
sum+=split;
}
alSourceQueueBuffers(al->source, NUM_STREAMING_BUFFERS, al->buffers);AL_CHECKERROR;
alSourcePlay(al->source);AL_CHECKERROR;
}
assert(sum==decoded_size);
}
}
return decoded_size;
}
static openal_t al = {0};
// curl callback declaration
static size_t stream_callback(char *ptr, size_t size, size_t nmemb, void *userdata);
int main(int argc, char* argv[]) {
const char* mp3_radio_urls[] = {NULL,"http://194.97.151.153:80/rockantenne","http://194.97.151.139:80/antenne","https://streamingv2.shoutcast.com/rtl-1025"};
const size_t num_mp3_radio_urls = sizeof(mp3_radio_urls)/sizeof(mp3_radio_urls[0]);
size_t cmp3_radio_index = 0;
CURL *c;CURLcode res;
const char * webaddr = NULL; /* URL of the radio-station in <ip-addr>:<port> format */
if (argc>1) mp3_radio_urls[0] = argv[1];
openal_init(&al);
// curl init
c = curl_easy_init();
curl_easy_setopt (c, CURLOPT_WRITEFUNCTION, stream_callback);
curl_easy_setopt (c, CURLOPT_NOPROGRESS, 1);
curl_easy_setopt (c, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
# ifdef STOP_STREAM_USING_GETCH
while (1) {
# endif
// fetch a non-NULL webaddr from the list
webaddr = mp3_radio_urls[cmp3_radio_index];
while (!webaddr) {
if (++cmp3_radio_index>=num_mp3_radio_urls) cmp3_radio_index=0;
webaddr=mp3_radio_urls[cmp3_radio_index];
}
// curl perform (hopefully this block can be repeated when implementing radio station switch)
curl_easy_setopt(c, CURLOPT_URL, webaddr);
res = curl_easy_perform(c);
/* Ideally code following this line is NEVER executed as the radio station will reply with a continuous never-ending infinite encoded audio stream. */
if(res != CURLE_OK
# ifdef STOP_STREAM_USING_GETCH
&& res!= CURLE_WRITE_ERROR /* This seems to happen when al.stop_streaming is set to 1 */
# endif
) fprintf(stderr,"curl_easy_perform() failed: %s [res=%d]\n",curl_easy_strerror(res),res);
# ifdef STOP_STREAM_USING_GETCH
if (res==CURLE_WRITE_ERROR && al.pressed_char=='c') {if (++cmp3_radio_index>=num_mp3_radio_urls) cmp3_radio_index=0;}
else break; // just exit
}
# endif
// curl cleanup
curl_easy_cleanup(c);
openal_destroy(&al);
return 0;
}
// curl stream callback [most important function of the program]
size_t stream_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {
int is_mp3_inited = (al.mp3info.channels > 0);
// here we handle the encoded stream on the write-side (read-size is inside openal_update_buffers(...))
size_t amount = size*nmemb, rv = amount;
(void)userdata; // unused
if (amount>=ENCODED_BUFFER_SIZE) {
// [Robustness] keep only most recent data
memcpy(&encodedBuffer[0],&ptr[amount-ENCODED_BUFFER_SIZE],ENCODED_BUFFER_SIZE);
encodedBufferSize=ENCODED_BUFFER_SIZE;
}
else if (encodedBufferSize+amount>ENCODED_BUFFER_SIZE) {
// [Robustness] keep only most recent data
memmove(&encodedBuffer[0],&encodedBuffer[encodedBufferSize+amount-ENCODED_BUFFER_SIZE],ENCODED_BUFFER_SIZE-amount); // is this line correct ?
memcpy(&encodedBuffer[ENCODED_BUFFER_SIZE-amount],ptr,amount);
encodedBufferSize=ENCODED_BUFFER_SIZE;
}
else {
// this should happen 99.9% of the time
memcpy(&encodedBuffer[encodedBufferSize],ptr,amount);
encodedBufferSize+=amount;
}
// here we decode the stream
if (!is_mp3_inited) {
if (encodedBufferSize >= ENCODED_BUFFER_INIT_CHUNK_SIZE) {
drmp3dec_init(&al.mp3);
openal_update_buffers(&al);
assert(al.mp3info.bitrate_kbps>0);
assert(al.mp3info.channels>0);
assert(al.mp3info.hz>0);
fprintf(stderr,"drmp3dec_init(...) OK [SR:%dHz;CH:%d;BR:%dkbps].\n",al.mp3info.hz,al.mp3info.channels,al.mp3info.bitrate_kbps);
is_mp3_inited = 1;
}
}
else if (encodedBufferSize >= ENCODED_BUFFER_UPDATE_CHUNK_SIZE) openal_update_buffers(&al);
// exit strategy
# ifdef STOP_STREAM_USING_GETCH
al.pressed_char = async_getch();
if (al.pressed_char!='\0') {
printf("\b \b");fflush(stdout); // (bad) way to delete the pressed char from terminal
al.stop_streaming = 1;
}
# endif
if (al.stop_streaming) {
rv = 0; // returning zero stops curl_easy_perform(...), with a CURLE_WRITE_ERROR
if (is_mp3_inited) {
// cleanup (hopefully to restart with another radio station)
alSourceStop(al.source);AL_CHECKERROR;
memset(&al.mp3,0,sizeof(al.mp3));
memset(&al.mp3info,0,sizeof(al.mp3info));
}
encodedBufferSize=0;
al.stop_streaming=0;
}
return rv;
}
@Flix01
Copy link
Author

Flix01 commented Jul 29, 2020

CHANGELOG:

REVISION 4

  • Fixed a problem with newer versions of OpenAL that caused a program crash at startup.

REVISION 3

  • Now code is 10 lines shorter 😄.
  • Fixed (:crossed_fingers:) robustness in case of encoded buffer overflow in stream_callback(...).

REVISION 2

  • Now code is 11 lines shorter 😄.
  • Removed second argument of openal_update_buffers(...).
  • Improved robustness in case of encoded buffer overflow in stream_callback(...).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment