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