From a0a4537281fe34898e0ab893cb708774e1890360 Mon Sep 17 00:00:00 2001 From: Trek H Date: Mon, 11 Nov 2019 16:47:23 +1030 Subject: [PATCH 1/8] input/audio: audio.Device now implements AVDevice --- go.mod | 1 - go.sum | 38 +++----------------------- input/audio/audio.go | 56 ++++++++++++++++++++++++++++----------- input/audio/audio_test.go | 14 +++++----- 4 files changed, 52 insertions(+), 57 deletions(-) diff --git a/go.mod b/go.mod index bbc7d76e..c0ae53a4 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.13 require ( bitbucket.org/ausocean/iot v1.2.7 - bitbucket.org/ausocean/test v1.6.0 bitbucket.org/ausocean/utils v1.2.10 github.com/BurntSushi/toml v0.3.1 // indirect github.com/Comcast/gots v0.0.0-20190305015453-8d56e473f0f7 diff --git a/go.sum b/go.sum index 1770d71f..bb222988 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,5 @@ bitbucket.org/ausocean/iot v1.2.7 h1:dZgrmVtuXnzHgybDthn0bYgAJms9euTONXBsqsx9g5M= bitbucket.org/ausocean/iot v1.2.7/go.mod h1:aAWgPo2f8sD2OPmxae1E5/iD9+tKY/iW4pcQMQXUvHM= -bitbucket.org/ausocean/test v0.0.0-20190821085226-7a524f2344ba/go.mod h1:MbKtu9Pu8l3hiVGX6ep8S1VwAVY5uCbifCFOYsm914w= bitbucket.org/ausocean/utils v1.2.9/go.mod h1:uXzX9z3PLemyURTMWRhVI8uLhPX4uuvaaO85v2hcob8= bitbucket.org/ausocean/utils v1.2.10 h1:JTS7n+K+0o/FQFWKjdGgA1ElZ4TQu9aHX3wTJXOayXw= bitbucket.org/ausocean/utils v1.2.10/go.mod h1:uXzX9z3PLemyURTMWRhVI8uLhPX4uuvaaO85v2hcob8= @@ -13,50 +12,33 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMx github.com/adrianmo/go-nmea v1.1.1-0.20190109062325-c448653979f7/go.mod h1:HHPxPAm2kmev+61qmkZh7xgZF/7qHtSpsWppip2Ipv8= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= -github.com/cosiner/argv v0.0.0-20170225145430-13bacc38a0a5/go.mod h1:p/NrK5tF6ICIly4qwEDsf6VDirFiWWz0FenfYBwJaKQ= -github.com/cpuguy83/go-md2man v1.0.8/go.mod h1:N6JayAiVKtlHSnuTCeuLSQVs75hb8q+dYQLjr7cDsKY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-audio/aiff v0.0.0-20180403003018-6c3a8a6aff12/go.mod h1:AMSAp6W1zd0koOdX6QDgGIuBDTUvLa2SLQtm7d9eM3c= github.com/go-audio/audio v0.0.0-20180206231410-b697a35b5608/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= github.com/go-audio/audio v0.0.0-20181013203223-7b2a6ca21480 h1:4sGU+UABMMsRJyD+Y2yzMYxq0GJFUsRRESI0P1gZ2ig= github.com/go-audio/audio v0.0.0-20181013203223-7b2a6ca21480/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= github.com/go-audio/wav v0.0.0-20181013172942-de841e69b884 h1:2TaXIaVA4ff/MHHezOj83tCypALTFAcXOImcFWNa3jw= github.com/go-audio/wav v0.0.0-20181013172942-de841e69b884/go.mod h1:UiqzUyfX0zs3pJ/DPyvS5v8sN6s5bXPUDDIVA5v8dks= -github.com/go-delve/delve v1.3.2 h1:K8VjV+Q2YnBYlPq0ctjrvc9h7h03wXszlszzfGW5Tog= -github.com/go-delve/delve v1.3.2/go.mod h1:LLw6qJfIsRK9WcwV2IRRqsdlgrqzOeuGrQOCOIhDpt8= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4/go.mod h1:2RvX5ZjVtsznNZPEt4xwJXNJrM3VTZoQf7V6gk0ysvs= github.com/kidoman/embd v0.0.0-20170508013040-d3d8c0c5c68d/go.mod h1:ACKj9jnzOzj1lw2ETilpFGK7L9dtJhAzT7T1OhAGtRQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattetti/audio v0.0.0-20180912171649-01576cde1f21 h1:Hc1iKlyxNHp3CV59G2E/qabUkHvEwOIJxDK0CJ7CRjA= github.com/mattetti/audio v0.0.0-20180912171649-01576cde1f21/go.mod h1:LlQmBGkOuV/SKzEDXBPKauvN2UqCgzXO2XjecTGj40s= -github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mewkiz/flac v1.0.5 h1:dHGW/2kf+/KZ2GGqSVayNEhL9pluKn/rr/h/QqD9Ogc= github.com/mewkiz/flac v1.0.5/go.mod h1:EHZNU32dMF6alpurYyKHDLYpW1lYpBZ5WrXi/VuNIGs= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v0.0.0-20170413231811-06b906832ed0/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday v0.0.0-20180428102519-11635eb403ff/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/sirupsen/logrus v0.0.0-20180523074243-ea8897e79973/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/spf13/cobra v0.0.0-20170417170307-b6cb39589372/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/pflag v0.0.0-20170417173400-9e4c21054fa1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= @@ -64,7 +46,6 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e h1:3NIzz7weXhh3NToPgbtlQtKiVgerEaG4/nY2skGoGG0= github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e/go.mod h1:CaowXBWOiSGWEpBBV8LoVnQTVPV4ycyviC9IBLj8dRw= github.com/yryz/ds18b20 v0.0.0-20180211073435-3cf383a40624/go.mod h1:MqFju5qeLDFh+S9PqxYT7TEla8xeW7bgGr/69q3oki0= -go.starlark.net v0.0.0-20190702223751-32f345186213/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg= go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= @@ -72,25 +53,12 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/ go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -gocv.io/x/gocv v0.21.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs= -golang.org/x/arch v0.0.0-20171004143515-077ac972c2e4/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8= -golang.org/x/crypto v0.0.0-20180614174826-fd5f17ee7299/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190305064518-30e92a19ae4a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20181120060634-fc4f04983f62/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/input/audio/audio.go b/input/audio/audio.go index 54edb00c..a356ea48 100644 --- a/input/audio/audio.go +++ b/input/audio/audio.go @@ -38,6 +38,8 @@ import ( "bitbucket.org/ausocean/av/codec/adpcm" "bitbucket.org/ausocean/av/codec/codecutil" "bitbucket.org/ausocean/av/codec/pcm" + "bitbucket.org/ausocean/av/device" + "bitbucket.org/ausocean/av/revid/config" "bitbucket.org/ausocean/utils/logger" "bitbucket.org/ausocean/utils/ring" ) @@ -69,7 +71,7 @@ type Device struct { ab alsa.Buffer // ALSA's buffer. rb *ring.Buffer // Our buffer. chunkSize int // This is the number of bytes that will be stored in rb at a time. - *Config // Configuration parameters for this device. + Config // Configuration parameters for this device. } // Config provides parameters used by Device. @@ -91,24 +93,48 @@ type Logger interface { // OpenError is used to determine whether an error has originated from attempting to open a device. type OpenError error -// NewDevice initializes and returns an Device which can be started, read from, and stopped. -func NewDevice(cfg *Config, l Logger) (*Device, error) { - - err := validate(cfg) - if err != nil { - return nil, err - } - +// NewDevice initializes and returns a Device which has its logger set as the given logger. +func NewDevice(l Logger) *Device { d := &Device{ - Config: cfg, - l: l, + l: l, + } + return d +} + +// Set will take a Config struct, check the validity of the relevant fields +// and then performs any configuration necessary. If fields are not valid, +// an error is added to the multiError and a default value is used. +// It then initialises the Device which can then be started, read from, and stopped. +func (d *Device) Set(c config.Config) error { + var errs device.MultiError + if c.SampleRate <= 0 { + errs = append(errs, fmt.Errorf("invalid sample rate: %v", c.SampleRate)) + } + if c.Channels <= 0 { + errs = append(errs, fmt.Errorf("invalid number of channels: %v", c.Channels)) + } + if c.BitDepth <= 0 { + errs = append(errs, fmt.Errorf("invalid bitdepth: %v", c.BitDepth)) + } + if c.RecPeriod <= 0 { + errs = append(errs, fmt.Errorf("invalid recording period: %v", c.RecPeriod)) + } + if !codecutil.IsValid(c.InputCodec) { + errs = append(errs, errors.New("invalid codec")) + } + d.Config = Config{ + SampleRate: c.SampleRate, + Channels: c.Channels, + BitDepth: c.BitDepth, + RecPeriod: c.RecPeriod, + Codec: c.InputCodec, } // Open the requested audio device. - err = d.open() + err := d.open() if err != nil { d.l.Log(logger.Error, pkg+"failed to open device") - return nil, err + return err } // Setup the device to record with desired period. @@ -120,7 +146,7 @@ func NewDevice(cfg *Config, l Logger) (*Device, error) { // Account for resampling. chunkSize = (chunkSize / float64(d.dev.BufferFormat().Rate)) * float64(d.SampleRate) if chunkSize < 1 { - return nil, errors.New("given Config parameters are too small") + return errors.New("given Config parameters are too small") } // Account for codec conversion. @@ -137,7 +163,7 @@ func NewDevice(cfg *Config, l Logger) (*Device, error) { d.mode = paused go d.input() - return d, nil + return nil } // Start will start recording audio and writing to the ringbuffer. diff --git a/input/audio/audio_test.go b/input/audio/audio_test.go index 5389524b..9c5735c5 100644 --- a/input/audio/audio_test.go +++ b/input/audio/audio_test.go @@ -32,23 +32,25 @@ import ( "time" "bitbucket.org/ausocean/av/codec/codecutil" + "bitbucket.org/ausocean/av/revid/config" "bitbucket.org/ausocean/utils/logger" ) func TestDevice(t *testing.T) { // We want to open a device with a standard configuration. - ac := &Config{ + c := config.Config{ SampleRate: 8000, Channels: 1, RecPeriod: 0.3, BitDepth: 16, - Codec: codecutil.ADPCM, + InputCodec: codecutil.ADPCM, } n := 2 // Number of periods to wait while recording. // Create a new audio Device, start, read/lex, and then stop it. - l := logger.New(logger.Debug, os.Stderr, true) - ai, err := NewDevice(ac, l) + l := logger.New(logger.Debug, os.Stderr) + ai := NewDevice(l) + err := ai.Set(c) // If there was an error opening the device, skip this test. if _, ok := err.(OpenError); ok { t.Skip(err) @@ -63,8 +65,8 @@ func TestDevice(t *testing.T) { } chunkSize := ai.ChunkSize() lexer := codecutil.NewByteLexer(&chunkSize) - go lexer.Lex(ioutil.Discard, ai, time.Duration(ac.RecPeriod*float64(time.Second))) - time.Sleep(time.Duration(ac.RecPeriod*float64(time.Second)) * time.Duration(n)) + go lexer.Lex(ioutil.Discard, ai, time.Duration(c.RecPeriod*float64(time.Second))) + time.Sleep(time.Duration(c.RecPeriod*float64(time.Second)) * time.Duration(n)) ai.Stop() } From 4858797d91fd7ef0b06e9ebc0fb2d49bb1ba0812 Mon Sep 17 00:00:00 2001 From: Trek H Date: Mon, 11 Nov 2019 16:55:47 +1030 Subject: [PATCH 2/8] audio: moved package from input/audio to device/audio --- device/audio/audio.go | 490 +++++++++++++++++++++++++++++++++++++ device/audio/audio_test.go | 107 ++++++++ 2 files changed, 597 insertions(+) create mode 100644 device/audio/audio.go create mode 100644 device/audio/audio_test.go diff --git a/device/audio/audio.go b/device/audio/audio.go new file mode 100644 index 00000000..a356ea48 --- /dev/null +++ b/device/audio/audio.go @@ -0,0 +1,490 @@ +/* +NAME + audio.go + +AUTHOR + Alan Noble + Trek Hopton + +LICENSE + This file is Copyright (C) 2019 the Australian Ocean Lab (AusOcean) + + It is free software: you can redistribute it and/or modify them + under the terms of the GNU General Public License as published by the + Free Software Foundation, either version 3 of the License, or (at your + option) any later version. + + It is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + + You should have received a copy of the GNU General Public License in gpl.txt. + If not, see [GNU licenses](http://www.gnu.org/licenses). +*/ + +// Package audio provides access to input from audio devices. +package audio + +import ( + "bytes" + "errors" + "fmt" + "sync" + "time" + + "github.com/yobert/alsa" + + "bitbucket.org/ausocean/av/codec/adpcm" + "bitbucket.org/ausocean/av/codec/codecutil" + "bitbucket.org/ausocean/av/codec/pcm" + "bitbucket.org/ausocean/av/device" + "bitbucket.org/ausocean/av/revid/config" + "bitbucket.org/ausocean/utils/logger" + "bitbucket.org/ausocean/utils/ring" +) + +const ( + pkg = "audio: " + rbTimeout = 100 * time.Millisecond + rbNextTimeout = 100 * time.Millisecond + rbLen = 200 + defaultSampleRate = 48000 +) + +// "running" means the input goroutine is reading from the ALSA device and writing to the ringbuffer. +// "paused" means the input routine is sleeping until unpaused or stopped. +// "stopped" means the input routine is stopped and the ALSA device is closed. +const ( + running = iota + 1 + paused + stopped +) + +// Device holds everything we need to know about the audio input stream and implements io.Reader. +type Device struct { + l Logger // Logger for device's routines to log to. + mode uint8 // Operating mode, either running, paused, or stopped. + mu sync.Mutex // Provides synchronisation when changing modes concurrently. + title string // Name of audio title, or empty for the default title. + dev *alsa.Device // ALSA's Audio input device. + ab alsa.Buffer // ALSA's buffer. + rb *ring.Buffer // Our buffer. + chunkSize int // This is the number of bytes that will be stored in rb at a time. + Config // Configuration parameters for this device. +} + +// Config provides parameters used by Device. +type Config struct { + SampleRate int + Channels int + BitDepth int + RecPeriod float64 + Codec uint8 +} + +// Logger enables any implementation of a logger to be used. +// TODO: Make this part of the logger package. +type Logger interface { + SetLevel(int8) + Log(level int8, message string, params ...interface{}) +} + +// OpenError is used to determine whether an error has originated from attempting to open a device. +type OpenError error + +// NewDevice initializes and returns a Device which has its logger set as the given logger. +func NewDevice(l Logger) *Device { + d := &Device{ + l: l, + } + return d +} + +// Set will take a Config struct, check the validity of the relevant fields +// and then performs any configuration necessary. If fields are not valid, +// an error is added to the multiError and a default value is used. +// It then initialises the Device which can then be started, read from, and stopped. +func (d *Device) Set(c config.Config) error { + var errs device.MultiError + if c.SampleRate <= 0 { + errs = append(errs, fmt.Errorf("invalid sample rate: %v", c.SampleRate)) + } + if c.Channels <= 0 { + errs = append(errs, fmt.Errorf("invalid number of channels: %v", c.Channels)) + } + if c.BitDepth <= 0 { + errs = append(errs, fmt.Errorf("invalid bitdepth: %v", c.BitDepth)) + } + if c.RecPeriod <= 0 { + errs = append(errs, fmt.Errorf("invalid recording period: %v", c.RecPeriod)) + } + if !codecutil.IsValid(c.InputCodec) { + errs = append(errs, errors.New("invalid codec")) + } + d.Config = Config{ + SampleRate: c.SampleRate, + Channels: c.Channels, + BitDepth: c.BitDepth, + RecPeriod: c.RecPeriod, + Codec: c.InputCodec, + } + + // Open the requested audio device. + err := d.open() + if err != nil { + d.l.Log(logger.Error, pkg+"failed to open device") + return err + } + + // Setup the device to record with desired period. + d.ab = d.dev.NewBufferDuration(time.Duration(d.RecPeriod * float64(time.Second))) + + // Account for channel conversion. + chunkSize := float64(len(d.ab.Data) / d.dev.BufferFormat().Channels * d.Channels) + + // Account for resampling. + chunkSize = (chunkSize / float64(d.dev.BufferFormat().Rate)) * float64(d.SampleRate) + if chunkSize < 1 { + return errors.New("given Config parameters are too small") + } + + // Account for codec conversion. + if d.Codec == codecutil.ADPCM { + d.chunkSize = adpcm.EncBytes(int(chunkSize)) + } else { + d.chunkSize = int(chunkSize) + } + + // Create ring buffer with appropriate chunk size. + d.rb = ring.NewBuffer(rbLen, d.chunkSize, rbTimeout) + + // Start device in paused mode. + d.mode = paused + go d.input() + + return nil +} + +// Start will start recording audio and writing to the ringbuffer. +// Once a Device has been stopped it cannot be started again. This is likely to change in future. +func (d *Device) Start() error { + d.mu.Lock() + mode := d.mode + d.mu.Unlock() + switch mode { + case paused: + d.mu.Lock() + d.mode = running + d.mu.Unlock() + return nil + case stopped: + // TODO(Trek): Make this reopen device and start recording. + return errors.New("device is stopped") + case running: + return nil + default: + return fmt.Errorf("invalid mode: %d", mode) + } +} + +// Stop will stop recording audio and close the device. +// Once a Device has been stopped it cannot be started again. This is likely to change in future. +func (d *Device) Stop() { + d.mu.Lock() + d.mode = stopped + d.mu.Unlock() +} + +// ChunkSize returns the number of bytes written to the ringbuffer per d.RecPeriod. +func (d *Device) ChunkSize() int { + return d.chunkSize +} + +// validate checks if Config parameters are valid and returns an error if they are not. +func validate(c *Config) error { + if c.SampleRate <= 0 { + return fmt.Errorf("invalid sample rate: %v", c.SampleRate) + } + if c.Channels <= 0 { + return fmt.Errorf("invalid number of channels: %v", c.Channels) + } + if c.BitDepth <= 0 { + return fmt.Errorf("invalid bitdepth: %v", c.BitDepth) + } + if c.RecPeriod <= 0 { + return fmt.Errorf("invalid recording period: %v", c.RecPeriod) + } + if !codecutil.IsValid(c.Codec) { + return errors.New("invalid codec") + } + return nil +} + +// open the recording device with the given name and prepare it to record. +// If name is empty, the first recording device is used. +func (d *Device) open() error { + // Close any existing device. + if d.dev != nil { + d.l.Log(logger.Debug, pkg+"closing device", "title", d.title) + d.dev.Close() + d.dev = nil + } + + // Open sound card and open recording device. + d.l.Log(logger.Debug, pkg+"opening sound card") + cards, err := alsa.OpenCards() + if err != nil { + return OpenError(err) + } + defer alsa.CloseCards(cards) + + d.l.Log(logger.Debug, pkg+"finding audio device") + for _, card := range cards { + devices, err := card.Devices() + if err != nil { + continue + } + for _, dev := range devices { + if dev.Type != alsa.PCM || !dev.Record { + continue + } + if dev.Title == d.title || d.title == "" { + d.dev = dev + break + } + } + } + if d.dev == nil { + return OpenError(errors.New("no audio device found")) + } + + d.l.Log(logger.Debug, pkg+"opening audio device", "title", d.dev.Title) + err = d.dev.Open() + if err != nil { + return OpenError(err) + } + + // 2 channels is what most devices need to record in. If mono is requested, + // the recording will be converted in formatBuffer(). + channels, err := d.dev.NegotiateChannels(2) + if err != nil { + return OpenError(err) + } + d.l.Log(logger.Debug, pkg+"alsa device channels set", "channels", channels) + + // Try to negotiate a rate to record in that is divisible by the wanted rate + // so that it can be easily downsampled to the wanted rate. + // rates is a slice of common sample rates including the standard for CD (44100Hz) and standard for professional audio recording (48000Hz). + // Note: if a card thinks it can record at a rate but can't actually, this can cause a failure. + // Eg. the audioinjector sound card is supposed to record at 8000Hz and 16000Hz but it can't due to a firmware issue, + // a fix for this is to remove 8000 and 16000 from the rates slice. + var rates = [8]int{8000, 16000, 32000, 44100, 48000, 88200, 96000, 192000} + + var rate int + foundRate := false + for r := range rates { + if r < d.SampleRate { + continue + } + if r%d.SampleRate == 0 { + rate, err = d.dev.NegotiateRate(r) + if err == nil { + foundRate = true + d.l.Log(logger.Debug, pkg+"alsa device sample rate set", "rate", rate) + break + } + } + } + + // If no easily divisible rate is found, then use the default rate. + if !foundRate { + d.l.Log(logger.Warning, pkg+"Unable to sample at requested rate, default used.", "rateRequested", d.SampleRate) + rate, err = d.dev.NegotiateRate(defaultSampleRate) + if err != nil { + return OpenError(err) + } + d.l.Log(logger.Debug, pkg+"alsa device sample rate set", "rate", rate) + } + + var aFmt alsa.FormatType + switch d.BitDepth { + case 16: + aFmt = alsa.S16_LE + case 32: + aFmt = alsa.S32_LE + default: + return OpenError(fmt.Errorf("unsupported sample bits %v", d.BitDepth)) + } + devFmt, err := d.dev.NegotiateFormat(aFmt) + if err != nil { + return err + } + var bitdepth int + switch devFmt { + case alsa.S16_LE: + bitdepth = 16 + case alsa.S32_LE: + bitdepth = 32 + default: + return OpenError(fmt.Errorf("unsupported sample bits %v", d.BitDepth)) + } + d.l.Log(logger.Debug, pkg+"alsa device bit depth set", "bitdepth", bitdepth) + + // A 50ms period is a sensible value for low-ish latency. (this could be made configurable if needed) + // Some devices only accept even period sizes while others want powers of 2. + // So we will find the closest power of 2 to the desired period size. + const wantPeriod = 0.05 //seconds + bytesPerSecond := rate * channels * (bitdepth / 8) + wantPeriodSize := int(float64(bytesPerSecond) * wantPeriod) + nearWantPeriodSize := nearestPowerOfTwo(wantPeriodSize) + + // At least two period sizes should fit within the buffer. + bufSize, err := d.dev.NegotiateBufferSize(nearWantPeriodSize * 2) + if err != nil { + return OpenError(err) + } + d.l.Log(logger.Debug, pkg+"alsa device buffer size set", "buffersize", bufSize) + + if err = d.dev.Prepare(); err != nil { + return OpenError(err) + } + + d.l.Log(logger.Debug, pkg+"successfully negotiated ALSA params") + return nil +} + +// input continously records audio and writes it to the ringbuffer. +// Re-opens the device and tries again if ASLA returns an error. +func (d *Device) input() { + for { + // Check mode. + d.mu.Lock() + mode := d.mode + d.mu.Unlock() + switch mode { + case paused: + time.Sleep(time.Duration(d.RecPeriod) * time.Second) + continue + case stopped: + if d.dev != nil { + d.l.Log(logger.Debug, pkg+"closing audio device", "title", d.title) + d.dev.Close() + d.dev = nil + } + return + } + + // Read from audio device. + d.l.Log(logger.Debug, pkg+"recording audio for period", "seconds", d.RecPeriod) + err := d.dev.Read(d.ab.Data) + if err != nil { + d.l.Log(logger.Debug, pkg+"read failed", "error", err.Error()) + err = d.open() // re-open + if err != nil { + d.l.Log(logger.Fatal, pkg+"reopening device failed", "error", err.Error()) + return + } + continue + } + + // Process audio. + d.l.Log(logger.Debug, pkg+"processing audio") + toWrite := d.formatBuffer() + + // Write audio to ringbuffer. + n, err := d.rb.Write(toWrite.Data) + switch err { + case nil: + d.l.Log(logger.Debug, pkg+"wrote audio to ringbuffer", "length", n) + case ring.ErrDropped: + d.l.Log(logger.Warning, pkg+"old audio data overwritten") + default: + d.l.Log(logger.Error, pkg+"unexpected ringbuffer error", "error", err.Error()) + return + } + } +} + +// Read reads from the ringbuffer, returning the number of bytes read upon success. +func (d *Device) Read(p []byte) (int, error) { + // Ready ringbuffer for read. + _, err := d.rb.Next(rbNextTimeout) + if err != nil { + return 0, err + } + + // Read from ring buffer. + return d.rb.Read(p) +} + +// formatBuffer returns audio that has been converted to the desired format. +func (d *Device) formatBuffer() alsa.Buffer { + var err error + + // If nothing needs to be changed, return the original. + if d.ab.Format.Channels == d.Channels && d.ab.Format.Rate == d.SampleRate { + return d.ab + } + var formatted alsa.Buffer + if d.ab.Format.Channels != d.Channels { + // Convert channels. + // TODO(Trek): Make this work for conversions other than stereo to mono. + if d.ab.Format.Channels == 2 && d.Channels == 1 { + formatted, err = pcm.StereoToMono(d.ab) + if err != nil { + d.l.Log(logger.Fatal, pkg+"channel conversion failed", "error", err.Error()) + } + } + } + + if d.ab.Format.Rate != d.SampleRate { + // Convert rate. + formatted, err = pcm.Resample(formatted, d.SampleRate) + if err != nil { + d.l.Log(logger.Fatal, pkg+"rate conversion failed", "error", err.Error()) + } + } + + switch d.Codec { + case codecutil.PCM: + case codecutil.ADPCM: + b := bytes.NewBuffer(make([]byte, 0, adpcm.EncBytes(len(formatted.Data)))) + enc := adpcm.NewEncoder(b) + _, err = enc.Write(formatted.Data) + if err != nil { + d.l.Log(logger.Fatal, pkg+"unable to encode", "error", err.Error()) + } + formatted.Data = b.Bytes() + default: + d.l.Log(logger.Error, pkg+"unhandled audio codec") + } + + return formatted +} + +// nearestPowerOfTwo finds and returns the nearest power of two to the given integer. +// If the lower and higher power of two are the same distance, it returns the higher power. +// For negative values, 1 is returned. +// Source: https://stackoverflow.com/a/45859570 +func nearestPowerOfTwo(n int) int { + if n <= 0 { + return 1 + } + if n == 1 { + return 2 + } + v := n + v-- + v |= v >> 1 + v |= v >> 2 + v |= v >> 4 + v |= v >> 8 + v |= v >> 16 + v++ // higher power of 2 + x := v >> 1 // lower power of 2 + if (v - n) > (n - x) { + return x + } + return v +} diff --git a/device/audio/audio_test.go b/device/audio/audio_test.go new file mode 100644 index 00000000..f27a5659 --- /dev/null +++ b/device/audio/audio_test.go @@ -0,0 +1,107 @@ +/* +NAME + audio_test.go + +AUTHOR + Trek Hopton + +LICENSE + This file is Copyright (C) 2019 the Australian Ocean Lab (AusOcean) + + It is free software: you can redistribute it and/or modify them + under the terms of the GNU General Public License as published by the + Free Software Foundation, either version 3 of the License, or (at your + option) any later version. + + It is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + + You should have received a copy of the GNU General Public License in gpl.txt. + If not, see [GNU licenses](http://www.gnu.org/licenses). +*/ + +package audio + +import ( + "io/ioutil" + "os" + "strconv" + "testing" + "time" + + "bitbucket.org/ausocean/av/codec/codecutil" + "bitbucket.org/ausocean/av/revid/config" + "bitbucket.org/ausocean/utils/logger" +) + +func TestDevice(t *testing.T) { + // We want to open a device with a standard configuration. + c := config.Config{ + SampleRate: 8000, + Channels: 1, + RecPeriod: 0.3, + BitDepth: 16, + InputCodec: codecutil.ADPCM, + } + n := 2 // Number of periods to wait while recording. + + // Create a new audio Device, start, read/lex, and then stop it. + l := logger.New(logger.Debug, os.Stderr, true) + ai := NewDevice(l) + err := ai.Set(c) + // If there was an error opening the device, skip this test. + if _, ok := err.(OpenError); ok { + t.Skip(err) + } + // For any other error, report it. + if err != nil { + t.Error(err) + } + err = ai.Start() + if err != nil { + t.Error(err) + } + chunkSize := ai.ChunkSize() + lexer := codecutil.NewByteLexer(&chunkSize) + go lexer.Lex(ioutil.Discard, ai, time.Duration(c.RecPeriod*float64(time.Second))) + time.Sleep(time.Duration(c.RecPeriod*float64(time.Second)) * time.Duration(n)) + ai.Stop() +} + +var powerTests = []struct { + in int + out int +}{ + {36, 32}, + {47, 32}, + {3, 4}, + {46, 32}, + {7, 8}, + {2, 2}, + {36, 32}, + {757, 512}, + {2464, 2048}, + {18980, 16384}, + {70000, 65536}, + {8192, 8192}, + {2048, 2048}, + {65536, 65536}, + {-2048, 1}, + {-127, 1}, + {-1, 1}, + {0, 1}, + {1, 2}, +} + +func TestNearestPowerOfTwo(t *testing.T) { + for _, tt := range powerTests { + t.Run(strconv.Itoa(tt.in), func(t *testing.T) { + v := nearestPowerOfTwo(tt.in) + if v != tt.out { + t.Errorf("got %v, want %v", v, tt.out) + } + }) + } +} From 3df9300c1c9ee924ba53c4044a8c0c1dc41313b2 Mon Sep 17 00:00:00 2001 From: Trek H Date: Mon, 11 Nov 2019 17:05:10 +1030 Subject: [PATCH 3/8] audio: updated startAudioDevice to use AVDevice implementation --- input/audio/audio.go | 490 -------------------------------------- input/audio/audio_test.go | 107 --------- revid/audio_linux.go | 14 +- 3 files changed, 4 insertions(+), 607 deletions(-) delete mode 100644 input/audio/audio.go delete mode 100644 input/audio/audio_test.go diff --git a/input/audio/audio.go b/input/audio/audio.go deleted file mode 100644 index a356ea48..00000000 --- a/input/audio/audio.go +++ /dev/null @@ -1,490 +0,0 @@ -/* -NAME - audio.go - -AUTHOR - Alan Noble - Trek Hopton - -LICENSE - This file is Copyright (C) 2019 the Australian Ocean Lab (AusOcean) - - It is free software: you can redistribute it and/or modify them - under the terms of the GNU General Public License as published by the - Free Software Foundation, either version 3 of the License, or (at your - option) any later version. - - It is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - for more details. - - You should have received a copy of the GNU General Public License in gpl.txt. - If not, see [GNU licenses](http://www.gnu.org/licenses). -*/ - -// Package audio provides access to input from audio devices. -package audio - -import ( - "bytes" - "errors" - "fmt" - "sync" - "time" - - "github.com/yobert/alsa" - - "bitbucket.org/ausocean/av/codec/adpcm" - "bitbucket.org/ausocean/av/codec/codecutil" - "bitbucket.org/ausocean/av/codec/pcm" - "bitbucket.org/ausocean/av/device" - "bitbucket.org/ausocean/av/revid/config" - "bitbucket.org/ausocean/utils/logger" - "bitbucket.org/ausocean/utils/ring" -) - -const ( - pkg = "audio: " - rbTimeout = 100 * time.Millisecond - rbNextTimeout = 100 * time.Millisecond - rbLen = 200 - defaultSampleRate = 48000 -) - -// "running" means the input goroutine is reading from the ALSA device and writing to the ringbuffer. -// "paused" means the input routine is sleeping until unpaused or stopped. -// "stopped" means the input routine is stopped and the ALSA device is closed. -const ( - running = iota + 1 - paused - stopped -) - -// Device holds everything we need to know about the audio input stream and implements io.Reader. -type Device struct { - l Logger // Logger for device's routines to log to. - mode uint8 // Operating mode, either running, paused, or stopped. - mu sync.Mutex // Provides synchronisation when changing modes concurrently. - title string // Name of audio title, or empty for the default title. - dev *alsa.Device // ALSA's Audio input device. - ab alsa.Buffer // ALSA's buffer. - rb *ring.Buffer // Our buffer. - chunkSize int // This is the number of bytes that will be stored in rb at a time. - Config // Configuration parameters for this device. -} - -// Config provides parameters used by Device. -type Config struct { - SampleRate int - Channels int - BitDepth int - RecPeriod float64 - Codec uint8 -} - -// Logger enables any implementation of a logger to be used. -// TODO: Make this part of the logger package. -type Logger interface { - SetLevel(int8) - Log(level int8, message string, params ...interface{}) -} - -// OpenError is used to determine whether an error has originated from attempting to open a device. -type OpenError error - -// NewDevice initializes and returns a Device which has its logger set as the given logger. -func NewDevice(l Logger) *Device { - d := &Device{ - l: l, - } - return d -} - -// Set will take a Config struct, check the validity of the relevant fields -// and then performs any configuration necessary. If fields are not valid, -// an error is added to the multiError and a default value is used. -// It then initialises the Device which can then be started, read from, and stopped. -func (d *Device) Set(c config.Config) error { - var errs device.MultiError - if c.SampleRate <= 0 { - errs = append(errs, fmt.Errorf("invalid sample rate: %v", c.SampleRate)) - } - if c.Channels <= 0 { - errs = append(errs, fmt.Errorf("invalid number of channels: %v", c.Channels)) - } - if c.BitDepth <= 0 { - errs = append(errs, fmt.Errorf("invalid bitdepth: %v", c.BitDepth)) - } - if c.RecPeriod <= 0 { - errs = append(errs, fmt.Errorf("invalid recording period: %v", c.RecPeriod)) - } - if !codecutil.IsValid(c.InputCodec) { - errs = append(errs, errors.New("invalid codec")) - } - d.Config = Config{ - SampleRate: c.SampleRate, - Channels: c.Channels, - BitDepth: c.BitDepth, - RecPeriod: c.RecPeriod, - Codec: c.InputCodec, - } - - // Open the requested audio device. - err := d.open() - if err != nil { - d.l.Log(logger.Error, pkg+"failed to open device") - return err - } - - // Setup the device to record with desired period. - d.ab = d.dev.NewBufferDuration(time.Duration(d.RecPeriod * float64(time.Second))) - - // Account for channel conversion. - chunkSize := float64(len(d.ab.Data) / d.dev.BufferFormat().Channels * d.Channels) - - // Account for resampling. - chunkSize = (chunkSize / float64(d.dev.BufferFormat().Rate)) * float64(d.SampleRate) - if chunkSize < 1 { - return errors.New("given Config parameters are too small") - } - - // Account for codec conversion. - if d.Codec == codecutil.ADPCM { - d.chunkSize = adpcm.EncBytes(int(chunkSize)) - } else { - d.chunkSize = int(chunkSize) - } - - // Create ring buffer with appropriate chunk size. - d.rb = ring.NewBuffer(rbLen, d.chunkSize, rbTimeout) - - // Start device in paused mode. - d.mode = paused - go d.input() - - return nil -} - -// Start will start recording audio and writing to the ringbuffer. -// Once a Device has been stopped it cannot be started again. This is likely to change in future. -func (d *Device) Start() error { - d.mu.Lock() - mode := d.mode - d.mu.Unlock() - switch mode { - case paused: - d.mu.Lock() - d.mode = running - d.mu.Unlock() - return nil - case stopped: - // TODO(Trek): Make this reopen device and start recording. - return errors.New("device is stopped") - case running: - return nil - default: - return fmt.Errorf("invalid mode: %d", mode) - } -} - -// Stop will stop recording audio and close the device. -// Once a Device has been stopped it cannot be started again. This is likely to change in future. -func (d *Device) Stop() { - d.mu.Lock() - d.mode = stopped - d.mu.Unlock() -} - -// ChunkSize returns the number of bytes written to the ringbuffer per d.RecPeriod. -func (d *Device) ChunkSize() int { - return d.chunkSize -} - -// validate checks if Config parameters are valid and returns an error if they are not. -func validate(c *Config) error { - if c.SampleRate <= 0 { - return fmt.Errorf("invalid sample rate: %v", c.SampleRate) - } - if c.Channels <= 0 { - return fmt.Errorf("invalid number of channels: %v", c.Channels) - } - if c.BitDepth <= 0 { - return fmt.Errorf("invalid bitdepth: %v", c.BitDepth) - } - if c.RecPeriod <= 0 { - return fmt.Errorf("invalid recording period: %v", c.RecPeriod) - } - if !codecutil.IsValid(c.Codec) { - return errors.New("invalid codec") - } - return nil -} - -// open the recording device with the given name and prepare it to record. -// If name is empty, the first recording device is used. -func (d *Device) open() error { - // Close any existing device. - if d.dev != nil { - d.l.Log(logger.Debug, pkg+"closing device", "title", d.title) - d.dev.Close() - d.dev = nil - } - - // Open sound card and open recording device. - d.l.Log(logger.Debug, pkg+"opening sound card") - cards, err := alsa.OpenCards() - if err != nil { - return OpenError(err) - } - defer alsa.CloseCards(cards) - - d.l.Log(logger.Debug, pkg+"finding audio device") - for _, card := range cards { - devices, err := card.Devices() - if err != nil { - continue - } - for _, dev := range devices { - if dev.Type != alsa.PCM || !dev.Record { - continue - } - if dev.Title == d.title || d.title == "" { - d.dev = dev - break - } - } - } - if d.dev == nil { - return OpenError(errors.New("no audio device found")) - } - - d.l.Log(logger.Debug, pkg+"opening audio device", "title", d.dev.Title) - err = d.dev.Open() - if err != nil { - return OpenError(err) - } - - // 2 channels is what most devices need to record in. If mono is requested, - // the recording will be converted in formatBuffer(). - channels, err := d.dev.NegotiateChannels(2) - if err != nil { - return OpenError(err) - } - d.l.Log(logger.Debug, pkg+"alsa device channels set", "channels", channels) - - // Try to negotiate a rate to record in that is divisible by the wanted rate - // so that it can be easily downsampled to the wanted rate. - // rates is a slice of common sample rates including the standard for CD (44100Hz) and standard for professional audio recording (48000Hz). - // Note: if a card thinks it can record at a rate but can't actually, this can cause a failure. - // Eg. the audioinjector sound card is supposed to record at 8000Hz and 16000Hz but it can't due to a firmware issue, - // a fix for this is to remove 8000 and 16000 from the rates slice. - var rates = [8]int{8000, 16000, 32000, 44100, 48000, 88200, 96000, 192000} - - var rate int - foundRate := false - for r := range rates { - if r < d.SampleRate { - continue - } - if r%d.SampleRate == 0 { - rate, err = d.dev.NegotiateRate(r) - if err == nil { - foundRate = true - d.l.Log(logger.Debug, pkg+"alsa device sample rate set", "rate", rate) - break - } - } - } - - // If no easily divisible rate is found, then use the default rate. - if !foundRate { - d.l.Log(logger.Warning, pkg+"Unable to sample at requested rate, default used.", "rateRequested", d.SampleRate) - rate, err = d.dev.NegotiateRate(defaultSampleRate) - if err != nil { - return OpenError(err) - } - d.l.Log(logger.Debug, pkg+"alsa device sample rate set", "rate", rate) - } - - var aFmt alsa.FormatType - switch d.BitDepth { - case 16: - aFmt = alsa.S16_LE - case 32: - aFmt = alsa.S32_LE - default: - return OpenError(fmt.Errorf("unsupported sample bits %v", d.BitDepth)) - } - devFmt, err := d.dev.NegotiateFormat(aFmt) - if err != nil { - return err - } - var bitdepth int - switch devFmt { - case alsa.S16_LE: - bitdepth = 16 - case alsa.S32_LE: - bitdepth = 32 - default: - return OpenError(fmt.Errorf("unsupported sample bits %v", d.BitDepth)) - } - d.l.Log(logger.Debug, pkg+"alsa device bit depth set", "bitdepth", bitdepth) - - // A 50ms period is a sensible value for low-ish latency. (this could be made configurable if needed) - // Some devices only accept even period sizes while others want powers of 2. - // So we will find the closest power of 2 to the desired period size. - const wantPeriod = 0.05 //seconds - bytesPerSecond := rate * channels * (bitdepth / 8) - wantPeriodSize := int(float64(bytesPerSecond) * wantPeriod) - nearWantPeriodSize := nearestPowerOfTwo(wantPeriodSize) - - // At least two period sizes should fit within the buffer. - bufSize, err := d.dev.NegotiateBufferSize(nearWantPeriodSize * 2) - if err != nil { - return OpenError(err) - } - d.l.Log(logger.Debug, pkg+"alsa device buffer size set", "buffersize", bufSize) - - if err = d.dev.Prepare(); err != nil { - return OpenError(err) - } - - d.l.Log(logger.Debug, pkg+"successfully negotiated ALSA params") - return nil -} - -// input continously records audio and writes it to the ringbuffer. -// Re-opens the device and tries again if ASLA returns an error. -func (d *Device) input() { - for { - // Check mode. - d.mu.Lock() - mode := d.mode - d.mu.Unlock() - switch mode { - case paused: - time.Sleep(time.Duration(d.RecPeriod) * time.Second) - continue - case stopped: - if d.dev != nil { - d.l.Log(logger.Debug, pkg+"closing audio device", "title", d.title) - d.dev.Close() - d.dev = nil - } - return - } - - // Read from audio device. - d.l.Log(logger.Debug, pkg+"recording audio for period", "seconds", d.RecPeriod) - err := d.dev.Read(d.ab.Data) - if err != nil { - d.l.Log(logger.Debug, pkg+"read failed", "error", err.Error()) - err = d.open() // re-open - if err != nil { - d.l.Log(logger.Fatal, pkg+"reopening device failed", "error", err.Error()) - return - } - continue - } - - // Process audio. - d.l.Log(logger.Debug, pkg+"processing audio") - toWrite := d.formatBuffer() - - // Write audio to ringbuffer. - n, err := d.rb.Write(toWrite.Data) - switch err { - case nil: - d.l.Log(logger.Debug, pkg+"wrote audio to ringbuffer", "length", n) - case ring.ErrDropped: - d.l.Log(logger.Warning, pkg+"old audio data overwritten") - default: - d.l.Log(logger.Error, pkg+"unexpected ringbuffer error", "error", err.Error()) - return - } - } -} - -// Read reads from the ringbuffer, returning the number of bytes read upon success. -func (d *Device) Read(p []byte) (int, error) { - // Ready ringbuffer for read. - _, err := d.rb.Next(rbNextTimeout) - if err != nil { - return 0, err - } - - // Read from ring buffer. - return d.rb.Read(p) -} - -// formatBuffer returns audio that has been converted to the desired format. -func (d *Device) formatBuffer() alsa.Buffer { - var err error - - // If nothing needs to be changed, return the original. - if d.ab.Format.Channels == d.Channels && d.ab.Format.Rate == d.SampleRate { - return d.ab - } - var formatted alsa.Buffer - if d.ab.Format.Channels != d.Channels { - // Convert channels. - // TODO(Trek): Make this work for conversions other than stereo to mono. - if d.ab.Format.Channels == 2 && d.Channels == 1 { - formatted, err = pcm.StereoToMono(d.ab) - if err != nil { - d.l.Log(logger.Fatal, pkg+"channel conversion failed", "error", err.Error()) - } - } - } - - if d.ab.Format.Rate != d.SampleRate { - // Convert rate. - formatted, err = pcm.Resample(formatted, d.SampleRate) - if err != nil { - d.l.Log(logger.Fatal, pkg+"rate conversion failed", "error", err.Error()) - } - } - - switch d.Codec { - case codecutil.PCM: - case codecutil.ADPCM: - b := bytes.NewBuffer(make([]byte, 0, adpcm.EncBytes(len(formatted.Data)))) - enc := adpcm.NewEncoder(b) - _, err = enc.Write(formatted.Data) - if err != nil { - d.l.Log(logger.Fatal, pkg+"unable to encode", "error", err.Error()) - } - formatted.Data = b.Bytes() - default: - d.l.Log(logger.Error, pkg+"unhandled audio codec") - } - - return formatted -} - -// nearestPowerOfTwo finds and returns the nearest power of two to the given integer. -// If the lower and higher power of two are the same distance, it returns the higher power. -// For negative values, 1 is returned. -// Source: https://stackoverflow.com/a/45859570 -func nearestPowerOfTwo(n int) int { - if n <= 0 { - return 1 - } - if n == 1 { - return 2 - } - v := n - v-- - v |= v >> 1 - v |= v >> 2 - v |= v >> 4 - v |= v >> 8 - v |= v >> 16 - v++ // higher power of 2 - x := v >> 1 // lower power of 2 - if (v - n) > (n - x) { - return x - } - return v -} diff --git a/input/audio/audio_test.go b/input/audio/audio_test.go deleted file mode 100644 index 9c5735c5..00000000 --- a/input/audio/audio_test.go +++ /dev/null @@ -1,107 +0,0 @@ -/* -NAME - audio_test.go - -AUTHOR - Trek Hopton - -LICENSE - This file is Copyright (C) 2019 the Australian Ocean Lab (AusOcean) - - It is free software: you can redistribute it and/or modify them - under the terms of the GNU General Public License as published by the - Free Software Foundation, either version 3 of the License, or (at your - option) any later version. - - It is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - for more details. - - You should have received a copy of the GNU General Public License in gpl.txt. - If not, see [GNU licenses](http://www.gnu.org/licenses). -*/ - -package audio - -import ( - "io/ioutil" - "os" - "strconv" - "testing" - "time" - - "bitbucket.org/ausocean/av/codec/codecutil" - "bitbucket.org/ausocean/av/revid/config" - "bitbucket.org/ausocean/utils/logger" -) - -func TestDevice(t *testing.T) { - // We want to open a device with a standard configuration. - c := config.Config{ - SampleRate: 8000, - Channels: 1, - RecPeriod: 0.3, - BitDepth: 16, - InputCodec: codecutil.ADPCM, - } - n := 2 // Number of periods to wait while recording. - - // Create a new audio Device, start, read/lex, and then stop it. - l := logger.New(logger.Debug, os.Stderr) - ai := NewDevice(l) - err := ai.Set(c) - // If there was an error opening the device, skip this test. - if _, ok := err.(OpenError); ok { - t.Skip(err) - } - // For any other error, report it. - if err != nil { - t.Error(err) - } - err = ai.Start() - if err != nil { - t.Error(err) - } - chunkSize := ai.ChunkSize() - lexer := codecutil.NewByteLexer(&chunkSize) - go lexer.Lex(ioutil.Discard, ai, time.Duration(c.RecPeriod*float64(time.Second))) - time.Sleep(time.Duration(c.RecPeriod*float64(time.Second)) * time.Duration(n)) - ai.Stop() -} - -var powerTests = []struct { - in int - out int -}{ - {36, 32}, - {47, 32}, - {3, 4}, - {46, 32}, - {7, 8}, - {2, 2}, - {36, 32}, - {757, 512}, - {2464, 2048}, - {18980, 16384}, - {70000, 65536}, - {8192, 8192}, - {2048, 2048}, - {65536, 65536}, - {-2048, 1}, - {-127, 1}, - {-1, 1}, - {0, 1}, - {1, 2}, -} - -func TestNearestPowerOfTwo(t *testing.T) { - for _, tt := range powerTests { - t.Run(strconv.Itoa(tt.in), func(t *testing.T) { - v := nearestPowerOfTwo(tt.in) - if v != tt.out { - t.Errorf("got %v, want %v", v, tt.out) - } - }) - } -} diff --git a/revid/audio_linux.go b/revid/audio_linux.go index 76d30a9f..622188b4 100644 --- a/revid/audio_linux.go +++ b/revid/audio_linux.go @@ -21,7 +21,7 @@ package revid import ( "time" - "bitbucket.org/ausocean/av/input/audio" + "bitbucket.org/ausocean/av/device/audio" "bitbucket.org/ausocean/utils/logger" ) @@ -29,17 +29,11 @@ import ( // It returns a function that can be used to stop the device and any errors that occur. func (r *Revid) startAudioDevice() (func() error, error) { // Create audio device. - ac := &audio.Config{ - SampleRate: r.cfg.SampleRate, - Channels: r.cfg.Channels, - RecPeriod: r.cfg.RecPeriod, - BitDepth: r.cfg.BitDepth, - Codec: r.cfg.InputCodec, - } + ai := audio.NewDevice(r.cfg.Logger) - ai, err := audio.NewDevice(ac, r.cfg.Logger) + err := ai.Set(r.cfg) if err != nil { - r.cfg.Logger.Log(logger.Fatal, pkg+"failed to create audio device", "error", err.Error()) + r.cfg.Logger.Log(logger.Fatal, pkg+"failed to setup audio device", "error", err.Error()) } // Start audio device From 78b31c6322f80b457e78071e4ea0c8de93f4e19d Mon Sep 17 00:00:00 2001 From: Trek H Date: Mon, 11 Nov 2019 17:23:15 +1030 Subject: [PATCH 4/8] audio: inline syntax for NewDevice --- device/audio/audio.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/device/audio/audio.go b/device/audio/audio.go index a356ea48..949a4741 100644 --- a/device/audio/audio.go +++ b/device/audio/audio.go @@ -94,12 +94,7 @@ type Logger interface { type OpenError error // NewDevice initializes and returns a Device which has its logger set as the given logger. -func NewDevice(l Logger) *Device { - d := &Device{ - l: l, - } - return d -} +func NewDevice(l Logger) *Device { return &Device{l: l} } // Set will take a Config struct, check the validity of the relevant fields // and then performs any configuration necessary. If fields are not valid, From b66f415e1ccdc142ddc74df2d75aa7cf24eccd4b Mon Sep 17 00:00:00 2001 From: Trek H Date: Mon, 11 Nov 2019 17:50:46 +1030 Subject: [PATCH 5/8] audio: renamed Device to AudioDevice --- device/audio/audio.go | 32 ++++++++++++++++---------------- device/audio/audio_test.go | 2 +- revid/audio_linux.go | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/device/audio/audio.go b/device/audio/audio.go index 949a4741..3ba11f69 100644 --- a/device/audio/audio.go +++ b/device/audio/audio.go @@ -61,8 +61,8 @@ const ( stopped ) -// Device holds everything we need to know about the audio input stream and implements io.Reader. -type Device struct { +// AudioDevice holds everything we need to know about the audio input stream and implements io.Reader. +type AudioDevice struct { l Logger // Logger for device's routines to log to. mode uint8 // Operating mode, either running, paused, or stopped. mu sync.Mutex // Provides synchronisation when changing modes concurrently. @@ -74,7 +74,7 @@ type Device struct { Config // Configuration parameters for this device. } -// Config provides parameters used by Device. +// Config provides parameters used by AudioDevice. type Config struct { SampleRate int Channels int @@ -93,14 +93,14 @@ type Logger interface { // OpenError is used to determine whether an error has originated from attempting to open a device. type OpenError error -// NewDevice initializes and returns a Device which has its logger set as the given logger. -func NewDevice(l Logger) *Device { return &Device{l: l} } +// NewAudioDevice initializes and returns a AudioDevice which has its logger set as the given logger. +func NewAudioDevice(l Logger) *AudioDevice { return &AudioDevice{l: l} } // Set will take a Config struct, check the validity of the relevant fields // and then performs any configuration necessary. If fields are not valid, // an error is added to the multiError and a default value is used. -// It then initialises the Device which can then be started, read from, and stopped. -func (d *Device) Set(c config.Config) error { +// It then initialises the AudioDevice which can then be started, read from, and stopped. +func (d *AudioDevice) Set(c config.Config) error { var errs device.MultiError if c.SampleRate <= 0 { errs = append(errs, fmt.Errorf("invalid sample rate: %v", c.SampleRate)) @@ -162,8 +162,8 @@ func (d *Device) Set(c config.Config) error { } // Start will start recording audio and writing to the ringbuffer. -// Once a Device has been stopped it cannot be started again. This is likely to change in future. -func (d *Device) Start() error { +// Once a AudioDevice has been stopped it cannot be started again. This is likely to change in future. +func (d *AudioDevice) Start() error { d.mu.Lock() mode := d.mode d.mu.Unlock() @@ -184,15 +184,15 @@ func (d *Device) Start() error { } // Stop will stop recording audio and close the device. -// Once a Device has been stopped it cannot be started again. This is likely to change in future. -func (d *Device) Stop() { +// Once a AudioDevice has been stopped it cannot be started again. This is likely to change in future. +func (d *AudioDevice) Stop() { d.mu.Lock() d.mode = stopped d.mu.Unlock() } // ChunkSize returns the number of bytes written to the ringbuffer per d.RecPeriod. -func (d *Device) ChunkSize() int { +func (d *AudioDevice) ChunkSize() int { return d.chunkSize } @@ -218,7 +218,7 @@ func validate(c *Config) error { // open the recording device with the given name and prepare it to record. // If name is empty, the first recording device is used. -func (d *Device) open() error { +func (d *AudioDevice) open() error { // Close any existing device. if d.dev != nil { d.l.Log(logger.Debug, pkg+"closing device", "title", d.title) @@ -351,7 +351,7 @@ func (d *Device) open() error { // input continously records audio and writes it to the ringbuffer. // Re-opens the device and tries again if ASLA returns an error. -func (d *Device) input() { +func (d *AudioDevice) input() { for { // Check mode. d.mu.Lock() @@ -402,7 +402,7 @@ func (d *Device) input() { } // Read reads from the ringbuffer, returning the number of bytes read upon success. -func (d *Device) Read(p []byte) (int, error) { +func (d *AudioDevice) Read(p []byte) (int, error) { // Ready ringbuffer for read. _, err := d.rb.Next(rbNextTimeout) if err != nil { @@ -414,7 +414,7 @@ func (d *Device) Read(p []byte) (int, error) { } // formatBuffer returns audio that has been converted to the desired format. -func (d *Device) formatBuffer() alsa.Buffer { +func (d *AudioDevice) formatBuffer() alsa.Buffer { var err error // If nothing needs to be changed, return the original. diff --git a/device/audio/audio_test.go b/device/audio/audio_test.go index f27a5659..da4c2cfd 100644 --- a/device/audio/audio_test.go +++ b/device/audio/audio_test.go @@ -49,7 +49,7 @@ func TestDevice(t *testing.T) { // Create a new audio Device, start, read/lex, and then stop it. l := logger.New(logger.Debug, os.Stderr, true) - ai := NewDevice(l) + ai := NewAudioDevice(l) err := ai.Set(c) // If there was an error opening the device, skip this test. if _, ok := err.(OpenError); ok { diff --git a/revid/audio_linux.go b/revid/audio_linux.go index 622188b4..631af00e 100644 --- a/revid/audio_linux.go +++ b/revid/audio_linux.go @@ -29,7 +29,7 @@ import ( // It returns a function that can be used to stop the device and any errors that occur. func (r *Revid) startAudioDevice() (func() error, error) { // Create audio device. - ai := audio.NewDevice(r.cfg.Logger) + ai := audio.NewAudioDevice(r.cfg.Logger) err := ai.Set(r.cfg) if err != nil { From 733785254e6fac2c1ae93c599642abf9e9b06bc9 Mon Sep 17 00:00:00 2001 From: Trek H Date: Mon, 11 Nov 2019 19:39:10 +1030 Subject: [PATCH 6/8] audio: renamed AudioDevice to ADPCMDevice --- device/audio/audio.go | 32 ++++++++++++++++---------------- device/audio/audio_test.go | 4 ++-- revid/audio_linux.go | 2 +- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/device/audio/audio.go b/device/audio/audio.go index 3ba11f69..244ea195 100644 --- a/device/audio/audio.go +++ b/device/audio/audio.go @@ -61,8 +61,8 @@ const ( stopped ) -// AudioDevice holds everything we need to know about the audio input stream and implements io.Reader. -type AudioDevice struct { +// ADPCMDevice holds everything we need to know about the audio input stream and implements io.Reader. +type ADPCMDevice struct { l Logger // Logger for device's routines to log to. mode uint8 // Operating mode, either running, paused, or stopped. mu sync.Mutex // Provides synchronisation when changing modes concurrently. @@ -74,7 +74,7 @@ type AudioDevice struct { Config // Configuration parameters for this device. } -// Config provides parameters used by AudioDevice. +// Config provides parameters used by ADPCMDevice. type Config struct { SampleRate int Channels int @@ -93,14 +93,14 @@ type Logger interface { // OpenError is used to determine whether an error has originated from attempting to open a device. type OpenError error -// NewAudioDevice initializes and returns a AudioDevice which has its logger set as the given logger. -func NewAudioDevice(l Logger) *AudioDevice { return &AudioDevice{l: l} } +// NewADPCMDevice initializes and returns an ADPCMDevice which has its logger set as the given logger. +func NewADPCMDevice(l Logger) *ADPCMDevice { return &ADPCMDevice{l: l} } // Set will take a Config struct, check the validity of the relevant fields // and then performs any configuration necessary. If fields are not valid, // an error is added to the multiError and a default value is used. -// It then initialises the AudioDevice which can then be started, read from, and stopped. -func (d *AudioDevice) Set(c config.Config) error { +// It then initialises the ADPCMDevice which can then be started, read from, and stopped. +func (d *ADPCMDevice) Set(c config.Config) error { var errs device.MultiError if c.SampleRate <= 0 { errs = append(errs, fmt.Errorf("invalid sample rate: %v", c.SampleRate)) @@ -162,8 +162,8 @@ func (d *AudioDevice) Set(c config.Config) error { } // Start will start recording audio and writing to the ringbuffer. -// Once a AudioDevice has been stopped it cannot be started again. This is likely to change in future. -func (d *AudioDevice) Start() error { +// Once a ADPCMDevice has been stopped it cannot be started again. This is likely to change in future. +func (d *ADPCMDevice) Start() error { d.mu.Lock() mode := d.mode d.mu.Unlock() @@ -184,15 +184,15 @@ func (d *AudioDevice) Start() error { } // Stop will stop recording audio and close the device. -// Once a AudioDevice has been stopped it cannot be started again. This is likely to change in future. -func (d *AudioDevice) Stop() { +// Once a ADPCMDevice has been stopped it cannot be started again. This is likely to change in future. +func (d *ADPCMDevice) Stop() { d.mu.Lock() d.mode = stopped d.mu.Unlock() } // ChunkSize returns the number of bytes written to the ringbuffer per d.RecPeriod. -func (d *AudioDevice) ChunkSize() int { +func (d *ADPCMDevice) ChunkSize() int { return d.chunkSize } @@ -218,7 +218,7 @@ func validate(c *Config) error { // open the recording device with the given name and prepare it to record. // If name is empty, the first recording device is used. -func (d *AudioDevice) open() error { +func (d *ADPCMDevice) open() error { // Close any existing device. if d.dev != nil { d.l.Log(logger.Debug, pkg+"closing device", "title", d.title) @@ -351,7 +351,7 @@ func (d *AudioDevice) open() error { // input continously records audio and writes it to the ringbuffer. // Re-opens the device and tries again if ASLA returns an error. -func (d *AudioDevice) input() { +func (d *ADPCMDevice) input() { for { // Check mode. d.mu.Lock() @@ -402,7 +402,7 @@ func (d *AudioDevice) input() { } // Read reads from the ringbuffer, returning the number of bytes read upon success. -func (d *AudioDevice) Read(p []byte) (int, error) { +func (d *ADPCMDevice) Read(p []byte) (int, error) { // Ready ringbuffer for read. _, err := d.rb.Next(rbNextTimeout) if err != nil { @@ -414,7 +414,7 @@ func (d *AudioDevice) Read(p []byte) (int, error) { } // formatBuffer returns audio that has been converted to the desired format. -func (d *AudioDevice) formatBuffer() alsa.Buffer { +func (d *ADPCMDevice) formatBuffer() alsa.Buffer { var err error // If nothing needs to be changed, return the original. diff --git a/device/audio/audio_test.go b/device/audio/audio_test.go index da4c2cfd..c567f550 100644 --- a/device/audio/audio_test.go +++ b/device/audio/audio_test.go @@ -47,9 +47,9 @@ func TestDevice(t *testing.T) { } n := 2 // Number of periods to wait while recording. - // Create a new audio Device, start, read/lex, and then stop it. + // Create a new ADPCMDevice, start, read/lex, and then stop it. l := logger.New(logger.Debug, os.Stderr, true) - ai := NewAudioDevice(l) + ai := NewADPCMDevice(l) err := ai.Set(c) // If there was an error opening the device, skip this test. if _, ok := err.(OpenError); ok { diff --git a/revid/audio_linux.go b/revid/audio_linux.go index 631af00e..f637b30d 100644 --- a/revid/audio_linux.go +++ b/revid/audio_linux.go @@ -29,7 +29,7 @@ import ( // It returns a function that can be used to stop the device and any errors that occur. func (r *Revid) startAudioDevice() (func() error, error) { // Create audio device. - ai := audio.NewAudioDevice(r.cfg.Logger) + ai := audio.NewADPCMDevice(r.cfg.Logger) err := ai.Set(r.cfg) if err != nil { From c4134fd30e3fe4b0e0f25a8675fa766a865b8756 Mon Sep 17 00:00:00 2001 From: Trek H Date: Mon, 11 Nov 2019 22:26:53 +1030 Subject: [PATCH 7/8] audio: renamed ADPCMDevice to ALSA --- device/audio/audio.go | 32 ++++++++++++++++---------------- device/audio/audio_test.go | 2 +- revid/audio_linux.go | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/device/audio/audio.go b/device/audio/audio.go index 244ea195..f67046d4 100644 --- a/device/audio/audio.go +++ b/device/audio/audio.go @@ -61,8 +61,8 @@ const ( stopped ) -// ADPCMDevice holds everything we need to know about the audio input stream and implements io.Reader. -type ADPCMDevice struct { +// ALSA holds everything we need to know about the audio input stream and implements io.Reader and device.AVDevice. +type ALSA struct { l Logger // Logger for device's routines to log to. mode uint8 // Operating mode, either running, paused, or stopped. mu sync.Mutex // Provides synchronisation when changing modes concurrently. @@ -74,7 +74,7 @@ type ADPCMDevice struct { Config // Configuration parameters for this device. } -// Config provides parameters used by ADPCMDevice. +// Config provides parameters used by ALSA. type Config struct { SampleRate int Channels int @@ -93,14 +93,14 @@ type Logger interface { // OpenError is used to determine whether an error has originated from attempting to open a device. type OpenError error -// NewADPCMDevice initializes and returns an ADPCMDevice which has its logger set as the given logger. -func NewADPCMDevice(l Logger) *ADPCMDevice { return &ADPCMDevice{l: l} } +// NewALSA initializes and returns an ALSA which has its logger set as the given logger. +func NewALSA(l Logger) *ALSA { return &ALSA{l: l} } // Set will take a Config struct, check the validity of the relevant fields // and then performs any configuration necessary. If fields are not valid, // an error is added to the multiError and a default value is used. -// It then initialises the ADPCMDevice which can then be started, read from, and stopped. -func (d *ADPCMDevice) Set(c config.Config) error { +// It then initialises the ALSA which can then be started, read from, and stopped. +func (d *ALSA) Set(c config.Config) error { var errs device.MultiError if c.SampleRate <= 0 { errs = append(errs, fmt.Errorf("invalid sample rate: %v", c.SampleRate)) @@ -162,8 +162,8 @@ func (d *ADPCMDevice) Set(c config.Config) error { } // Start will start recording audio and writing to the ringbuffer. -// Once a ADPCMDevice has been stopped it cannot be started again. This is likely to change in future. -func (d *ADPCMDevice) Start() error { +// Once a ALSA has been stopped it cannot be started again. This is likely to change in future. +func (d *ALSA) Start() error { d.mu.Lock() mode := d.mode d.mu.Unlock() @@ -184,15 +184,15 @@ func (d *ADPCMDevice) Start() error { } // Stop will stop recording audio and close the device. -// Once a ADPCMDevice has been stopped it cannot be started again. This is likely to change in future. -func (d *ADPCMDevice) Stop() { +// Once a ALSA has been stopped it cannot be started again. This is likely to change in future. +func (d *ALSA) Stop() { d.mu.Lock() d.mode = stopped d.mu.Unlock() } // ChunkSize returns the number of bytes written to the ringbuffer per d.RecPeriod. -func (d *ADPCMDevice) ChunkSize() int { +func (d *ALSA) ChunkSize() int { return d.chunkSize } @@ -218,7 +218,7 @@ func validate(c *Config) error { // open the recording device with the given name and prepare it to record. // If name is empty, the first recording device is used. -func (d *ADPCMDevice) open() error { +func (d *ALSA) open() error { // Close any existing device. if d.dev != nil { d.l.Log(logger.Debug, pkg+"closing device", "title", d.title) @@ -351,7 +351,7 @@ func (d *ADPCMDevice) open() error { // input continously records audio and writes it to the ringbuffer. // Re-opens the device and tries again if ASLA returns an error. -func (d *ADPCMDevice) input() { +func (d *ALSA) input() { for { // Check mode. d.mu.Lock() @@ -402,7 +402,7 @@ func (d *ADPCMDevice) input() { } // Read reads from the ringbuffer, returning the number of bytes read upon success. -func (d *ADPCMDevice) Read(p []byte) (int, error) { +func (d *ALSA) Read(p []byte) (int, error) { // Ready ringbuffer for read. _, err := d.rb.Next(rbNextTimeout) if err != nil { @@ -414,7 +414,7 @@ func (d *ADPCMDevice) Read(p []byte) (int, error) { } // formatBuffer returns audio that has been converted to the desired format. -func (d *ADPCMDevice) formatBuffer() alsa.Buffer { +func (d *ALSA) formatBuffer() alsa.Buffer { var err error // If nothing needs to be changed, return the original. diff --git a/device/audio/audio_test.go b/device/audio/audio_test.go index c567f550..a84fad81 100644 --- a/device/audio/audio_test.go +++ b/device/audio/audio_test.go @@ -49,7 +49,7 @@ func TestDevice(t *testing.T) { // Create a new ADPCMDevice, start, read/lex, and then stop it. l := logger.New(logger.Debug, os.Stderr, true) - ai := NewADPCMDevice(l) + ai := NewALSA(l) err := ai.Set(c) // If there was an error opening the device, skip this test. if _, ok := err.(OpenError); ok { diff --git a/revid/audio_linux.go b/revid/audio_linux.go index f637b30d..b209480d 100644 --- a/revid/audio_linux.go +++ b/revid/audio_linux.go @@ -29,7 +29,7 @@ import ( // It returns a function that can be used to stop the device and any errors that occur. func (r *Revid) startAudioDevice() (func() error, error) { // Create audio device. - ai := audio.NewADPCMDevice(r.cfg.Logger) + ai := audio.NewALSA(r.cfg.Logger) err := ai.Set(r.cfg) if err != nil { From 151ec56fc953133002923a3dcfa273610a5dc6f5 Mon Sep 17 00:00:00 2001 From: Trek H Date: Mon, 11 Nov 2019 22:40:13 +1030 Subject: [PATCH 8/8] audio: changed comment wording --- device/audio/audio.go | 20 ++++++++++---------- device/audio/audio_test.go | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/device/audio/audio.go b/device/audio/audio.go index f67046d4..1197ccda 100644 --- a/device/audio/audio.go +++ b/device/audio/audio.go @@ -61,20 +61,20 @@ const ( stopped ) -// ALSA holds everything we need to know about the audio input stream and implements io.Reader and device.AVDevice. +// An ALSA device holds everything we need to know about the audio input stream and implements io.Reader and device.AVDevice. type ALSA struct { l Logger // Logger for device's routines to log to. mode uint8 // Operating mode, either running, paused, or stopped. mu sync.Mutex // Provides synchronisation when changing modes concurrently. title string // Name of audio title, or empty for the default title. - dev *alsa.Device // ALSA's Audio input device. - ab alsa.Buffer // ALSA's buffer. + dev *alsa.Device // ALSA device's Audio input device. + ab alsa.Buffer // ALSA device's buffer. rb *ring.Buffer // Our buffer. chunkSize int // This is the number of bytes that will be stored in rb at a time. Config // Configuration parameters for this device. } -// Config provides parameters used by ALSA. +// Config provides parameters used by the ALSA device. type Config struct { SampleRate int Channels int @@ -93,13 +93,13 @@ type Logger interface { // OpenError is used to determine whether an error has originated from attempting to open a device. type OpenError error -// NewALSA initializes and returns an ALSA which has its logger set as the given logger. +// NewALSA initializes and returns an ALSA device which has its logger set as the given logger. func NewALSA(l Logger) *ALSA { return &ALSA{l: l} } // Set will take a Config struct, check the validity of the relevant fields // and then performs any configuration necessary. If fields are not valid, // an error is added to the multiError and a default value is used. -// It then initialises the ALSA which can then be started, read from, and stopped. +// It then initialises the ALSA device which can then be started, read from, and stopped. func (d *ALSA) Set(c config.Config) error { var errs device.MultiError if c.SampleRate <= 0 { @@ -162,7 +162,7 @@ func (d *ALSA) Set(c config.Config) error { } // Start will start recording audio and writing to the ringbuffer. -// Once a ALSA has been stopped it cannot be started again. This is likely to change in future. +// Once an ALSA device has been stopped it cannot be started again. This is likely to change in future. func (d *ALSA) Start() error { d.mu.Lock() mode := d.mode @@ -184,7 +184,7 @@ func (d *ALSA) Start() error { } // Stop will stop recording audio and close the device. -// Once a ALSA has been stopped it cannot be started again. This is likely to change in future. +// Once an ALSA device has been stopped it cannot be started again. This is likely to change in future. func (d *ALSA) Stop() { d.mu.Lock() d.mode = stopped @@ -345,12 +345,12 @@ func (d *ALSA) open() error { return OpenError(err) } - d.l.Log(logger.Debug, pkg+"successfully negotiated ALSA params") + d.l.Log(logger.Debug, pkg+"successfully negotiated device params") return nil } // input continously records audio and writes it to the ringbuffer. -// Re-opens the device and tries again if ASLA returns an error. +// Re-opens the device and tries again if the ASLA device returns an error. func (d *ALSA) input() { for { // Check mode. diff --git a/device/audio/audio_test.go b/device/audio/audio_test.go index a84fad81..db54ab17 100644 --- a/device/audio/audio_test.go +++ b/device/audio/audio_test.go @@ -47,7 +47,7 @@ func TestDevice(t *testing.T) { } n := 2 // Number of periods to wait while recording. - // Create a new ADPCMDevice, start, read/lex, and then stop it. + // Create a new ALSA device, start, read/lex, and then stop it. l := logger.New(logger.Debug, os.Stderr, true) ai := NewALSA(l) err := ai.Set(c)