4         "code.octet-stream.net/broadcaster/protocol" 
   7         "github.com/gopxl/beep/v2" 
   8         "github.com/gopxl/beep/v2/mp3" 
   9         "github.com/gopxl/beep/v2/speaker" 
  10         "github.com/gopxl/beep/v2/wav" 
  11         "golang.org/x/net/websocket" 
  21 const sampleRate 
= 44100 
  23 var config RadioConfig 
= NewRadioConfig() 
  26         configFlag 
:= flag
.String("c", "", "path to configuration file") 
  28         //generateFlag := flag.String("g", "", "create a template config file with specified name then exit") 
  31         if *configFlag 
== "" { 
  32                 log
.Fatal("must specify a configuration file with -c") 
  34         config
.LoadFromFile(*configFlag
) 
  35         statusCollector
.Config 
<- config
 
  37         playbackSampleRate 
:= beep
.SampleRate(sampleRate
) 
  38         speaker
.Init(playbackSampleRate
, playbackSampleRate
.N(time
.Second
/10)) 
  40         if config
.PTTPin 
!= -1 { 
  41                 InitRaspberryPiPTT(config
.PTTPin
, config
.GpioDevice
) 
  43         if config
.COSPin 
!= -1 { 
  44                 InitRaspberryPiCOS(config
.COSPin
, config
.GpioDevice
) 
  47         sig 
:= make(chan os
.Signal
, 1) 
  48         signal
.Notify(sig
, syscall
.SIGINT
, syscall
.SIGTERM
) 
  51                 log
.Println("Radio shutting down due to signal", sig
) 
  52                 // Make sure we always stop PTT when program ends 
  57         log
.Println("Config checks out, radio coming online") 
  58         log
.Println("Audio file cache:", config
.CachePath
) 
  60         fileSpecChan 
:= make(chan []protocol
.FileSpec
) 
  61         go filesWorker(config
.CachePath
, fileSpecChan
) 
  63         stop 
:= make(chan bool) 
  64         playlistSpecChan 
:= make(chan []protocol
.PlaylistSpec
) 
  65         go playlistWorker(playlistSpecChan
, stop
) 
  68                 runWebsocket(fileSpecChan
, playlistSpecChan
, stop
) 
  69                 log
.Println("Websocket failed, retry in 30 seconds") 
  70                 time
.Sleep(time
.Second 
* time
.Duration(30)) 
  74 func runWebsocket(fileSpecChan 
chan []protocol
.FileSpec
, playlistSpecChan 
chan []protocol
.PlaylistSpec
, stop 
chan bool) error 
{ 
  75         log
.Println("Establishing websocket connection to:", config
.WebsocketURL()) 
  76         ws
, err 
:= websocket
.Dial(config
.WebsocketURL(), "", config
.ServerURL
) 
  81         auth 
:= protocol
.AuthenticateMessage
{ 
  85         msg
, _ 
:= json
.Marshal(auth
) 
  87         if _
, err 
:= ws
.Write(msg
); err 
!= nil { 
  90         statusCollector
.Websocket 
<- ws
 
  92         buf 
:= make([]byte, 16384) 
  95                 n
, err 
:= ws
.Read(buf
) 
  97                         log
.Println("Lost websocket to server") 
 100                 // Ignore any massively oversize messages 
 109                 t
, msg
, err 
:= protocol
.ParseMessage(buf
[:n
]) 
 111                         log
.Println("Message parse error", err
) 
 115                 if t 
== protocol
.FilesType 
{ 
 116                         filesMsg 
:= msg
.(protocol
.FilesMessage
) 
 117                         fileSpecChan 
<- filesMsg
.Files
 
 120                 if t 
== protocol
.PlaylistsType 
{ 
 121                         playlistsMsg 
:= msg
.(protocol
.PlaylistsMessage
) 
 122                         playlistSpecChan 
<- playlistsMsg
.Playlists
 
 125                 if t 
== protocol
.StopType 
{ 
 131 func filesWorker(cachePath 
string, ch 
chan []protocol
.FileSpec
) { 
 132         machine 
:= NewFilesMachine(cachePath
) 
 133         isDownloading 
:= false 
 134         downloadResult 
:= make(chan error
) 
 135         var timer 
*time
.Timer
 
 138                 var timerCh 
<-chan time
.Time 
= nil 
 145                         log
.Println("Received new file specs", specs
) 
 146                         machine
.UpdateSpecs(specs
) 
 149                 case err 
:= <-downloadResult
: 
 150                         isDownloading 
= false 
 151                         machine
.RefreshMissing() 
 154                                 if !machine
.IsCacheComplete() { 
 155                                         timer 
= time
.NewTimer(30 * time
.Second
) 
 158                                 if !machine
.IsCacheComplete() { 
 159                                         timer 
= time
.NewTimer(10 * time
.Millisecond
) 
 167                 if doNext 
&& !isDownloading 
&& !machine
.IsCacheComplete() { 
 168                         next 
:= machine
.NextFile() 
 170                         go machine
.DownloadSingle(next
, downloadResult
) 
 175 func playlistWorker(ch 
<-chan []protocol
.PlaylistSpec
, stop 
<-chan bool) { 
 176         var specs 
[]protocol
.PlaylistSpec
 
 178         playbackFinished 
:= make(chan error
) 
 179         cancel 
:= make(chan bool) 
 181         var timer 
*time
.Timer
 
 184                 var timerCh 
<-chan time
.Time 
= nil 
 191                         log
.Println("Received new playlist specs", specs
) 
 193                 case <-playbackFinished
: 
 196                         cancel 
= make(chan bool) 
 200                         for _
, v 
:= range specs 
{ 
 202                                         go playPlaylist(v
, playbackFinished
, cancel
) 
 207                                 log
.Println("Cancelling playlist in progress") 
 212                 if doNext 
&& !isPlaying 
{ 
 215                         loc
, err 
:= time
.LoadLocation(config
.TimeZone
) 
 219                         var soonestTime time
.Time
 
 220                         for _
, v 
:= range specs 
{ 
 221                                 t
, err 
:= time
.ParseInLocation(protocol
.StartTimeFormat
, v
.StartTime
, loc
) 
 223                                         log
.Println("Error parsing start time", err
) 
 226                                 if t
.Before(time
.Now()) { 
 229                                 if !found || t
.Before(soonestTime
) { 
 236                                 duration 
:= soonestTime
.Sub(time
.Now()) 
 237                                 log
.Println("Next playlist will be id", nextId
, "in", duration
.Seconds(), "seconds") 
 238                                 timer 
= time
.NewTimer(duration
) 
 240                                 log
.Println("No future playlists") 
 246 func playPlaylist(playlist protocol
.PlaylistSpec
, playbackFinished 
chan<- error
, cancel 
<-chan bool) { 
 247         startTime 
:= time
.Now() 
 248         log
.Println("Beginning playback of playlist", playlist
.Name
) 
 250         for _
, p 
:= range playlist
.Entries 
{ 
 252                 var duration time
.Duration
 
 254                         duration 
= time
.Second 
* time
.Duration(p
.DelaySeconds
) 
 256                         duration 
= time
.Until(startTime
.Add(time
.Second 
* time
.Duration(p
.DelaySeconds
))) 
 258                 statusCollector
.PlaylistBeginDelay 
<- BeginDelayStatus
{ 
 259                         Playlist
: playlist
.Name
, 
 260                         Seconds
:  int(duration
.Seconds()), 
 261                         Filename
: p
.Filename
, 
 264                 case <-time
.After(duration
): 
 269                 statusCollector
.PlaylistBeginWaitForChannel 
<- BeginWaitForChannelStatus
{ 
 270                         Playlist
: playlist
.Name
, 
 271                         Filename
: p
.Filename
, 
 273                 cos
.WaitForChannelClear() 
 276                 statusCollector
.PlaylistBeginPlayback 
<- BeginPlaybackStatus
{ 
 277                         Playlist
: playlist
.Name
, 
 278                         Filename
: p
.Filename
, 
 281                 f
, err 
:= os
.Open(filepath
.Join(config
.CachePath
, p
.Filename
)) 
 283                         log
.Println("Couldn't open file for playlist", p
.Filename
) 
 286                 log
.Println("Playing file", p
.Filename
) 
 287                 l 
:= strings
.ToLower(p
.Filename
) 
 288                 var streamer beep
.StreamSeekCloser
 
 289                 var format beep
.Format
 
 290                 if strings
.HasSuffix(l
, ".mp3") { 
 291                         streamer
, format
, err 
= mp3
.Decode(f
) 
 292                 } else if strings
.HasSuffix(l
, ".wav") { 
 293                         streamer
, format
, err 
= wav
.Decode(f
) 
 295                         log
.Println("Unrecognised file extension (.wav and .mp3 supported), moving on") 
 298                         log
.Println("Could not decode media file", err
) 
 301                 defer streamer
.Close() 
 303                 done 
:= make(chan bool) 
 305                 if format
.SampleRate 
!= sampleRate 
{ 
 306                         resampled 
:= beep
.Resample(4, format
.SampleRate
, sampleRate
, streamer
) 
 307                         speaker
.Play(beep
.Seq(resampled
, beep
.Callback(func() { 
 311                         speaker
.Play(beep
.Seq(streamer
, beep
.Callback(func() { 
 324         log
.Println("Playlist finished", playlist
.Name
) 
 325         statusCollector
.PlaylistBeginIdle 
<- true 
 326         playbackFinished 
<- nil