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"
"time"
"bitbucket.org/ausocean/av/codec/codecutil"
"bitbucket.org/ausocean/av/container/mts"
"bitbucket.org/ausocean/av/container/mts/meta"
"bitbucket.org/ausocean/av/revid"
@ -200,11 +201,11 @@ func handleFlags() revid.Config {
switch *inputCodecPtr {
case "H264":
cfg.InputCodec = revid.H264
cfg.InputCodec = codecutil.H264
case "PCM":
cfg.InputCodec = revid.PCM
cfg.InputCodec = codecutil.PCM
case "ADPCM":
cfg.InputCodec = revid.ADPCM
cfg.InputCodec = codecutil.ADPCM
case "":
default:
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
audio-input.go
audio.go
AUTHOR
Alan Noble <alan@ausocean.org>
Trek Hopton <trek@ausocean.org>
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
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).
*/
package revid
// Package audio provides access to input from audio devices.
package audio
import (
"bytes"
@ -35,15 +37,18 @@ import (
"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/utils/logger"
"bitbucket.org/ausocean/utils/ring"
)
const (
pkg = "pkg: "
rbTimeout = 100 * time.Millisecond
rbNextTimeout = 100 * time.Millisecond
rbLen = 200
defaultSampleRate = 48000
)
const (
@ -52,30 +57,31 @@ const (
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}
// AudioDevice holds everything we need to know about the audio input stream.
type AudioDevice struct {
// Device holds everything we need to know about the audio input stream.
type Device struct {
l Logger
mu sync.Mutex
source string // Name of audio source, or empty for the default source.
// Operating mode, either running, paused, or stopped.
// "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.
mode uint8
mu sync.Mutex
title string // Name of audio title, or empty for the default title.
dev *alsa.Device // 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 at a time.
*AudioConfig
*Config
}
// AudioConfig provides parameters used by AudioDevice.
type AudioConfig struct {
// Config provides parameters used by Device.
type Config struct {
SampleRate int
Channels int
BitDepth int
@ -83,10 +89,17 @@ type AudioConfig struct {
Codec uint8
}
// NewAudioDevice initializes and returns an AudioDevice which can be started, read from, and stopped.
func NewAudioDevice(cfg *AudioConfig, l Logger) (*AudioDevice, error) {
a := &AudioDevice{}
a.AudioConfig = cfg
// 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{})
}
// 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
// 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)))
cs := (float64((len(a.ab.Data)/a.dev.BufferFormat().Channels)*a.Channels) / float64(a.dev.BufferFormat().Rate)) * float64(a.SampleRate)
if cs < 1 {
a.l.Log(logger.Error, pkg+"given AudioConfig parameters are too small", "error", err.Error())
return nil, errors.New("given AudioConfig parameters are too small")
a.l.Log(logger.Error, pkg+"given Config parameters are too small", "error", err.Error())
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))
} else {
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.
func (a *AudioDevice) Start() error {
func (a *Device) Start() error {
a.mu.Lock()
mode := a.mode
a.mu.Unlock()
@ -138,23 +151,23 @@ func (a *AudioDevice) Start() error {
}
// Stop will stop recording audio and close the device.
func (a *AudioDevice) Stop() {
func (a *Device) Stop() {
a.mu.Lock()
a.mode = stopped
a.mu.Unlock()
}
// 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
}
// open the recording device with the given name and prepare it to record.
// If name is empty, the first recording device is used.
func (a *AudioDevice) open() error {
func (a *Device) open() error {
// Close any existing device.
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 = nil
}
@ -178,7 +191,7 @@ func (a *AudioDevice) open() error {
if dev.Type != alsa.PCM || !dev.Record {
continue
}
if dev.Title == a.source || a.source == "" {
if dev.Title == a.title || a.title == "" {
a.dev = dev
break
}
@ -189,7 +202,7 @@ func (a *AudioDevice) open() error {
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()
if err != nil {
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.
// Re-opens the device and tries again if ASLA returns an error.
func (a *AudioDevice) input() {
func (a *Device) input() {
for {
// Check mode.
a.mu.Lock()
@ -273,7 +286,7 @@ func (a *AudioDevice) input() {
continue
case stopped:
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 = 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.
// Any errors returned are unexpected and should be considered fatal.
func (a *AudioDevice) Read(p []byte) (n int, err error) {
// Read reads from the ringbuffer, returning the number of bytes read upon success.
func (a *Device) Read(p []byte) (n int, err error) {
// Ready ringbuffer for read.
_, err = a.rb.Next(rbNextTimeout)
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.
func (a *AudioDevice) formatBuffer() alsa.Buffer {
func (a *Device) formatBuffer() alsa.Buffer {
var err error
// If nothing needs to be changed, return the original.
@ -367,8 +379,8 @@ func (a *AudioDevice) formatBuffer() alsa.Buffer {
}
switch a.Codec {
case PCM:
case ADPCM:
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)

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 (
"bytes"
"errors"
"fmt"
"os"
"runtime"
"testing"
"time"
"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 *AudioConfig) error {
func checkDevice(ac *Config) error {
cards, err := alsa.OpenCards()
if err != nil {
return errors.New("no audio cards found")
@ -89,35 +112,14 @@ func checkDevice(ac *AudioConfig) error {
return nil
}
// rTestLogger implements a revid.Logger.
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) {
func TestDevice(t *testing.T) {
// We want to open a device with a standard configuration.
ac := &AudioConfig{
ac := &Config{
SampleRate: 8000,
Channels: 1,
RecPeriod: 0.5,
RecPeriod: 0.3,
BitDepth: 16,
Codec: ADPCM,
Codec: codecutil.ADPCM,
}
n := 2 // Number of periods to wait while recording.
@ -128,8 +130,8 @@ func TestAudioDevice(t *testing.T) {
}
// Create a new audio Device, start, read/lex, and then stop it.
var l rTestLogger
ai, err := NewAudioDevice(ac, l)
l := logger.New(logger.Debug, os.Stderr)
ai, err := NewDevice(ac, l)
if err != nil {
t.Error(err)
}
@ -139,6 +141,6 @@ func TestAudioDevice(t *testing.T) {
t.Error(err)
}
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()
}

View File

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

View File

@ -45,6 +45,7 @@ import (
"bitbucket.org/ausocean/av/codec/h265"
"bitbucket.org/ausocean/av/container/flv"
"bitbucket.org/ausocean/av/container/mts"
"bitbucket.org/ausocean/av/input/audio"
"bitbucket.org/ausocean/av/protocol/rtcp"
"bitbucket.org/ausocean/av/protocol/rtp"
"bitbucket.org/ausocean/av/protocol/rtsp"
@ -539,7 +540,7 @@ func (r *Revid) startRaspivid() (func() error, error) {
switch r.config.InputCodec {
default:
return nil, fmt.Errorf("revid: invalid input codec: %v", r.config.InputCodec)
case H264:
case codecutil.H264:
args = append(args,
"--codec", "H264",
"--inline",
@ -548,7 +549,7 @@ func (r *Revid) startRaspivid() (func() error, error) {
if r.config.Quantize {
args = append(args, "-qp", fmt.Sprint(r.config.Quantization))
}
case Mjpeg:
case codecutil.MJPEG:
args = append(args, "--codec", "MJPEG")
}
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.
func (r *Revid) startAudioDevice() (func() error, error) {
// Create audio device.
ac := &AudioConfig{
ac := &audio.Config{
SampleRate: r.config.SampleRate,
Channels: r.config.Channels,
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("bitDepth", strconv.Itoa(r.config.BitDepth))
switch r.config.InputCodec {
case PCM:
case codecutil.PCM:
mts.Meta.Add("codec", "pcm")
case ADPCM:
case codecutil.ADPCM:
mts.Meta.Add("codec", "adpcm")
default:
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 {
r.config.Logger.Log(logger.Fatal, pkg+"failed to create audio device", "error", err.Error())
}