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
|
||||
)
|
||||
|
||||
// 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.
|
||||
type Device struct {
|
||||
l Logger // Logger for device's routines to log to.
|
||||
|
@ -91,6 +88,9 @@ type Logger 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.
|
||||
func NewDevice(cfg *Config, l Logger) (*Device, error) {
|
||||
d := &Device{
|
||||
|
@ -135,6 +135,7 @@ func NewDevice(cfg *Config, l Logger) (*Device, 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 {
|
||||
d.mu.Lock()
|
||||
mode := d.mode
|
||||
|
@ -156,6 +157,7 @@ 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() {
|
||||
d.mu.Lock()
|
||||
d.mode = stopped
|
||||
|
@ -181,7 +183,7 @@ func (d *Device) open() error {
|
|||
d.l.Log(logger.Debug, pkg+"opening sound card")
|
||||
cards, err := alsa.OpenCards()
|
||||
if err != nil {
|
||||
return err
|
||||
return OpenError(err)
|
||||
}
|
||||
defer alsa.CloseCards(cards)
|
||||
|
||||
|
@ -202,39 +204,41 @@ func (d *Device) open() error {
|
|||
}
|
||||
}
|
||||
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)
|
||||
err = d.dev.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
return OpenError(err)
|
||||
}
|
||||
|
||||
// 2 channels is what most devices need to record in. If mono is requested,
|
||||
// the recording will be converted in formatBuffer().
|
||||
devChan, err := d.dev.NegotiateChannels(2)
|
||||
channels, err := d.dev.NegotiateChannels(2)
|
||||
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
|
||||
// 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.
|
||||
// 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
|
||||
var devRate int
|
||||
for i := 0; i < len(Rates) && !foundRate; i++ {
|
||||
if Rates[i] < d.SampleRate {
|
||||
var rate int
|
||||
for i := 0; i < len(rates) && !foundRate; i++ {
|
||||
if rates[i] < d.SampleRate {
|
||||
continue
|
||||
}
|
||||
if Rates[i]%d.SampleRate == 0 {
|
||||
devRate, err = d.dev.NegotiateRate(Rates[i])
|
||||
if rates[i]%d.SampleRate == 0 {
|
||||
rate, err = d.dev.NegotiateRate(rates[i])
|
||||
if err == nil {
|
||||
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 !foundRate {
|
||||
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 {
|
||||
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
|
||||
|
@ -256,40 +260,40 @@ func (d *Device) open() error {
|
|||
case 32:
|
||||
aFmt = alsa.S32_LE
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var devBits int
|
||||
var bitdepth int
|
||||
switch devFmt {
|
||||
case alsa.S16_LE:
|
||||
devBits = 16
|
||||
bitdepth = 16
|
||||
case alsa.S32_LE:
|
||||
devBits = 32
|
||||
bitdepth = 32
|
||||
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)
|
||||
// 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 := devRate * devChan * (devBits / 8)
|
||||
bytesPerSecond := rate * channels * (bitdepth / 8)
|
||||
wantPeriodSize := int(float64(bytesPerSecond) * wantPeriod)
|
||||
nearWantPeriodSize := nearestPowerOfTwo(wantPeriodSize)
|
||||
|
||||
// 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 {
|
||||
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 {
|
||||
return err
|
||||
return OpenError(err)
|
||||
}
|
||||
|
||||
d.l.Log(logger.Debug, pkg+"successfully negotiated ALSA params")
|
||||
|
|
|
@ -25,7 +25,6 @@ LICENSE
|
|||
package audio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
|
@ -34,85 +33,8 @@ import (
|
|||
|
||||
"bitbucket.org/ausocean/av/codec/codecutil"
|
||||
"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) {
|
||||
// We want to open a device with a standard configuration.
|
||||
ac := &Config{
|
||||
|
@ -124,15 +46,14 @@ func TestDevice(t *testing.T) {
|
|||
}
|
||||
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.
|
||||
l := logger.New(logger.Debug, os.Stderr)
|
||||
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 {
|
||||
t.Error(err)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue