4 "code.octet-stream.net/broadcaster/internal/protocol"
8 "github.com/gopxl/beep/v2"
9 "github.com/gopxl/beep/v2/mp3"
10 "github.com/gopxl/beep/v2/speaker"
11 "github.com/gopxl/beep/v2/wav"
12 "golang.org/x/net/websocket"
22 const version
= "v1.0.0"
23 const sampleRate
= 44100
25 var config RadioConfig
= NewRadioConfig()
28 configFlag
:= flag
.String("c", "", "path to configuration file")
29 versionFlag
:= flag
.Bool("v", false, "print version and exit")
33 fmt
.Println("Broadcaster Radio", version
)
36 if *configFlag
== "" {
37 log
.Fatal("must specify a configuration file with -c")
40 log
.Println("Broadcaster Radio", version
, "starting up")
41 config
.LoadFromFile(*configFlag
)
42 statusCollector
.Config
<- config
44 playbackSampleRate
:= beep
.SampleRate(sampleRate
)
45 speaker
.Init(playbackSampleRate
, playbackSampleRate
.N(time
.Second
/10))
47 if config
.PTTPin
!= -1 {
48 InitRaspberryPiPTT(config
.PTTPin
, config
.GpioDevice
)
50 if config
.COSPin
!= -1 {
51 InitRaspberryPiCOS(config
.COSPin
, config
.GpioDevice
)
54 sig
:= make(chan os
.Signal
, 1)
55 signal
.Notify(sig
, syscall
.SIGINT
, syscall
.SIGTERM
)
58 log
.Println("Radio shutting down due to signal", sig
)
59 // Make sure we always stop PTT when program ends
64 log
.Println("Config checks out, radio coming online")
65 log
.Println("Audio file cache:", config
.CachePath
)
67 fileSpecChan
:= make(chan []protocol
.FileSpec
)
68 go filesWorker(config
.CachePath
, fileSpecChan
)
70 stop
:= make(chan bool)
71 playlistSpecChan
:= make(chan []protocol
.PlaylistSpec
)
72 go playlistWorker(playlistSpecChan
, stop
)
75 runWebsocket(fileSpecChan
, playlistSpecChan
, stop
)
76 log
.Println("Websocket failed, retry in 30 seconds")
77 time
.Sleep(time
.Second
* time
.Duration(30))
81 func runWebsocket(fileSpecChan
chan []protocol
.FileSpec
, playlistSpecChan
chan []protocol
.PlaylistSpec
, stop
chan bool) error
{
82 log
.Println("Establishing websocket connection to:", config
.WebsocketURL())
83 ws
, err
:= websocket
.Dial(config
.WebsocketURL(), "", config
.ServerURL
)
88 auth
:= protocol
.AuthenticateMessage
{
92 msg
, _
:= json
.Marshal(auth
)
94 if _
, err
:= ws
.Write(msg
); err
!= nil {
97 statusCollector
.Websocket
<- ws
99 buf
:= make([]byte, 16384)
102 n
, err
:= ws
.Read(buf
)
104 log
.Println("Lost websocket to server")
107 // Ignore any massively oversize messages
116 t
, msg
, err
:= protocol
.ParseMessage(buf
[:n
])
118 log
.Println("Message parse error", err
)
122 if t
== protocol
.FilesType
{
123 filesMsg
:= msg
.(protocol
.FilesMessage
)
124 fileSpecChan
<- filesMsg
.Files
127 if t
== protocol
.PlaylistsType
{
128 playlistsMsg
:= msg
.(protocol
.PlaylistsMessage
)
129 playlistSpecChan
<- playlistsMsg
.Playlists
132 if t
== protocol
.StopType
{
138 func filesWorker(cachePath
string, ch
chan []protocol
.FileSpec
) {
139 machine
:= NewFilesMachine(cachePath
)
140 isDownloading
:= false
141 downloadResult
:= make(chan error
)
142 var timer
*time
.Timer
145 var timerCh
<-chan time
.Time
= nil
152 log
.Println("Received new file specs", specs
)
153 machine
.UpdateSpecs(specs
)
156 case err
:= <-downloadResult
:
157 isDownloading
= false
158 machine
.RefreshMissing()
161 if !machine
.IsCacheComplete() {
162 timer
= time
.NewTimer(30 * time
.Second
)
165 if !machine
.IsCacheComplete() {
166 timer
= time
.NewTimer(10 * time
.Millisecond
)
174 if doNext
&& !isDownloading
&& !machine
.IsCacheComplete() {
175 next
:= machine
.NextFile()
177 go machine
.DownloadSingle(next
, downloadResult
)
182 func playlistWorker(ch
<-chan []protocol
.PlaylistSpec
, stop
<-chan bool) {
183 var specs
[]protocol
.PlaylistSpec
185 playbackFinished
:= make(chan error
)
186 cancel
:= make(chan bool)
188 var timer
*time
.Timer
191 var timerCh
<-chan time
.Time
= nil
198 log
.Println("Received new playlist specs", specs
)
200 case <-playbackFinished
:
203 cancel
= make(chan bool)
207 for _
, v
:= range specs
{
209 go playPlaylist(v
, playbackFinished
, cancel
)
214 log
.Println("Cancelling playlist in progress")
219 if doNext
&& !isPlaying
{
222 loc
, err
:= time
.LoadLocation(config
.TimeZone
)
226 var soonestTime time
.Time
227 for _
, v
:= range specs
{
228 t
, err
:= time
.ParseInLocation(protocol
.StartTimeFormat
, v
.StartTime
, loc
)
230 log
.Println("Error parsing start time", err
)
233 if t
.Before(time
.Now()) {
236 if !found || t
.Before(soonestTime
) {
243 duration
:= soonestTime
.Sub(time
.Now())
244 log
.Println("Next playlist will be id", nextId
, "in", duration
.Seconds(), "seconds")
245 timer
= time
.NewTimer(duration
)
247 log
.Println("No future playlists")
253 func playPlaylist(playlist protocol
.PlaylistSpec
, playbackFinished
chan<- error
, cancel
<-chan bool) {
254 startTime
:= time
.Now()
255 log
.Println("Beginning playback of playlist", playlist
.Name
)
257 for _
, p
:= range playlist
.Entries
{
259 var duration time
.Duration
261 duration
= time
.Second
* time
.Duration(p
.DelaySeconds
)
263 duration
= time
.Until(startTime
.Add(time
.Second
* time
.Duration(p
.DelaySeconds
)))
265 statusCollector
.PlaylistBeginDelay
<- BeginDelayStatus
{
266 Playlist
: playlist
.Name
,
267 Seconds
: int(duration
.Seconds()),
268 Filename
: p
.Filename
,
271 case <-time
.After(duration
):
276 statusCollector
.PlaylistBeginWaitForChannel
<- BeginWaitForChannelStatus
{
277 Playlist
: playlist
.Name
,
278 Filename
: p
.Filename
,
280 cos
.WaitForChannelClear()
283 statusCollector
.PlaylistBeginPlayback
<- BeginPlaybackStatus
{
284 Playlist
: playlist
.Name
,
285 Filename
: p
.Filename
,
288 f
, err
:= os
.Open(filepath
.Join(config
.CachePath
, p
.Filename
))
290 log
.Println("Couldn't open file for playlist", p
.Filename
)
293 log
.Println("Playing file", p
.Filename
)
294 l
:= strings
.ToLower(p
.Filename
)
295 var streamer beep
.StreamSeekCloser
296 var format beep
.Format
297 if strings
.HasSuffix(l
, ".mp3") {
298 streamer
, format
, err
= mp3
.Decode(f
)
299 } else if strings
.HasSuffix(l
, ".wav") {
300 streamer
, format
, err
= wav
.Decode(f
)
302 log
.Println("Unrecognised file extension (.wav and .mp3 supported), moving on")
305 log
.Println("Could not decode media file", err
)
308 defer streamer
.Close()
310 done
:= make(chan bool)
312 if format
.SampleRate
!= sampleRate
{
313 resampled
:= beep
.Resample(4, format
.SampleRate
, sampleRate
, streamer
)
314 speaker
.Play(beep
.Seq(resampled
, beep
.Callback(func() {
318 speaker
.Play(beep
.Seq(streamer
, beep
.Callback(func() {
331 log
.Println("Playlist finished", playlist
.Name
)
332 statusCollector
.PlaylistBeginIdle
<- true
333 playbackFinished
<- nil