This commit is contained in:
Daniel 2024-08-04 23:05:23 -04:00
parent 962ce282f7
commit 358b7c8754
6 changed files with 381 additions and 3 deletions

59
cmd/calls/audio.go Normal file
View file

@ -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
}

View file

@ -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:

7
go.mod
View file

@ -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
)

16
go.sum
View file

@ -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=

107
internal/mpg123/decode.go Normal file
View file

@ -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()
}

189
internal/mpg123/mpg123.go Normal file
View file

@ -0,0 +1,189 @@
// mpg123.go contains all bindings to the C library
package mpg123
/*
#include <stdlib.h>
#include <mpg123.h>
#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
}