revid and audio: seperated audio into own package

audio device input is now handle in its own package which resides in the new input directory
a list of codecs was added to codecutil package to help with multiple packages using the same codecs
This commit is contained in:
Trek H 2019-06-06 02:09:55 +09:30
parent 3e2ff49420
commit 96c1b51173
6 changed files with 136 additions and 92 deletions

View File

@ -38,6 +38,7 @@ import (
"strings" "strings"
"time" "time"
"bitbucket.org/ausocean/av/codec/codecutil"
"bitbucket.org/ausocean/av/container/mts" "bitbucket.org/ausocean/av/container/mts"
"bitbucket.org/ausocean/av/container/mts/meta" "bitbucket.org/ausocean/av/container/mts/meta"
"bitbucket.org/ausocean/av/revid" "bitbucket.org/ausocean/av/revid"
@ -200,11 +201,11 @@ func handleFlags() revid.Config {
switch *inputCodecPtr { switch *inputCodecPtr {
case "H264": case "H264":
cfg.InputCodec = revid.H264 cfg.InputCodec = codecutil.H264
case "PCM": case "PCM":
cfg.InputCodec = revid.PCM cfg.InputCodec = codecutil.PCM
case "ADPCM": case "ADPCM":
cfg.InputCodec = revid.ADPCM cfg.InputCodec = codecutil.ADPCM
case "": case "":
default: default:
log.Log(logger.Error, pkg+"bad input codec argument") log.Log(logger.Error, pkg+"bad input codec argument")

34
codec/codecutil/list.go Normal file
View File

@ -0,0 +1,34 @@
/*
NAME
list.go
AUTHOR
Trek Hopton <trek@ausocean.org>
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 codecutil
// A global list containing all available codecs for reference in any application.
const (
PCM = iota
ADPCM
H264
H265
MJPEG
)

View File

@ -1,12 +1,13 @@
/* /*
NAME NAME
audio-input.go audio.go
AUTHOR AUTHOR
Alan Noble <alan@ausocean.org>
Trek Hopton <trek@ausocean.org> Trek Hopton <trek@ausocean.org>
LICENSE LICENSE
audio-input.go is Copyright (C) 2019 the Australian Ocean Lab (AusOcean) This file is Copyright (C) 2019 the Australian Ocean Lab (AusOcean)
It is free software: you can redistribute it and/or modify them 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 under the terms of the GNU General Public License as published by the
@ -22,7 +23,8 @@ LICENSE
If not, see [GNU licenses](http://www.gnu.org/licenses). If not, see [GNU licenses](http://www.gnu.org/licenses).
*/ */
package revid // Package audio provides access to input from audio devices.
package audio
import ( import (
"bytes" "bytes"
@ -35,15 +37,18 @@ import (
"github.com/yobert/alsa" "github.com/yobert/alsa"
"bitbucket.org/ausocean/av/codec/adpcm" "bitbucket.org/ausocean/av/codec/adpcm"
"bitbucket.org/ausocean/av/codec/codecutil"
"bitbucket.org/ausocean/av/codec/pcm" "bitbucket.org/ausocean/av/codec/pcm"
"bitbucket.org/ausocean/utils/logger" "bitbucket.org/ausocean/utils/logger"
"bitbucket.org/ausocean/utils/ring" "bitbucket.org/ausocean/utils/ring"
) )
const ( const (
rbTimeout = 100 * time.Millisecond pkg = "pkg: "
rbNextTimeout = 100 * time.Millisecond rbTimeout = 100 * time.Millisecond
rbLen = 200 rbNextTimeout = 100 * time.Millisecond
rbLen = 200
defaultSampleRate = 48000
) )
const ( const (
@ -52,30 +57,31 @@ const (
stopped stopped
) )
// Rates contains the audio sample rates used by revid. // Rates contains the audio sample rates used by audio.
var Rates = [8]int{8000, 16000, 32000, 44100, 48000, 88200, 96000, 192000} var Rates = [8]int{8000, 16000, 32000, 44100, 48000, 88200, 96000, 192000}
// AudioDevice holds everything we need to know about the audio input stream. // Device holds everything we need to know about the audio input stream.
type AudioDevice struct { type Device struct {
l Logger l Logger
mu sync.Mutex
source string // Name of audio source, or empty for the default source.
// Operating mode, either running, paused, or stopped. // Operating mode, either running, paused, or stopped.
// "running" means the input goroutine is reading from the ALSA device and writing to the ringbuffer. // "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. // "paused" means the input routine is sleeping until unpaused or stopped.
// "stopped" means the input routine is stopped and the ALSA device is closed. // "stopped" means the input routine is stopped and the ALSA device is closed.
mode uint8 mode uint8
mu sync.Mutex
title string // Name of audio title, or empty for the default title.
dev *alsa.Device // Audio input device. dev *alsa.Device // Audio input device.
ab alsa.Buffer // ALSA's buffer. ab alsa.Buffer // ALSA's buffer.
rb *ring.Buffer // Our buffer. rb *ring.Buffer // Our buffer.
chunkSize int // This is the number of bytes that will be stored at a time. chunkSize int // This is the number of bytes that will be stored at a time.
*AudioConfig *Config
} }
// AudioConfig provides parameters used by AudioDevice. // Config provides parameters used by Device.
type AudioConfig struct { type Config struct {
SampleRate int SampleRate int
Channels int Channels int
BitDepth int BitDepth int
@ -83,10 +89,17 @@ type AudioConfig struct {
Codec uint8 Codec uint8
} }
// NewAudioDevice initializes and returns an AudioDevice which can be started, read from, and stopped. // Logger enables any implementation of a logger to be used.
func NewAudioDevice(cfg *AudioConfig, l Logger) (*AudioDevice, error) { // TODO: Make this part of the logger package.
a := &AudioDevice{} type Logger interface {
a.AudioConfig = cfg SetLevel(int8)
Log(level int8, message string, params ...interface{})
}
// NewDevice initializes and returns an Device which can be started, read from, and stopped.
func NewDevice(cfg *Config, l Logger) (*Device, error) {
a := &Device{}
a.Config = cfg
a.l = l a.l = l
// Open the requested audio device. // Open the requested audio device.
@ -100,10 +113,10 @@ func NewAudioDevice(cfg *AudioConfig, l Logger) (*AudioDevice, error) {
a.ab = a.dev.NewBufferDuration(time.Duration(a.RecPeriod * float64(time.Second))) a.ab = a.dev.NewBufferDuration(time.Duration(a.RecPeriod * float64(time.Second)))
cs := (float64((len(a.ab.Data)/a.dev.BufferFormat().Channels)*a.Channels) / float64(a.dev.BufferFormat().Rate)) * float64(a.SampleRate) cs := (float64((len(a.ab.Data)/a.dev.BufferFormat().Channels)*a.Channels) / float64(a.dev.BufferFormat().Rate)) * float64(a.SampleRate)
if cs < 1 { if cs < 1 {
a.l.Log(logger.Error, pkg+"given AudioConfig parameters are too small", "error", err.Error()) a.l.Log(logger.Error, pkg+"given Config parameters are too small", "error", err.Error())
return nil, errors.New("given AudioConfig parameters are too small") return nil, errors.New("given Config parameters are too small")
} }
if a.Codec == ADPCM { if a.Codec == codecutil.ADPCM {
a.chunkSize = adpcm.EncBytes(int(cs)) a.chunkSize = adpcm.EncBytes(int(cs))
} else { } else {
a.chunkSize = int(cs) a.chunkSize = int(cs)
@ -117,7 +130,7 @@ func NewAudioDevice(cfg *AudioConfig, l Logger) (*AudioDevice, error) {
} }
// Start will start recording audio and writing to the ringbuffer. // Start will start recording audio and writing to the ringbuffer.
func (a *AudioDevice) Start() error { func (a *Device) Start() error {
a.mu.Lock() a.mu.Lock()
mode := a.mode mode := a.mode
a.mu.Unlock() a.mu.Unlock()
@ -138,23 +151,23 @@ func (a *AudioDevice) Start() error {
} }
// Stop will stop recording audio and close the device. // Stop will stop recording audio and close the device.
func (a *AudioDevice) Stop() { func (a *Device) Stop() {
a.mu.Lock() a.mu.Lock()
a.mode = stopped a.mode = stopped
a.mu.Unlock() a.mu.Unlock()
} }
// ChunkSize returns the number of bytes written to the ringbuffer per a.RecPeriod. // ChunkSize returns the number of bytes written to the ringbuffer per a.RecPeriod.
func (a *AudioDevice) ChunkSize() int { func (a *Device) ChunkSize() int {
return a.chunkSize return a.chunkSize
} }
// open the recording device with the given name and prepare it to record. // open the recording device with the given name and prepare it to record.
// If name is empty, the first recording device is used. // If name is empty, the first recording device is used.
func (a *AudioDevice) open() error { func (a *Device) open() error {
// Close any existing device. // Close any existing device.
if a.dev != nil { if a.dev != nil {
a.l.Log(logger.Debug, pkg+"closing device", "source", a.source) a.l.Log(logger.Debug, pkg+"closing device", "title", a.title)
a.dev.Close() a.dev.Close()
a.dev = nil a.dev = nil
} }
@ -178,7 +191,7 @@ func (a *AudioDevice) open() error {
if dev.Type != alsa.PCM || !dev.Record { if dev.Type != alsa.PCM || !dev.Record {
continue continue
} }
if dev.Title == a.source || a.source == "" { if dev.Title == a.title || a.title == "" {
a.dev = dev a.dev = dev
break break
} }
@ -189,7 +202,7 @@ func (a *AudioDevice) open() error {
return errors.New("no audio device found") return errors.New("no audio device found")
} }
a.l.Log(logger.Debug, pkg+"opening audio device", "source", a.dev.Title) a.l.Log(logger.Debug, pkg+"opening audio device", "title", a.dev.Title)
err = a.dev.Open() err = a.dev.Open()
if err != nil { if err != nil {
a.l.Log(logger.Debug, pkg+"failed to open audio device") a.l.Log(logger.Debug, pkg+"failed to open audio device")
@ -261,7 +274,7 @@ func (a *AudioDevice) open() error {
// input continously records audio and writes it to the ringbuffer. // 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 ASLA returns an error.
func (a *AudioDevice) input() { func (a *Device) input() {
for { for {
// Check mode. // Check mode.
a.mu.Lock() a.mu.Lock()
@ -273,7 +286,7 @@ func (a *AudioDevice) input() {
continue continue
case stopped: case stopped:
if a.dev != nil { if a.dev != nil {
a.l.Log(logger.Debug, pkg+"closing audio device", "source", a.source) a.l.Log(logger.Debug, pkg+"closing audio device", "title", a.title)
a.dev.Close() a.dev.Close()
a.dev = nil a.dev = nil
} }
@ -311,9 +324,8 @@ func (a *AudioDevice) input() {
} }
} }
// Read reads a full PCM chunk from the ringbuffer, returning the number of bytes read upon success. // Read reads from the ringbuffer, returning the number of bytes read upon success.
// Any errors returned are unexpected and should be considered fatal. func (a *Device) Read(p []byte) (n int, err error) {
func (a *AudioDevice) Read(p []byte) (n int, err error) {
// Ready ringbuffer for read. // Ready ringbuffer for read.
_, err = a.rb.Next(rbNextTimeout) _, err = a.rb.Next(rbNextTimeout)
switch err { switch err {
@ -338,7 +350,7 @@ func (a *AudioDevice) Read(p []byte) (n int, err error) {
} }
// formatBuffer returns audio that has been converted to the desired format. // formatBuffer returns audio that has been converted to the desired format.
func (a *AudioDevice) formatBuffer() alsa.Buffer { func (a *Device) formatBuffer() alsa.Buffer {
var err error var err error
// If nothing needs to be changed, return the original. // If nothing needs to be changed, return the original.
@ -367,8 +379,8 @@ func (a *AudioDevice) formatBuffer() alsa.Buffer {
} }
switch a.Codec { switch a.Codec {
case PCM: case codecutil.PCM:
case ADPCM: case codecutil.ADPCM:
b := bytes.NewBuffer(make([]byte, 0, adpcm.EncBytes(len(formatted.Data)))) b := bytes.NewBuffer(make([]byte, 0, adpcm.EncBytes(len(formatted.Data))))
enc := adpcm.NewEncoder(b) enc := adpcm.NewEncoder(b)
_, err = enc.Write(formatted.Data) _, err = enc.Write(formatted.Data)

View File

@ -1,20 +1,43 @@
package revid /*
NAME
audio_test.go
AUTHOR
Trek Hopton <trek@ausocean.org>
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 ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"os" "os"
"runtime"
"testing" "testing"
"time" "time"
"bitbucket.org/ausocean/av/codec/codecutil" "bitbucket.org/ausocean/av/codec/codecutil"
"bitbucket.org/ausocean/utils/logger"
"github.com/yobert/alsa" "github.com/yobert/alsa"
) )
// Check that a device exists with the given config parameters. // Check that a device exists with the given config parameters.
func checkDevice(ac *AudioConfig) error { func checkDevice(ac *Config) error {
cards, err := alsa.OpenCards() cards, err := alsa.OpenCards()
if err != nil { if err != nil {
return errors.New("no audio cards found") return errors.New("no audio cards found")
@ -89,35 +112,14 @@ func checkDevice(ac *AudioConfig) error {
return nil return nil
} }
// rTestLogger implements a revid.Logger. func TestDevice(t *testing.T) {
type rTestLogger struct{}
func (tl rTestLogger) SetLevel(level int8) {}
func (tl rTestLogger) Log(level int8, msg string, params ...interface{}) {
logLevels := [...]string{"Debug", "Info", "Warn", "Error", "", "", "Fatal"}
if level < -1 || level > 5 {
panic("Invalid log level")
}
if !silent {
fmt.Printf("%s: %s\n", logLevels[level+1], msg)
}
if level == 5 {
buf := make([]byte, 1<<16)
size := runtime.Stack(buf, true)
fmt.Printf("%s\n", string(buf[:size]))
os.Exit(1)
}
}
func TestAudioDevice(t *testing.T) {
// We want to open a device with a standard configuration. // We want to open a device with a standard configuration.
ac := &AudioConfig{ ac := &Config{
SampleRate: 8000, SampleRate: 8000,
Channels: 1, Channels: 1,
RecPeriod: 0.5, RecPeriod: 0.3,
BitDepth: 16, BitDepth: 16,
Codec: ADPCM, Codec: codecutil.ADPCM,
} }
n := 2 // Number of periods to wait while recording. n := 2 // Number of periods to wait while recording.
@ -127,9 +129,9 @@ func TestAudioDevice(t *testing.T) {
t.Skip(err) t.Skip(err)
} }
// Create a new audioDevice, start, read/lex, and then stop it. // Create a new audio Device, start, read/lex, and then stop it.
var l rTestLogger l := logger.New(logger.Debug, os.Stderr)
ai, err := NewAudioDevice(ac, l) ai, err := NewDevice(ac, l)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -139,6 +141,6 @@ func TestAudioDevice(t *testing.T) {
t.Error(err) t.Error(err)
} }
go codecutil.LexBytes(dst, ai, time.Duration(ac.RecPeriod*float64(time.Second)), ai.ChunkSize()) go codecutil.LexBytes(dst, ai, time.Duration(ac.RecPeriod*float64(time.Second)), ai.ChunkSize())
time.Sleep(time.Second * time.Duration(ac.RecPeriod) * time.Duration(n)) time.Sleep(time.Duration(ac.RecPeriod*float64(time.Second)) * time.Duration(n))
ai.Stop() ai.Stop()
} }

View File

@ -28,6 +28,7 @@ package revid
import ( import (
"errors" "errors"
"bitbucket.org/ausocean/av/codec/codecutil"
"bitbucket.org/ausocean/utils/logger" "bitbucket.org/ausocean/utils/logger"
) )
@ -116,14 +117,9 @@ const (
NothingDefined = iota NothingDefined = iota
Raspivid Raspivid
V4L V4L
H264Codec
Audio Audio
File File
Http Http
H264
Mjpeg
PCM
ADPCM
None None
Mpegts Mpegts
Ffmpeg Ffmpeg
@ -157,7 +153,7 @@ const (
defaultQuantizationMode = QuantizationOff defaultQuantizationMode = QuantizationOff
defaultFramesPerClip = 1 defaultFramesPerClip = 1
httpFramesPerClip = 560 httpFramesPerClip = 560
defaultInputCodec = H264 defaultInputCodec = codecutil.H264
defaultVerbosity = logger.Error defaultVerbosity = logger.Error
defaultRtpAddr = "localhost:6970" defaultRtpAddr = "localhost:6970"
defaultBurstPeriod = 10 // Seconds defaultBurstPeriod = 10 // Seconds
@ -166,7 +162,7 @@ const (
defaultExposure = "auto" defaultExposure = "auto"
defaultAutoWhiteBalance = "auto" defaultAutoWhiteBalance = "auto"
defaultAudioInputCodec = ADPCM defaultAudioInputCodec = codecutil.ADPCM
defaultSampleRate = 48000 defaultSampleRate = 48000
defaultBitDepth = 16 defaultBitDepth = 16
defaultChannels = 1 defaultChannels = 1
@ -197,7 +193,7 @@ func (c *Config) Validate(r *Revid) error {
} }
switch c.InputCodec { switch c.InputCodec {
case H264: case codecutil.H264:
// FIXME(kortschak): This is not really what we want. // FIXME(kortschak): This is not really what we want.
// Configuration really needs to be rethought here. // Configuration really needs to be rethought here.
if c.Quantize && c.Quantization == 0 { if c.Quantize && c.Quantization == 0 {
@ -208,12 +204,12 @@ func (c *Config) Validate(r *Revid) error {
return errors.New("bad bitrate and quantization combination for H264 input") return errors.New("bad bitrate and quantization combination for H264 input")
} }
case Mjpeg: case codecutil.MJPEG:
if c.Quantization > 0 || c.Bitrate == 0 { if c.Quantization > 0 || c.Bitrate == 0 {
return errors.New("bad bitrate or quantization for mjpeg input") return errors.New("bad bitrate or quantization for mjpeg input")
} }
case PCM, ADPCM: case codecutil.PCM, codecutil.ADPCM:
case NothingDefined: default:
switch c.Input { switch c.Input {
case Audio: case Audio:
c.Logger.Log(logger.Info, pkg+"input is audio but no codec defined, defaulting", "inputCodec", defaultAudioInputCodec) c.Logger.Log(logger.Info, pkg+"input is audio but no codec defined, defaulting", "inputCodec", defaultAudioInputCodec)
@ -224,8 +220,6 @@ func (c *Config) Validate(r *Revid) error {
c.Logger.Log(logger.Info, pkg+"defaulting quantization", "quantization", defaultQuantization) c.Logger.Log(logger.Info, pkg+"defaulting quantization", "quantization", defaultQuantization)
c.Quantization = defaultQuantization c.Quantization = defaultQuantization
} }
default:
return errors.New("bad input codec defined in config")
} }
if c.Outputs == nil { if c.Outputs == nil {

View File

@ -45,6 +45,7 @@ import (
"bitbucket.org/ausocean/av/codec/h265" "bitbucket.org/ausocean/av/codec/h265"
"bitbucket.org/ausocean/av/container/flv" "bitbucket.org/ausocean/av/container/flv"
"bitbucket.org/ausocean/av/container/mts" "bitbucket.org/ausocean/av/container/mts"
"bitbucket.org/ausocean/av/input/audio"
"bitbucket.org/ausocean/av/protocol/rtcp" "bitbucket.org/ausocean/av/protocol/rtcp"
"bitbucket.org/ausocean/av/protocol/rtp" "bitbucket.org/ausocean/av/protocol/rtp"
"bitbucket.org/ausocean/av/protocol/rtsp" "bitbucket.org/ausocean/av/protocol/rtsp"
@ -539,7 +540,7 @@ func (r *Revid) startRaspivid() (func() error, error) {
switch r.config.InputCodec { switch r.config.InputCodec {
default: default:
return nil, fmt.Errorf("revid: invalid input codec: %v", r.config.InputCodec) return nil, fmt.Errorf("revid: invalid input codec: %v", r.config.InputCodec)
case H264: case codecutil.H264:
args = append(args, args = append(args,
"--codec", "H264", "--codec", "H264",
"--inline", "--inline",
@ -548,7 +549,7 @@ func (r *Revid) startRaspivid() (func() error, error) {
if r.config.Quantize { if r.config.Quantize {
args = append(args, "-qp", fmt.Sprint(r.config.Quantization)) args = append(args, "-qp", fmt.Sprint(r.config.Quantization))
} }
case Mjpeg: case codecutil.MJPEG:
args = append(args, "--codec", "MJPEG") args = append(args, "--codec", "MJPEG")
} }
r.config.Logger.Log(logger.Info, pkg+"raspivid args", "raspividArgs", strings.Join(args, " ")) r.config.Logger.Log(logger.Info, pkg+"raspivid args", "raspividArgs", strings.Join(args, " "))
@ -628,7 +629,7 @@ func (r *Revid) setupInputForFile() (func() error, error) {
// startAudioDevice is used to start capturing audio from an audio device and processing it. // startAudioDevice is used to start capturing audio from an audio device and processing it.
func (r *Revid) startAudioDevice() (func() error, error) { func (r *Revid) startAudioDevice() (func() error, error) {
// Create audio device. // Create audio device.
ac := &AudioConfig{ ac := &audio.Config{
SampleRate: r.config.SampleRate, SampleRate: r.config.SampleRate,
Channels: r.config.Channels, Channels: r.config.Channels,
RecPeriod: r.config.RecPeriod, RecPeriod: r.config.RecPeriod,
@ -640,15 +641,15 @@ func (r *Revid) startAudioDevice() (func() error, error) {
mts.Meta.Add("period", fmt.Sprintf("%.6f", r.config.RecPeriod)) mts.Meta.Add("period", fmt.Sprintf("%.6f", r.config.RecPeriod))
mts.Meta.Add("bitDepth", strconv.Itoa(r.config.BitDepth)) mts.Meta.Add("bitDepth", strconv.Itoa(r.config.BitDepth))
switch r.config.InputCodec { switch r.config.InputCodec {
case PCM: case codecutil.PCM:
mts.Meta.Add("codec", "pcm") mts.Meta.Add("codec", "pcm")
case ADPCM: case codecutil.ADPCM:
mts.Meta.Add("codec", "adpcm") mts.Meta.Add("codec", "adpcm")
default: default:
r.config.Logger.Log(logger.Fatal, pkg+"no audio codec set in config") r.config.Logger.Log(logger.Fatal, pkg+"no audio codec set in config")
} }
ai, err := NewAudioDevice(ac, r.config.Logger) ai, err := audio.NewDevice(ac, r.config.Logger)
if err != nil { if err != nil {
r.config.Logger.Log(logger.Fatal, pkg+"failed to create audio device", "error", err.Error()) r.config.Logger.Log(logger.Fatal, pkg+"failed to create audio device", "error", err.Error())
} }