15 "code.octet-stream.net/broadcaster/internal/protocol"
16 "github.com/gopxl/beep/v2"
17 "github.com/gopxl/beep/v2/mp3"
18 "github.com/gopxl/beep/v2/speaker"
19 "github.com/gopxl/beep/v2/wav"
20 "golang.org/x/net/websocket"
23 const version
= "v1.2.0"
24 const sampleRate
= 44100
26 var config RadioConfig
= NewRadioConfig()
29 configFlag
:= flag
.String("c", "", "path to configuration file")
30 versionFlag
:= flag
.Bool("v", false, "print version and exit")
34 fmt
.Println("Broadcaster Radio", version
)
37 if *configFlag
== "" {
38 log
.Fatal("must specify a configuration file with -c")
41 log
.Println("Broadcaster Radio", version
, "starting up")
42 config
.LoadFromFile(*configFlag
)
43 statusCollector
.Config
<- config
45 playbackSampleRate
:= beep
.SampleRate(sampleRate
)
46 speaker
.Init(playbackSampleRate
, playbackSampleRate
.N(time
.Second
/10))
48 if config
.PTTPin
!= -1 {
49 InitRaspberryPiPTT(config
.PTTPin
, config
.GpioDevice
)
51 if config
.COSPin
!= -1 {
52 InitRaspberryPiCOS(config
.COSPin
, config
.GpioDevice
)
55 sig
:= make(chan os
.Signal
, 1)
56 signal
.Notify(sig
, syscall
.SIGINT
, syscall
.SIGTERM
)
59 log
.Println("Radio shutting down due to signal:", sig
)
60 // Make sure we always stop PTT when program ends
65 log
.Println("Config checks out, radio coming online")
66 log
.Println("Audio file cache:", config
.CachePath
)
68 fileSpecChan
:= make(chan []protocol
.FileSpec
)
69 go filesWorker(config
.CachePath
, fileSpecChan
)
71 stop
:= make(chan bool)
72 playlistSpecChan
:= make(chan []protocol
.PlaylistSpec
)
73 go playlistWorker(playlistSpecChan
, stop
)
76 runWebsocket(fileSpecChan
, playlistSpecChan
, stop
)
77 log
.Println("Websocket failed, retry in 30 seconds")
78 time
.Sleep(time
.Second
* time
.Duration(30))
82 func runWebsocket(fileSpecChan
chan []protocol
.FileSpec
, playlistSpecChan
chan []protocol
.PlaylistSpec
, stop
chan bool) error
{
83 log
.Println("Establishing websocket connection to:", config
.WebsocketURL())
84 ws
, err
:= websocket
.Dial(config
.WebsocketURL(), "", config
.ServerURL
)
89 auth
:= protocol
.AuthenticateMessage
{
93 msg
, _
:= json
.Marshal(auth
)
95 if _
, err
:= ws
.Write(msg
); err
!= nil {
98 statusCollector
.Websocket
<- ws
100 buf
:= make([]byte, 16384)
103 n
, err
:= ws
.Read(buf
)
105 log
.Println("Lost websocket to server")
108 // Ignore any massively oversize messages
117 t
, msg
, err
:= protocol
.ParseMessage(buf
[:n
])
119 log
.Println("Message parse error", err
)
123 if t
== protocol
.FilesType
{
124 filesMsg
:= msg
.(protocol
.FilesMessage
)
125 fileSpecChan
<- filesMsg
.Files
128 if t
== protocol
.PlaylistsType
{
129 playlistsMsg
:= msg
.(protocol
.PlaylistsMessage
)
130 playlistSpecChan
<- playlistsMsg
.Playlists
133 if t
== protocol
.StopType
{
134 log
.Println("Received stop transmission message from server")
140 func filesWorker(cachePath
string, ch
chan []protocol
.FileSpec
) {
141 machine
:= NewFilesMachine(cachePath
)
142 isDownloading
:= false
143 downloadResult
:= make(chan error
)
144 var timer
*time
.Timer
147 var timerCh
<-chan time
.Time
= nil
154 log
.Println("Received new file specs", specs
)
155 machine
.UpdateSpecs(specs
)
158 case err
:= <-downloadResult
:
159 isDownloading
= false
160 machine
.RefreshMissing()
163 if !machine
.IsCacheComplete() {
164 timer
= time
.NewTimer(30 * time
.Second
)
167 if !machine
.IsCacheComplete() {
168 timer
= time
.NewTimer(10 * time
.Millisecond
)
176 if doNext
&& !isDownloading
&& !machine
.IsCacheComplete() {
177 next
:= machine
.NextFile()
179 go machine
.DownloadSingle(next
, downloadResult
)
184 func playlistWorker(ch
<-chan []protocol
.PlaylistSpec
, stop
<-chan bool) {
185 var specs
[]protocol
.PlaylistSpec
187 playbackFinished
:= make(chan error
)
188 cancel
:= make(chan bool)
190 var timer
*time
.Timer
193 var timerCh
<-chan time
.Time
= nil
200 log
.Println("Received new playlist specs", specs
)
202 case <-playbackFinished
:
205 cancel
= make(chan bool)
209 for _
, v
:= range specs
{
211 go playPlaylist(v
, playbackFinished
, cancel
)
216 log
.Println("Cancelling playlist in progress")
221 if doNext
&& !isPlaying
{
224 loc
, err
:= time
.LoadLocation(config
.TimeZone
)
228 var soonestTime time
.Time
229 for _
, v
:= range specs
{
230 t
, err
:= time
.ParseInLocation(protocol
.StartTimeFormatSecs
, v
.StartTime
, loc
)
232 t
, err
= time
.ParseInLocation(protocol
.StartTimeFormat
, v
.StartTime
, loc
)
235 log
.Println("Error parsing start time", err
)
238 if t
.Before(time
.Now()) {
241 if !found || t
.Before(soonestTime
) {
248 duration
:= time
.Until(soonestTime
)
249 log
.Println("Next playlist will be id", nextId
, "in", duration
.Seconds(), "seconds")
250 timer
= time
.NewTimer(duration
)
252 log
.Println("No future playlists")
258 func playPlaylist(playlist protocol
.PlaylistSpec
, playbackFinished
chan<- error
, cancel
<-chan bool) {
259 startTime
:= time
.Now()
260 log
.Println("Beginning playback of playlist", playlist
.Name
)
262 for _
, p
:= range playlist
.Entries
{
264 var duration time
.Duration
266 duration
= time
.Second
* time
.Duration(p
.DelaySeconds
)
268 duration
= time
.Until(startTime
.Add(time
.Second
* time
.Duration(p
.DelaySeconds
)))
270 statusCollector
.PlaylistBeginDelay
<- BeginDelayStatus
{
271 Playlist
: playlist
.Name
,
272 Seconds
: int(duration
.Seconds()),
273 Filename
: p
.Filename
,
276 case <-time
.After(duration
):
278 log
.Println("Cancelling pre-play delay")
282 statusCollector
.PlaylistBeginWaitForChannel
<- BeginWaitForChannelStatus
{
283 Playlist
: playlist
.Name
,
284 Filename
: p
.Filename
,
286 cos
.WaitForChannelClear()
289 statusCollector
.PlaylistBeginPlayback
<- BeginPlaybackStatus
{
290 Playlist
: playlist
.Name
,
291 Filename
: p
.Filename
,
293 f
, err
:= os
.Open(filepath
.Join(config
.CachePath
, p
.Filename
))
295 log
.Println("Couldn't open file for playlist", p
.Filename
)
298 log
.Println("Playing file", p
.Filename
)
299 l
:= strings
.ToLower(p
.Filename
)
300 var streamer beep
.StreamSeekCloser
301 var format beep
.Format
302 if strings
.HasSuffix(l
, ".mp3") {
303 streamer
, format
, err
= mp3
.Decode(f
)
304 } else if strings
.HasSuffix(l
, ".wav") {
305 streamer
, format
, err
= wav
.Decode(f
)
307 log
.Println("Unrecognised file extension (.wav and .mp3 supported), moving on")
310 log
.Println("Could not decode media file", err
)
313 defer streamer
.Close()
315 done
:= make(chan bool)
316 log
.Println("PTT on for playback")
319 if format
.SampleRate
!= sampleRate
{
320 log
.Println("Configuring resampler for audio provided at sample rate", format
.SampleRate
)
321 resampled
:= beep
.Resample(4, format
.SampleRate
, sampleRate
, streamer
)
322 log
.Println("Playing resampled audio")
323 speaker
.Play(beep
.Seq(resampled
, beep
.Callback(func() {
327 log
.Println("Playing audio at native sample rate")
328 speaker
.Play(beep
.Seq(streamer
, beep
.Callback(func() {
336 log
.Println("Audio playback complete")
338 log
.Println("Playlist aborting as requested")
342 log
.Println("PTT off")
348 log
.Println("Playlist finished", playlist
.Name
)
349 statusCollector
.PlaylistBeginIdle
<- true
350 playbackFinished
<- nil