From 358b7c8754209dc655e7d88b44dd77a7ec47b595 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 4 Aug 2024 23:05:23 -0400 Subject: [PATCH] broken --- cmd/calls/audio.go | 59 ++++++++++++ cmd/calls/main.go | 6 ++ go.mod | 7 +- go.sum | 16 +++- internal/mpg123/decode.go | 107 +++++++++++++++++++++ internal/mpg123/mpg123.go | 189 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 381 insertions(+), 3 deletions(-) create mode 100644 cmd/calls/audio.go create mode 100644 internal/mpg123/decode.go create mode 100644 internal/mpg123/mpg123.go diff --git a/cmd/calls/audio.go b/cmd/calls/audio.go new file mode 100644 index 0000000..d30021b --- /dev/null +++ b/cmd/calls/audio.go @@ -0,0 +1,59 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "time" + + "github.com/gopxl/beep" + "github.com/gopxl/beep/mp3" + "github.com/gopxl/beep/speaker" + "github.com/gopxl/beep/wav" +) + +type Player struct { + rate beep.SampleRate +} + +func NewPlayer() *Player { + p := &Player{} + + return p +} + +func (p *Player) initSpeaker(rate beep.SampleRate) { + if p.rate != rate { + speaker.Init(rate, rate.N(time.Second/10)) + } +} + +func (p *Player) Play(audio []byte, mimeType string) error { + var streamer beep.StreamCloser + var err error + var format beep.Format + r := io.NopCloser(bytes.NewBuffer(audio)) + switch mimeType { + case "audio/mpeg": + streamer, format, err = mp3.Decode(r) + if err != nil { + return err + } + defer streamer.Close() + p.initSpeaker(format.SampleRate) + + case "audio/wav": + streamer, format, err = wav.Decode(r) + if err != nil { + return err + } + defer streamer.Close() + p.initSpeaker(format.SampleRate) + default: + return fmt.Errorf("unknown format %s", mimeType) + } + + speaker.Play(streamer) + + return nil +} diff --git a/cmd/calls/main.go b/cmd/calls/main.go index 8eb42af..e4f1064 100644 --- a/cmd/calls/main.go +++ b/cmd/calls/main.go @@ -30,6 +30,7 @@ func main() { interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) + play := NewPlayer() loginForm := url.Values{} loginForm.Add("username", *username) @@ -101,6 +102,11 @@ func main() { switch v := m.ToClientMessage.(type) { case *pb.Message_Call: log.Printf("call tg %d", v.Call.Talkgroup) + err := play.Play(v.Call.Audio, v.Call.AudioType) + if err != nil { + log.Println(err) + os.WriteFile("failed.mp3", v.Call.Audio, 0644) + } case *pb.Message_Notification: log.Println(v.Notification.Msg) default: diff --git a/go.mod b/go.mod index 47d9e81..1eec3d5 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/go-chi/render v1.0.3 github.com/golang-migrate/migrate/v4 v4.17.1 github.com/google/uuid v1.4.0 + github.com/gopxl/beep v1.4.1 github.com/gorilla/websocket v1.5.3 github.com/jackc/pgx/v5 v5.6.0 github.com/rs/zerolog v1.33.0 @@ -24,7 +25,10 @@ require ( github.com/ajg/form v1.5.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/ebitengine/oto/v3 v3.1.0 // indirect + github.com/ebitengine/purego v0.7.1 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/hajimehoshi/go-mp3 v0.3.4 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -41,11 +45,12 @@ require ( github.com/lestrrat-go/option v1.0.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/spf13/pflag v1.0.5 // indirect go.uber.org/atomic v1.7.0 // indirect golang.org/x/sync v0.5.0 // indirect - golang.org/x/sys v0.18.0 // indirect + golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index 1e3ee8b..9a32e49 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,10 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/oto/v3 v3.1.0 h1:9tChG6rizyeR2w3vsygTTTVVJ9QMMyu00m2yBOCch6U= +github.com/ebitengine/oto/v3 v3.1.0/go.mod h1:IK1QTnlfZK2GIB6ziyECm433hAdTaPpOsGMLhEyEGTg= +github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA= +github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE= github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= @@ -45,8 +49,13 @@ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopxl/beep v1.4.1 h1:WqNs9RsDAhG9M3khMyc1FaVY50dTdxG/6S6a3qsUHqE= +github.com/gopxl/beep v1.4.1/go.mod h1:A1dmiUkuY8kxsvcNJNUBIEcchmiP6eUyCHSxpXl0YO0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68= +github.com/hajimehoshi/go-mp3 v0.3.4/go.mod h1:fRtZraRFcWb0pu7ok0LqyFhCUrPeMsGRSVop0eemFmo= +github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -95,6 +104,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw= +github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -128,11 +139,12 @@ golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= diff --git a/internal/mpg123/decode.go b/internal/mpg123/decode.go new file mode 100644 index 0000000..6b5830d --- /dev/null +++ b/internal/mpg123/decode.go @@ -0,0 +1,107 @@ +// Package mp3 implements audio data decoding in MP3 format. +package mpg123 + +import ( + "fmt" + "io" + + "github.com/gopxl/beep" + "github.com/pkg/errors" +) + +const ( + gomp3NumChannels = 2 + gomp3Precision = 2 + gomp3BytesPerFrame = gomp3NumChannels * gomp3Precision +) + +// Decode takes a ReadCloser containing audio data in MP3 format and returns a StreamSeekCloser, +// which streams that audio. The Seek method will panic if rc is not io.Seeker. +// +// Do not close the supplied ReadSeekCloser, instead, use the Close method of the returned +// StreamSeekCloser when you want to release the resources. +func Decode(rc io.ReadCloser) (s beep.StreamCloser, format beep.Format, err error) { + defer func() { + if err != nil { + err = errors.Wrap(err, "mp3") + } + }() + d, err := NewDecoder("") + if err != nil { + return nil, beep.Format{}, err + } + + d.Format(8000, 2, ENC_FLOAT_64) + err = d.OpenFeed() + if err != nil { + return nil, beep.Format{}, err + } + // get 4k of file + buf := make([]byte, 4096) + for { + n, err := rc.Read(buf) + if err != nil && err != io.EOF { + return nil, beep.Format{}, err + } + if n == 0 { + break + } + err = d.Feed(buf) + if err != nil { + return nil, beep.Format{}, err + } + } + rate, channels, enc := d.GetFormat() + fmt.Printf("rate %d chan %d enc %d\n", rate, channels, enc) + + format = beep.Format{ + SampleRate: beep.SampleRate(8000), + NumChannels: 2, + Precision: 4, + } + return &decoder{rc, d, format, 0, nil}, format, nil +} + +type decoder struct { + closer io.Closer + d *Decoder + f beep.Format + pos int + err error +} + +func (d *decoder) Stream(samples [][2]float64) (n int, ok bool) { + if d.err != nil { + return 0, false + } + var tmp [gomp3BytesPerFrame]byte + for i := range samples { + dn, err := d.d.Read(tmp[:]) + if dn == len(tmp) { + samples[i], _ = d.f.DecodeSigned(tmp[:]) + d.pos += dn + n++ + ok = true + } + if err == io.EOF { + break + } + if err != nil { + d.err = errors.Wrap(err, "mp3") + break + } + } + return n, ok +} + +func (d *decoder) Err() error { + return d.err +} + +func (d *decoder) Close() error { + err := d.closer.Close() + if err != nil { + return errors.Wrap(err, "mp3") + } + return d.d.Close() +} diff --git a/internal/mpg123/mpg123.go b/internal/mpg123/mpg123.go new file mode 100644 index 0000000..0d8211d --- /dev/null +++ b/internal/mpg123/mpg123.go @@ -0,0 +1,189 @@ +// mpg123.go contains all bindings to the C library + +package mpg123 + +/* +#include +#include +#cgo LDFLAGS: -lmpg123 + +int do_mpg123_read(mpg123_handle *mh, void *outmemory, size_t outmemsize, size_t *done) { + return mpg123_read(mh, outmemory, outmemsize, done); +} +*/ +import "C" + +import ( + "errors" + "fmt" + "os" + "unsafe" +) + +var EOF = errors.New("EOF") + +// All output encoding formats supported by mpg123 +const ( + ENC_8 = C.MPG123_ENC_8 + ENC_16 = C.MPG123_ENC_16 + ENC_24 = C.MPG123_ENC_24 + ENC_32 = C.MPG123_ENC_32 + ENC_SIGNED = C.MPG123_ENC_SIGNED + ENC_FLOAT = C.MPG123_ENC_FLOAT + ENC_SIGNED_8 = C.MPG123_ENC_SIGNED_8 + ENC_UNSIGNED_8 = C.MPG123_ENC_UNSIGNED_8 + ENC_ULAW_8 = C.MPG123_ENC_ULAW_8 + ENC_ALAW_8 = C.MPG123_ENC_ALAW_8 + ENC_SIGNED_16 = C.MPG123_ENC_SIGNED_16 + ENC_UNSIGNED_16 = C.MPG123_ENC_UNSIGNED_16 + ENC_SIGNED_24 = C.MPG123_ENC_SIGNED_24 + ENC_UNSIGNED_24 = C.MPG123_ENC_UNSIGNED_24 + ENC_SIGNED_32 = C.MPG123_ENC_SIGNED_32 + ENC_UNSIGNED_32 = C.MPG123_ENC_UNSIGNED_32 + ENC_FLOAT_32 = C.MPG123_ENC_FLOAT_32 + ENC_FLOAT_64 = C.MPG123_ENC_FLOAT_64 + ENC_ANY = C.MPG123_ENC_ANY +) + +// Contains a handle for and mpg123 decoder instance +type Decoder struct { + handle *C.mpg123_handle +} + +// init initializes the mpg123 library when package is loaded +func init() { + err := C.mpg123_init() + if err != C.MPG123_OK { + //return fmt.Errorf("error initializing mpg123") + panic("failed to initialize mpg123") + } + //return nil +} + +/////////////////////////// +// DECODER INSTANCE CODE // +/////////////////////////// + +// NewDecoder creates a new mpg123 decoder instance +func NewDecoder(decoder string) (*Decoder, error) { + var err C.int + var mh *C.mpg123_handle + if decoder != "" { + mh = C.mpg123_new(nil, &err) + } else { + cdecoder := C.CString(decoder) + defer C.free(unsafe.Pointer(cdecoder)) + mh = C.mpg123_new(cdecoder, &err) + } + if mh == nil { + errstring := C.mpg123_plain_strerror(err) + err := C.GoString(errstring) + C.free(unsafe.Pointer(errstring)) + return nil, fmt.Errorf("error initializing mpg123 decoder: %s", err) + } + dec := new(Decoder) + dec.handle = mh + return dec, nil +} + +// Delete frees an mpg123 decoder instance +func (d *Decoder) Delete() { + C.mpg123_delete(d.handle) +} + +// returns a string containing the most recent error message corresponding to +// an mpg123 decoder instance +func (d *Decoder) strerror() string { + return C.GoString(C.mpg123_strerror(d.handle)) +} + +//////////////////////// +// OUTPUT FORMAT CODE // +//////////////////////// + +// FormatNone disables all decoder output formats (used to specifying supported formats) +func (d *Decoder) FormatNone() { + C.mpg123_format_none(d.handle) +} + +// FromatAll enables all decoder output formats (this is the default setting) +func (d *Decoder) FormatAll() { + C.mpg123_format_all(d.handle) +} + +// GetFormat returns current output format +func (d *Decoder) GetFormat() (rate int64, channels int, encoding int) { + var cRate C.long + var cChans, cEnc C.int + C.mpg123_getformat(d.handle, &cRate, &cChans, &cEnc) + return int64(cRate), int(cChans), int(cEnc) +} + +// Format sets the audio output format for decoder +func (d *Decoder) Format(rate int64, channels int, encodings int) { + C.mpg123_format(d.handle, C.long(rate), C.int(channels), C.int(encodings)) +} + +///////////////////////////// +// INPUT AND DECODING CODE // +///////////////////////////// + +// Open initializes a decoder for an mp3 file using a filename +func (d *Decoder) Open(file string) error { + cfile := C.CString(file) + defer C.free(unsafe.Pointer(cfile)) + err := C.mpg123_open(d.handle, cfile) + if err != C.MPG123_OK { + return fmt.Errorf("error opening %s: %s", file, d.strerror()) + } + return nil +} + +// OpenFile binds to an fd from an open *os.File for decoding +func (d *Decoder) OpenFile(f *os.File) error { + err := C.mpg123_open_fd(d.handle, C.int(f.Fd())) + if err != C.MPG123_OK { + return fmt.Errorf("error attaching file: %s", d.strerror()) + } + return nil +} + +// OpenFeed prepares a decoder for direct feeding via Feed(..) +func (d *Decoder) OpenFeed() error { + err := C.mpg123_open_feed(d.handle) + if err != C.MPG123_OK { + return fmt.Errorf("mpg123 error: %s", d.strerror()) + } + return nil +} + +// Close closes an input file if one was opened by mpg123 +func (d *Decoder) Close() error { + err := C.mpg123_close(d.handle) + if err != C.MPG123_OK { + return fmt.Errorf("mpg123 error: %s", d.strerror()) + } + return nil +} + +// Read decodes data and into buf and returns number of bytes decoded. +func (d *Decoder) Read(buf []byte) (int, error) { + var done C.size_t + err := C.do_mpg123_read(d.handle, (unsafe.Pointer)(&buf[0]), C.size_t(len(buf)), &done) + if err == C.MPG123_DONE { + return int(done), EOF + } + if err != C.MPG123_OK { + return int(done), fmt.Errorf("mpg123 error: %s", d.strerror()) + } + return int(done), nil +} + +// Feed provides data bytes into the decoder +func (d *Decoder) Feed(buf []byte) error { + err := C.mpg123_feed(d.handle, (*C.uchar)(unsafe.Pointer(&buf[0])), C.size_t(len(buf))) + if err != C.MPG123_OK { + return fmt.Errorf("mpg123 error: %s", d.strerror()) + } + return nil +}