audio: added error type for opening devices for simpler testing

also style changes and formatting etc.
This commit is contained in:
Trek H 2019-06-18 01:13:42 +09:30
parent fb12a2f69e
commit bcd59b98d2
2 changed files with 39 additions and 114 deletions

View File

@ -59,9 +59,6 @@ const (
stopped stopped
) )
// Rates contains the standard audio sample rates used by package audio.
var Rates = [8]int{8000, 16000, 32000, 44100, 48000, 88200, 96000, 192000}
// Device holds everything we need to know about the audio input stream and implements io.Reader. // Device holds everything we need to know about the audio input stream and implements io.Reader.
type Device struct { type Device struct {
l Logger // Logger for device's routines to log to. l Logger // Logger for device's routines to log to.
@ -91,6 +88,9 @@ type Logger interface {
Log(level int8, message string, params ...interface{}) 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 an Device which can be started, read from, and stopped. // NewDevice initializes and returns an Device which can be started, read from, and stopped.
func NewDevice(cfg *Config, l Logger) (*Device, error) { func NewDevice(cfg *Config, l Logger) (*Device, error) {
d := &Device{ d := &Device{
@ -135,6 +135,7 @@ func NewDevice(cfg *Config, l Logger) (*Device, error) {
} }
// Start will start recording audio and writing to the ringbuffer. // 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 { func (d *Device) Start() error {
d.mu.Lock() d.mu.Lock()
mode := d.mode mode := d.mode
@ -156,6 +157,7 @@ func (d *Device) Start() error {
} }
// Stop will stop recording audio and close the device. // 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() { func (d *Device) Stop() {
d.mu.Lock() d.mu.Lock()
d.mode = stopped d.mode = stopped
@ -181,7 +183,7 @@ func (d *Device) open() error {
d.l.Log(logger.Debug, pkg+"opening sound card") d.l.Log(logger.Debug, pkg+"opening sound card")
cards, err := alsa.OpenCards() cards, err := alsa.OpenCards()
if err != nil { if err != nil {
return err return OpenError(err)
} }
defer alsa.CloseCards(cards) defer alsa.CloseCards(cards)
@ -202,39 +204,41 @@ func (d *Device) open() error {
} }
} }
if d.dev == nil { if d.dev == nil {
return errors.New("no audio device found") return OpenError(errors.New("no audio device found"))
} }
d.l.Log(logger.Debug, pkg+"opening audio device", "title", d.dev.Title) d.l.Log(logger.Debug, pkg+"opening audio device", "title", d.dev.Title)
err = d.dev.Open() err = d.dev.Open()
if err != nil { if err != nil {
return err return OpenError(err)
} }
// 2 channels is what most devices need to record in. If mono is requested, // 2 channels is what most devices need to record in. If mono is requested,
// the recording will be converted in formatBuffer(). // the recording will be converted in formatBuffer().
devChan, err := d.dev.NegotiateChannels(2) channels, err := d.dev.NegotiateChannels(2)
if err != nil { if err != nil {
return err return OpenError(err)
} }
d.l.Log(logger.Debug, pkg+"alsa device channels set", "channels", devChan) 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 // 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. // 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. // 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, // 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. // 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}
foundRate := false foundRate := false
var devRate int var rate int
for i := 0; i < len(Rates) && !foundRate; i++ { for i := 0; i < len(rates) && !foundRate; i++ {
if Rates[i] < d.SampleRate { if rates[i] < d.SampleRate {
continue continue
} }
if Rates[i]%d.SampleRate == 0 { if rates[i]%d.SampleRate == 0 {
devRate, err = d.dev.NegotiateRate(Rates[i]) rate, err = d.dev.NegotiateRate(rates[i])
if err == nil { if err == nil {
foundRate = true foundRate = true
d.l.Log(logger.Debug, pkg+"alsa device sample rate set", "rate", devRate) d.l.Log(logger.Debug, pkg+"alsa device sample rate set", "rate", rate)
} }
} }
} }
@ -242,11 +246,11 @@ func (d *Device) open() error {
// If no easily divisible rate is found, then use the default rate. // If no easily divisible rate is found, then use the default rate.
if !foundRate { if !foundRate {
d.l.Log(logger.Warning, pkg+"Unable to sample at requested rate, default used.", "rateRequested", d.SampleRate) d.l.Log(logger.Warning, pkg+"Unable to sample at requested rate, default used.", "rateRequested", d.SampleRate)
devRate, err = d.dev.NegotiateRate(defaultSampleRate) rate, err = d.dev.NegotiateRate(defaultSampleRate)
if err != nil { if err != nil {
return err return OpenError(err)
} }
d.l.Log(logger.Debug, pkg+"alsa device sample rate set", "rate", devRate) d.l.Log(logger.Debug, pkg+"alsa device sample rate set", "rate", rate)
} }
var aFmt alsa.FormatType var aFmt alsa.FormatType
@ -256,40 +260,40 @@ func (d *Device) open() error {
case 32: case 32:
aFmt = alsa.S32_LE aFmt = alsa.S32_LE
default: default:
return fmt.Errorf("unsupported sample bits %v", d.BitDepth) return OpenError(fmt.Errorf("unsupported sample bits %v", d.BitDepth))
} }
devFmt, err := d.dev.NegotiateFormat(aFmt) devFmt, err := d.dev.NegotiateFormat(aFmt)
if err != nil { if err != nil {
return err return err
} }
var devBits int var bitdepth int
switch devFmt { switch devFmt {
case alsa.S16_LE: case alsa.S16_LE:
devBits = 16 bitdepth = 16
case alsa.S32_LE: case alsa.S32_LE:
devBits = 32 bitdepth = 32
default: default:
return fmt.Errorf("unsupported sample bits %v", d.BitDepth) return OpenError(fmt.Errorf("unsupported sample bits %v", d.BitDepth))
} }
d.l.Log(logger.Debug, pkg+"alsa device bit depth set", "bitdepth", devBits) 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) // 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. // 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. // So we will find the closest power of 2 to the desired period size.
const wantPeriod = 0.05 //seconds const wantPeriod = 0.05 //seconds
bytesPerSecond := devRate * devChan * (devBits / 8) bytesPerSecond := rate * channels * (bitdepth / 8)
wantPeriodSize := int(float64(bytesPerSecond) * wantPeriod) wantPeriodSize := int(float64(bytesPerSecond) * wantPeriod)
nearWantPeriodSize := nearestPowerOfTwo(wantPeriodSize) nearWantPeriodSize := nearestPowerOfTwo(wantPeriodSize)
// At least two period sizes should fit within the buffer. // At least two period sizes should fit within the buffer.
devBufferSize, err := d.dev.NegotiateBufferSize(nearWantPeriodSize * 2) bufSize, err := d.dev.NegotiateBufferSize(nearWantPeriodSize * 2)
if err != nil { if err != nil {
return err return OpenError(err)
} }
d.l.Log(logger.Debug, pkg+"alsa device buffer size set", "buffersize", devBufferSize) d.l.Log(logger.Debug, pkg+"alsa device buffer size set", "buffersize", bufSize)
if err = d.dev.Prepare(); err != nil { if err = d.dev.Prepare(); err != nil {
return err return OpenError(err)
} }
d.l.Log(logger.Debug, pkg+"successfully negotiated ALSA params") d.l.Log(logger.Debug, pkg+"successfully negotiated ALSA params")

View File

@ -25,7 +25,6 @@ LICENSE
package audio package audio
import ( import (
"errors"
"io/ioutil" "io/ioutil"
"os" "os"
"strconv" "strconv"
@ -34,85 +33,8 @@ import (
"bitbucket.org/ausocean/av/codec/codecutil" "bitbucket.org/ausocean/av/codec/codecutil"
"bitbucket.org/ausocean/utils/logger" "bitbucket.org/ausocean/utils/logger"
"github.com/yobert/alsa"
) )
// Check that a device exists with the given config parameters.
func checkDevice(ac *Config) error {
cards, err := alsa.OpenCards()
if err != nil {
return errors.New("no audio cards found")
}
defer alsa.CloseCards(cards)
var testDev *alsa.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
}
testDev = dev
break
}
}
if testDev == nil {
return errors.New("no suitable device found")
}
err = testDev.Open()
if err != nil {
return err
}
_, err = testDev.NegotiateChannels(2)
if err != nil {
return err
}
foundRate := false
for i := 0; i < len(Rates) && !foundRate; i++ {
if Rates[i] < ac.SampleRate {
continue
}
if Rates[i]%ac.SampleRate == 0 {
_, err = testDev.NegotiateRate(Rates[i])
if err == nil {
foundRate = true
}
}
}
if !foundRate {
_, err = testDev.NegotiateRate(defaultSampleRate)
if err != nil {
return err
}
}
var aFmt alsa.FormatType
switch ac.BitDepth {
case 16:
aFmt = alsa.S16_LE
case 32:
aFmt = alsa.S32_LE
default:
return errors.New("unsupported bitdepth")
}
_, err = testDev.NegotiateFormat(aFmt)
if err != nil {
return err
}
_, err = testDev.NegotiateBufferSize(8192, 16384)
if err != nil {
return err
}
if err = testDev.Prepare(); err != nil {
return err
}
if testDev != nil {
testDev.Close()
}
return nil
}
func TestDevice(t *testing.T) { func TestDevice(t *testing.T) {
// We want to open a device with a standard configuration. // We want to open a device with a standard configuration.
ac := &Config{ ac := &Config{
@ -124,15 +46,14 @@ func TestDevice(t *testing.T) {
} }
n := 2 // Number of periods to wait while recording. n := 2 // Number of periods to wait while recording.
// Skip if there are no suitable devices to test with.
err := checkDevice(ac)
if err != nil {
t.Skip(err)
}
// Create a new audio Device, start, read/lex, and then stop it. // Create a new audio Device, start, read/lex, and then stop it.
l := logger.New(logger.Debug, os.Stderr) l := logger.New(logger.Debug, os.Stderr)
ai, err := NewDevice(ac, l) ai, err := NewDevice(ac, l)
// 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 { if err != nil {
t.Error(err) t.Error(err)
} }