2021-10-04

Internet Radio Player on Raspberry Pi

I still remember the day I started to make an android app for my mother-in-law to listen to internet radio because the place she lived received has poor FM radio broadcast signal.  Why I needed to bother to code an app instead of using an off-the-shelf app is because my old mother-in-law is not versatile enough to play with the andriod UI.  So my app is simply there is power on / power charge, it will start automatically.  When there is no power charge, it will sleep automatically after a certain time-out (via BroadcastReceiver to receive the POWER_CONNECTED and POWER_DISCONECTED events).  So she could simply use a power switch to turn on/off the app.

I used the MediaPlayer android object as the main part of the program.  Yes, it is an easy-to-use object and I do not need to bother to handle the http download, mp3 conversion and sound playing logic.  However, problem occurred when I use the app on site because there is transient (but now always) wifi stability problem.  It seems that MediaPlayer object cannot handle the error in a very graceful manner. (I have already handled all the documented states of the object and capture all the exceptions!).  When there was wifi problem, the app simply played on sound (and then forever).  So far, what I could do is to ask my mother-in-law to switch off the charging and wait a few minutes and turn-on the charger again.  But this was really inconvenient.

Years passed now and my mother-in-law has also died.  But the idea of how to build a robust internet radio app is always in my mind.  Recently I still to study the idea to build this app on my Raspberry Pi (which is has already wifi, and a built-in ear-phone jack (not of hifi quality but okay for internet radio).

This time, I nearly started from scratch (not really).  I use libcurl for the http protocol, libmpg123 for the mp3 conversion and alsa (libasound) for the sound playing.  For libcurl, I used the multi interface because it allows time-out handling.  The program codes are not long after all these logic implementation.  Most of the time I have spent is on the studying of API and choose the right ones because I have not used these libraries before).  The name of the only C file is 'pi_rthk.c' because it played RTHK.  I have also tested the problem for other internet radio URL's.


/*
File: pi_rthk.c
Description: an internet radio to play RTHK
Libraries : curl, mpg123 asound
Findings:
(1) uses multi interface for curl to handle timeout (although there is only one easy handle
(2) the work at the curl write callback handler will affect the throughput.  But curl will increases the number of bytes per call. 
(3) my original idea is to use mpg123_decode() but it turns out that mpg123_decode() keeps on complaining MPG123_NEED_MORE. Even changing to use mpg123_feed() and mpg123_read() is in vain.  Finally after reading some blog, I manage to use mpg123_decode_frame()
(4) some discussion thread on mpg123
https://sourceforge.net/p/mpg123/mailman/mpg123-users/thread/BANLkTik8wkAQ9gVtzAPeij6jZ1SuK0%2BLWA%40mail.gmail.com/#msg27415661
(5) after I start the coding, I find there is a simlary program by Johnny Huang
http://hzqtc.github.io/2012/05/play-mp3-with-libmpg123-and-libao.html
The only difference is he uses libao and I use libasound
*/

#include <stdio.h>
#include <string.h>
#include <sys/time.h>
#include <unistd.h>
#include <signal.h>
#include <ctype.h>

#include <curl/curl.h>
#include <mpg123.h>
#include <alsa/asoundlib.h>

mpg123_handle *mh = NULL;
snd_pcm_t *playback_handle;
CURL *http_handle;
CURLM *multi_handle;
int channels;

void str_trim (char *s)
// triming the trailing white spaces by modifying the original string
{
int m = strlen (s);
int i;
for (i=m-1; i>=0; i--) {
  if (isspace(s[i]))
    s[i] = '\0';
  else
    break;
  }
} // str_trim();

void str_toupper (char *s)
// converting the string to upper case by modify ihe original string
{
int m = strlen (s);
if (m == 0)
  return;
int i;
for (i=0; i<m; i++)
  s[i] = toupper (s[i]);
} // str_toupper ()

static size_t curl_header_callback (char *buffer, size_t size, size_t nitems, void *userdata)
{
size_t numbytes = size * nitems;

// fprintf(stderr, "HTTP HEADER : %.*s", numbytes, buffer);

char b[1000];
char name[1000];
char value[1000];
memcpy (b, buffer, (numbytes>999) ? 999 : numbytes);
b[numbytes]= '\0';
str_trim (b);
// fprintf (stderr, "after triming [%s]\n", b);
fprintf (stderr, "HTTP HEADER : [%s]\n", b);
str_toupper (b);
// fprintf (stderr, "after toupper [%s]\n", b);
sscanf (b, "%s %s", name, value);
if (strcmp (name, "CONTENT-TYPE:") == 0) {
  if (strcmp (value, "AUDIO/MPEG") != 0) {
    fprintf (stderr, "ERROR: Content-Type (%s) is NOT \"audio/mpeg\"\n", name);
    return 0; // error exit
    }
  }
return numbytes;
}

size_t curl_write_callback_handler (char *ptr, size_t size, size_t nmemb, void *userdata)
{
size_t decoded_bytes;
long int rate;
int encoding;
int err = MPG123_OK;
int frames;

fprintf (stderr, "inside write_callback() receiving %d bytes\n", nmemb);

err = mpg123_feed (mh, ptr, nmemb); // size is always 1 in curl
if (err != MPG123_OK) {
  fprintf(stderr, "ERROR: mpg123_feed fails... %s", mpg123_plain_strerror(err));
  return 0;
  }
fprintf (stderr, "mpg123_feed() ok\n");

 off_t frame_offset;
 unsigned char *audio;
 do {
   err = mpg123_decode_frame (mh, &frame_offset, &audio, &decoded_bytes);
   switch (err) {
     case MPG123_NEW_FORMAT:
       fprintf (stderr, "mpg123_decode_frame returns MPG123_NEW_FORMAT\n");
       if (MPG123_OK != mpg123_getformat(mh, &rate, &channels, &encoding)) {
         fprintf (stderr, "ERROR: mpg123_getformat fails\n");
         return 0;
         }
       fprintf (stderr, "rate = %ld channels = %d encoding = %d\n", rate, channels, encoding);
       if (encoding == MPG123_ENC_SIGNED_16)
         fprintf (stderr, "encoding is signed 16 bit\n");
       if (MPG123_OK != mpg123_format (mh, rate, channels, encoding)) {
         fprintf (stderr, "mpg123_format fails\n");
         return 0;
         }
       fprintf (stderr, "mpg123_format ok\n");
       if ((err = snd_pcm_set_params(playback_handle,
             SND_PCM_FORMAT_S16_LE,
             SND_PCM_ACCESS_RW_INTERLEAVED,
             channels,
             (unsigned int) rate,
             0, /* disallow resampling */
             500000)) < 0) {   /* 0.5sec */
         fprintf(stderr, "ERROR: snd_pcm_set_params() fails: %s\n", snd_strerror(err));
         return 0;
         }
       fprintf (stderr, "snd_pcm_set_params() ok\n");
       break;
     case MPG123_NEED_MORE:
       fprintf (stderr, "mpg123_decode_frame returns MPG123_NEED_MORE with decoded_bytes = %d\n", decoded_bytes);
       break;
     case MPG123_OK :
       fprintf (stderr, "mpg123_decode_frame() returns MPG124_OK with decoded_bytes = %d\n", decoded_bytes);
       if (decoded_bytes > 0) {
         frames = decoded_bytes / 2 / channels; // 2 == 16(sample size) / 8(bits per byte)
         // frames = decoded_bytes * 8 / 2 / 16;
         err = snd_pcm_writei (playback_handle, audio, frames);
         if (err != frames) {
           fprintf (stderr, "write to audio interface failed (%s)\n",
             snd_strerror (err));
           return 0;
           }
         }
       break;
     default: 
       fprintf(stderr, "ERROR: mpg123_read fails... %s", mpg123_plain_strerror(err));
       break;
     } // switch
   } while (decoded_bytes > 0);

return nmemb;

} // curl_write_callback_handler()

void radio_clean_up()
{
mpg123_delete (mh);
snd_pcm_close (playback_handle);
curl_multi_remove_handle(multi_handle, http_handle);
curl_easy_cleanup(http_handle);
curl_multi_cleanup(multi_handle);
curl_global_cleanup();
}

void default_signal_handler(int signal_number)
{
  fprintf (stderr, "Inside %s\n", __func__);
  radio_clean_up();
  fprintf (stderr, "Program exits\n");
  exit(0);
}

int main(void)
{

signal(SIGINT, default_signal_handler);

int err;

if ((err = snd_pcm_open (&playback_handle, "default", SND_PCM_STREAM_PLAYBACK, 0)) < 0) {
  fprintf (stderr, "cannot open audio device (%s)\n", snd_strerror (err));
  return 1;
  }

curl_version_info_data *d = curl_version_info (CURLVERSION_NOW);
fprintf (stderr, "curl version %s\n", d->version);

fprintf (stderr, "mpg123 version %d\n", MPG123_API_VERSION);

fprintf (stderr, "ALSA version %s\n", snd_asoundlib_version());

#if MPG123_API_VERSION < 46
err = mpg123_init();
if (err != MPG123_OK) {
  fprintf (stderr, "ERROR: mpg123_init() fails... %s\n", mpg123_plain_strerror(err));
  return 0;
  }
#endif

mh = mpg123_new(NULL, &err);
if (mh  == NULL) {
  fprintf(stderr, "ERROR: mpg123_new fails... %s", mpg123_plain_strerror(err));
  return 0;
  }

err = mpg123_open_feed (mh);
if (err != MPG123_OK) {
  fprintf(stderr, "ERROR: mpg123_open_feed fails... %s", mpg123_plain_strerror(err));
  return 0;
  }
fprintf (stderr, "mpg123_open_feed ok\n");

int still_running = 1; /* keep number of running handles */

curl_global_init(CURL_GLOBAL_DEFAULT);

http_handle = curl_easy_init();

curl_easy_setopt (http_handle, CURLOPT_URL, "http://stm.rthk.hk/radio1");

curl_easy_setopt (http_handle, CURLOPT_HEADERFUNCTION, curl_header_callback);

curl_easy_setopt (http_handle, CURLOPT_WRITEFUNCTION, curl_write_callback_handler);
 
multi_handle = curl_multi_init();

curl_multi_add_handle(multi_handle, http_handle);

do {
  CURLMcode mc = curl_multi_perform(multi_handle, &still_running);
  if(!mc)
/* since the version of libcurl in Raspberry Pi is quite old */
#if LIBCURL_VERSION_NUM >= 0x076600
    mc = curl_multi_poll(multi_handle, NULL, 0, 1000, NULL);
#else
    mc = curl_multi_wait (multi_handle, NULL, 0, 1000, NULL);
#endif

  if (mc) {
    fprintf(stderr, "curl_multi_poll() or curl_multi_wait() failed, code %d.\n", (int)mc);
    break;
    }

  } while (still_running);

fprintf (stderr, "Program exits\n");
return 0;
} // main