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.0.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
.StartTimeFormat
, v
.StartTime
, loc
)
232 log
.Println("Error parsing start time", err
)
235 if t
.Before(time
.Now()) {
238 if !found || t
.Before(soonestTime
) {
245 duration
:= time
.Until(soonestTime
)
246 log
.Println("Next playlist will be id", nextId
, "in", duration
.Seconds(), "seconds")
247 timer
= time
.NewTimer(duration
)
249 log
.Println("No future playlists")
255 func playPlaylist(playlist protocol
.PlaylistSpec
, playbackFinished
chan<- error
, cancel
<-chan bool) {
256 startTime
:= time
.Now()
257 log
.Println("Beginning playback of playlist", playlist
.Name
)
259 for _
, p
:= range playlist
.Entries
{
261 var duration time
.Duration
263 duration
= time
.Second
* time
.Duration(p
.DelaySeconds
)
265 duration
= time
.Until(startTime
.Add(time
.Second
* time
.Duration(p
.DelaySeconds
)))
267 statusCollector
.PlaylistBeginDelay
<- BeginDelayStatus
{
268 Playlist
: playlist
.Name
,
269 Seconds
: int(duration
.Seconds()),
270 Filename
: p
.Filename
,
273 case <-time
.After(duration
):
275 log
.Println("Cancelling pre-play delay")
279 statusCollector
.PlaylistBeginWaitForChannel
<- BeginWaitForChannelStatus
{
280 Playlist
: playlist
.Name
,
281 Filename
: p
.Filename
,
283 cos
.WaitForChannelClear()
286 statusCollector
.PlaylistBeginPlayback
<- BeginPlaybackStatus
{
287 Playlist
: playlist
.Name
,
288 Filename
: p
.Filename
,
290 f
, err
:= os
.Open(filepath
.Join(config
.CachePath
, p
.Filename
))
292 log
.Println("Couldn't open file for playlist", p
.Filename
)
295 log
.Println("Playing file", p
.Filename
)
296 l
:= strings
.ToLower(p
.Filename
)
297 var streamer beep
.StreamSeekCloser
298 var format beep
.Format
299 if strings
.HasSuffix(l
, ".mp3") {
300 streamer
, format
, err
= mp3
.Decode(f
)
301 } else if strings
.HasSuffix(l
, ".wav") {
302 streamer
, format
, err
= wav
.Decode(f
)
304 log
.Println("Unrecognised file extension (.wav and .mp3 supported), moving on")
307 log
.Println("Could not decode media file", err
)
310 defer streamer
.Close()
312 done
:= make(chan bool)
313 log
.Println("PTT on for playback")
316 if format
.SampleRate
!= sampleRate
{
317 log
.Println("Configuring resampler for audio provided at sample rate", format
.SampleRate
)
318 resampled
:= beep
.Resample(4, format
.SampleRate
, sampleRate
, streamer
)
319 log
.Println("Playing resampled audio")
320 speaker
.Play(beep
.Seq(resampled
, beep
.Callback(func() {
324 log
.Println("Playing audio at native sample rate")
325 speaker
.Play(beep
.Seq(streamer
, beep
.Callback(func() {
332 log
.Println("Audio playback complete")
334 log
.Println("Disengaging PTT and aborting playlist playback")
338 log
.Println("PTT off since audio file has finished")
341 log
.Println("Playlist finished", playlist
.Name
)
342 statusCollector
.PlaylistBeginIdle
<- true
343 playbackFinished
<- nil