mirror of https://bitbucket.org/ausocean/av.git
audio: added error type for opening devices for simpler testing
also style changes and formatting etc.
This commit is contained in:
parent
fb12a2f69e
commit
bcd59b98d2
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue