Wednesday 22 March 2017

Internet Radio example using Gambas cli

Now that I'm taking Gambas command-line programming more seriously, I thought I might rewrite the software on my internet radios.


As mentioned in a recent post, Gambas can be used to write cli programs (command-line interface) as well as the more usual gui (graphical) applications.


So this post just describes the software aspects of this project, while the other design details can be found in earlier posts.


Three of my 16 Raspberry Pi boards are used as internet radios. Two of them (KitchenPi and the Insomniacs Bedside Radio) are used every single day.



All three radios seem to run very reliably, barring the occasional wifi glitch. They all use a similar Python program, on headless Pi systems (one A, one A+ and one B+).

the requirements


Each radio operates in a similar fashion:-
  • a simple push-button is used to select the radio source
  • the source may be stored music files, or one of several internet radio streams
  • each time the button is pressed, the system steps through the range of sources
  • the text-to-speech engine "eSpeak" is used to announce the name/description of the currently selected source
  • the media player "mplayer" is used to play the selected source
  • the power to the radio is simply switched off when no longer required (i.e. no shutdown procedure)
So the Gambas program will implement these requirements, but unlike the current Python program, I won't use a simple program loop. This Gambas version will use a couple of event timers.

For control of the Pi gpio I have chosen to use wiringPi, although I could have used pigpio.

the Gambas code


First of all, add the declarations. For wiringPi:-

'declare gpio library & methods using wiringPi
Library "/usr/local/lib/libwiringPi"

Public Extern wiringPiSetup() As Integer        'Initialises wiringPi
Public Extern pinMode(pin As Integer, pud As Integer)   'sets Pin mode (in/output)
Public Extern digitalRead(pin As Integer) As Integer      'returns the input state (low=0, high=1)


Const SWITCH_INPUT As Integer = 7   'the Pi switch input pin


Program Source stuff:-

'Program Sources
Const JUKE_BOX As Integer = 0
Const RADIO_CAROLINE As Integer = 1
Const PLANET_ROCK As Integer = 2
Const RADIO_2 As Integer = 3
Const RADIO_4 As Integer = 4
Const WORLD_SERVICE As Integer = 5
Const HIGHEST_SOURCE As Integer = 5   'change if you change the number of sources

Const LAST_SOURCE As String = "/home/pi/LastSource"   'source # file store

Const PLAYER As String = "mplayer"
Const MUSIC As String = "/home/pi/Music/"
Const URL_CAROLINE As String = "http://sc6.radiocaroline.net:8040/listen.pls" '128k
Const URL_ROCK As String = "http://tx.sharp-stream.com/icecast.php?i=planetrock.mp3" '112k
Const URL_R2 As String = "http://www.listenlive.eu/bbcradio2.m3u" '128k
Const URL_R4 As String = "http://www.listenlive.eu/bbcradio4.m3u" '128k
Const URL_WORLD As String = "http://wsdownload.bbc.co.uk/worldservice/meta/live/shoutcast/mp3/eieuk.pls" '48k mp3



The event timers:-

Public hTimerPlayer As Timer       'timer to ckeck player is running
Public hTimerSwitch As Timer        'timer to check switch status



Now the Main routine, which runs when the program is started:-

Public Sub Main()
Dim hFile As File

  'this timer checks whether media player is running
  hTimerPlayer = New Timer As "tmrPlayerCheck"
  hTimerPlayer.Delay = 10000
  hTimerPlayer.start

  'this timer checks if switch has been pressed
  hTimerSwitch = New Timer As "tmrSwitchStatus"
  hTimerSwitch.Delay = 300
  hTimerSwitch.start

'config gpio
  wiringPiSetup()
  pinMode(SWITCH_INPUT, 0)
'set Pi volume
  Exec ["amixer", "sset", "PCM,0", "100%"]  
'100% for best quality
  
  If Not Exist(LAST_SOURCE) Then      'file with the last source ref
    hfile = Open LAST_SOURCE For Create
    Write #hFile, "0", Len("0")       'create file & use default source (Juke-box)
    Close #hFile
  Endif
'update playlist in case music files have been added/removed since player was last used
  Shell "find " & MUSIC & " -type f -iname *.ogg -o -name *.wma -o -iname *.mp3 > " & MUSIC & "playlist"
'use music source from last time program was run
  SelectSource(GetPreviousSource())     'run the player with the previously selected source
End


The Gambas Shell and Exec commands are similar, but Exec cannot be used with redirection operators like ">" hence my use of Shell to generate a playlist in the code above.

Here are the routines called from the Main() routine;

In the SelectSource function, espeak is executed, then followed by Wait. This is to stop the announcement talking-over the start of the stream (although this only seems to be a problem with the faster Pi models like Pi2 & Pi3).

Public Function SelectSource(iSource As Integer) As Integer
'choose source based upon value of iSource, announce it, then load mplayer & play it
  Select iSource
    Case RADIO_CAROLINE
      Exec ["espeak", "-ven+f4", "-s110", "-k5", "-a", "30", "Radio Caroline"] Wait
          Exec ["mplayer", "-cache", "64", "-playlist", URL_CAROLINE]
    Case PLANET_ROCK
      Exec ["espeak", "-ven+m5", "-s100", "-k5", "-a", "30", "Planet Rock"] Wait
          Exec ["mplayer", "-cache", "64", "-playlist", URL_ROCK]
    Case RADIO_2
      Exec ["espeak", "-ven+f1", "-s100", "-k5", "-a", "30", "BBC Radio two"] Wait
          Exec ["mplayer", "-cache", "64", "-playlist", URL_R2]
    Case RADIO_4
      Exec ["espeak", "-ven+m1", "-s100", "-k5", "-a", "30", "BBC Radio four"] Wait
          Exec ["mplayer", "-cache", "64", "-playlist", URL_R4]
    Case WORLD_SERVICE
      Exec ["espeak", "-ven+m3", "-s110", "-k5", "-a", "30", "The BBC world service"] Wait
          Exec ["mplayer", "-cache", "64", "-playlist", URL_WORLD]
    Case Else 'Juke-Box
      Exec ["espeak", "-ven+f2", "-s90", "-k9", "-a", "30", "play that funky music, White boy"] Wait
      'shuffle the music playlist each time the Juke-box is restarted
      Exec ["mplayer", "-shuffle", "-playlist", MUSIC & "playlist"]
  End Select
End


For more info on eSpeak see my earlier post.

File read and write routines;-

Public Function GetPreviousSource() As Integer
'open file and retrieve Program source number
Dim hFile As File
Dim strFile As String

    If Access(LAST_SOURCE, gb.Read) Then
      hFile = Open LAST_SOURCE For Read
      Read #hFile, strfile, -512
      Close #hFile
      Return CInt(strfile)
    Else
      Return 0
    Endif
End

Public Function SaveCurrentSource(iSource As Integer) As Boolean
Dim hFile As File
 
  If Access(LAST_SOURCE, gb.Write) Then
    hFile = Open LAST_SOURCE For Write
    Write #hFile, CStr(iSource), Len(CStr(iSource))
    Close #hFile
    Return True
  Endif
End


...and finally the event timers.

The switch timer checks the switch status about 3 times per second, which is plenty fast enough. However, if the switch has been pressed, the timer interval increases to 2s to stop the program from stepping through the program sources too quickly.

Public Sub tmrSwitchStatus_Timer()
Dim strProcIDs As String
Dim strPID As String
Dim intNext As Integer
 
  If digitalRead(SWITCH_INPUT) = 0 Then     'someone is pushing the button!
    hTimerSwitch.Delay = 2000  'debounce switch
    Exec ["pgrep", PLAYER] To strProcIDs
    If Len(strProcIDs) > 1 Then
      Do
        strPID = Mid(strProcIDs, 1, InStr(strProcIDs, Chr(10)) - 1)
        Exec ["kill", strPID] Wait
        strProcIDs = Mid(strProcIDs, InStr(strProcIDs, Chr(10)) + 1)
      Loop Until strProcIDs = ""
      intNext = GetPreviousSource() + 1
      If intNext > HIGHEST_SOURCE Then
        intNext = 0
      Endif
      SaveCurrentSource(intNext)
      SelectSource(GetPreviousSource())
    Endif
  Else
    hTimerSwitch.Delay = 300
  Endif
Catch
  'error handler (trap it, but don't do anything)
End

The player timer checks that the mplayer task is still running. If not it restarts it.

Public Sub tmrPlayerCheck_Timer()
'if player stops running, this should detect it and restart it
Dim sOutput As String
 
  Exec ["pgrep", "-f", "-l", PLAYER] Wait To sOutput
  If Split(Trim$(sOutput), gb.newLine).Count < 1 Then     'player is not running
    SelectSource(GetPreviousSource())
  Endif
End



Comparing this code with my original Python program, the Gambas version has about 35% more lines of text. There are 2 reasons for this. As Gambas is a BASIC dialect, it is naturally more wordy. But I have also included more white-space and declared more routines in this Gambas version to make the code more readable.

The time taken to type more text is often offset by the time wasted in the future, when you come back to it, and have to work out what it all means!

The switch operates much better on my Gambas version. I can hold the switch down and the system will step through each source. With my Python code, I had to press/release/press to step through the sources.

deployment


An executable can be created from within the Gambas IDE, which can then be run on a Pi once you have installed the Gambas runtime, eSpeak, mplayer and wiringPi.

For my lounge radio I used a fresh install of Raspbian Jessie Lite on a headless Pi, so my workflow looked like this:-
initial steps on a Linux system;
  • image an SD card & use gParted to expand the file system
  • edit the /etc/wpa_supplicant/wpa_supplicant.conf file by adding wifi details
  • create new folder: /home/pi/Music and set permissions to "anyone"
  • copy music files to Music folder
  • copy the internet radio exe (e.g. InternetRadio.gambas) to /home/pi  and set execute permissions to "anyone"
  • download wiringPi, unzip and copy files/folder structure to /home/pi
  • rename wiringPi-xxxxxx to wPi
  • set execute permissions for /home/pi/wPi/build to "anyone"
Move SD card to Pi;
  • ensure wifi dongle is fitted, then boot
  • find Pi IP address from router
  • Access Pi from Linux laptop: ssh pi@{ipAddress}
  • Run raspi-config and change: password, hostname, audio (to jack)
  • after reboot, ssh again
  • sudo apt update
  • sudo apt upgrade
  • sudo apt install espeak mplayer gambas3-runtime
  • install wiringPi: cd wPi then run ./build
  • test radio: /home/pi/InternetRadio.gambas
  • autorun: sudo nano /etc/rc.local
  • add on a new line just before the "exit" line: /home/pi/InternetRadio.gambas &
  • now re-boot and final test

For KitchenPi I just did everything via SSH and kept the existing operating system. So this radio only needed the Gambas runtime, my Gambas exe, wiringPi and a quick edit to /etc/rc.local.

It would be nice to use a faster-booting OS like Arch Linux, but that's a rainy day job for the future.

No comments:

Post a Comment