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 playlistSpecChan
:= make(chan []protocol
.PlaylistSpec
)
64 go playlistWorker(playlistSpecChan
)
67 runWebsocket(fileSpecChan
, playlistSpecChan
)
68 log
.Println("Websocket failed, retry in 30 seconds")
69 time
.Sleep(time
.Second
* time
.Duration(30))
73 func runWebsocket(fileSpecChan
chan []protocol
.FileSpec
, playlistSpecChan
chan []protocol
.PlaylistSpec
) error
{
74 log
.Println("Establishing websocket connection to:", config
.WebsocketURL())
75 ws
, err
:= websocket
.Dial(config
.WebsocketURL(), "", config
.ServerURL
)
80 auth
:= protocol
.AuthenticateMessage
{
84 msg
, _
:= json
.Marshal(auth
)
86 if _
, err
:= ws
.Write(msg
); err
!= nil {
89 statusCollector
.Websocket
<- ws
91 buf
:= make([]byte, 16384)
94 n
, err
:= ws
.Read(buf
)
96 log
.Println("Lost websocket to server")
99 // Ignore any massively oversize messages
108 t
, msg
, err
:= protocol
.ParseMessage(buf
[:n
])
110 log
.Println("Message parse error", err
)
114 if t
== protocol
.FilesType
{
115 filesMsg
:= msg
.(protocol
.FilesMessage
)
116 fileSpecChan
<- filesMsg
.Files
119 if t
== protocol
.PlaylistsType
{
120 playlistsMsg
:= msg
.(protocol
.PlaylistsMessage
)
121 playlistSpecChan
<- playlistsMsg
.Playlists
126 func filesWorker(cachePath
string, ch
chan []protocol
.FileSpec
) {
127 machine
:= NewFilesMachine(cachePath
)
128 isDownloading
:= false
129 downloadResult
:= make(chan error
)
130 var timer
*time
.Timer
133 var timerCh
<-chan time
.Time
= nil
140 log
.Println("Received new file specs", specs
)
141 machine
.UpdateSpecs(specs
)
144 case err
:= <-downloadResult
:
145 isDownloading
= false
146 machine
.RefreshMissing()
149 if !machine
.IsCacheComplete() {
150 timer
= time
.NewTimer(30 * time
.Second
)
153 if !machine
.IsCacheComplete() {
154 timer
= time
.NewTimer(10 * time
.Millisecond
)
162 if doNext
&& !isDownloading
&& !machine
.IsCacheComplete() {
163 next
:= machine
.NextFile()
165 go machine
.DownloadSingle(next
, downloadResult
)
170 func playlistWorker(ch
<-chan []protocol
.PlaylistSpec
) {
171 var specs
[]protocol
.PlaylistSpec
173 playbackFinished
:= make(chan error
)
175 var timer
*time
.Timer
178 var timerCh
<-chan time
.Time
= nil
185 log
.Println("Received new playlist specs", specs
)
187 case <-playbackFinished
:
193 for _
, v
:= range specs
{
195 go playPlaylist(v
, playbackFinished
)
200 if doNext
&& !isPlaying
{
203 loc
, err
:= time
.LoadLocation(config
.TimeZone
)
207 var soonestTime time
.Time
208 for _
, v
:= range specs
{
209 t
, err
:= time
.ParseInLocation(protocol
.StartTimeFormat
, v
.StartTime
, loc
)
211 log
.Println("Error parsing start time", err
)
214 if t
.Before(time
.Now()) {
217 if !found || t
.Before(soonestTime
) {
224 duration
:= soonestTime
.Sub(time
.Now())
225 log
.Println("Next playlist will be id", nextId
, "in", duration
.Seconds(), "seconds")
226 timer
= time
.NewTimer(duration
)
228 log
.Println("No future playlists")
234 func playPlaylist(playlist protocol
.PlaylistSpec
, playbackFinished
chan<- error
) {
235 // TODO: possibility of on-demand cancellation
236 startTime
:= time
.Now()
237 log
.Println("Beginning playback of playlist", playlist
.Name
)
238 for _
, p
:= range playlist
.Entries
{
240 var duration time
.Duration
242 duration
= time
.Second
* time
.Duration(p
.DelaySeconds
)
244 duration
= time
.Until(startTime
.Add(time
.Second
* time
.Duration(p
.DelaySeconds
)))
246 statusCollector
.PlaylistBeginDelay
<- BeginDelayStatus
{
247 Playlist
: playlist
.Name
,
248 Seconds
: int(duration
.Seconds()),
249 Filename
: p
.Filename
,
251 <-time
.After(duration
)
253 statusCollector
.PlaylistBeginWaitForChannel
<- BeginWaitForChannelStatus
{
254 Playlist
: playlist
.Name
,
255 Filename
: p
.Filename
,
257 cos
.WaitForChannelClear()
260 statusCollector
.PlaylistBeginPlayback
<- BeginPlaybackStatus
{
261 Playlist
: playlist
.Name
,
262 Filename
: p
.Filename
,
265 f
, err
:= os
.Open(filepath
.Join(config
.CachePath
, p
.Filename
))
267 log
.Println("Couldn't open file for playlist", p
.Filename
)
270 log
.Println("Playing file", p
.Filename
)
271 l
:= strings
.ToLower(p
.Filename
)
272 var streamer beep
.StreamSeekCloser
273 var format beep
.Format
274 if strings
.HasSuffix(l
, ".mp3") {
275 streamer
, format
, err
= mp3
.Decode(f
)
276 } else if strings
.HasSuffix(l
, ".wav") {
277 streamer
, format
, err
= wav
.Decode(f
)
279 log
.Println("Unrecognised file extension (.wav and .mp3 supported), moving on")
282 log
.Println("Could not decode media file", err
)
285 defer streamer
.Close()
287 done
:= make(chan bool)
289 if format
.SampleRate
!= sampleRate
{
290 resampled
:= beep
.Resample(4, format
.SampleRate
, sampleRate
, streamer
)
291 speaker
.Play(beep
.Seq(resampled
, beep
.Callback(func() {
295 speaker
.Play(beep
.Seq(streamer
, beep
.Callback(func() {
303 log
.Println("Playlist finished", playlist
.Name
)
304 statusCollector
.PlaylistBeginIdle
<- true
305 playbackFinished
<- nil