mirror of https://bitbucket.org/ausocean/av.git
revid: created an audioInput struct to record audio that acts as a reader for revid
This commit is contained in:
parent
8a1f35c0a5
commit
46ca3e2611
3
go.mod
3
go.mod
|
@ -6,9 +6,12 @@ require (
|
||||||
bitbucket.org/ausocean/iot v1.2.4
|
bitbucket.org/ausocean/iot v1.2.4
|
||||||
bitbucket.org/ausocean/utils v0.0.0-20190408050157-66d3b4d4041e
|
bitbucket.org/ausocean/utils v0.0.0-20190408050157-66d3b4d4041e
|
||||||
github.com/Comcast/gots v0.0.0-20190305015453-8d56e473f0f7
|
github.com/Comcast/gots v0.0.0-20190305015453-8d56e473f0f7
|
||||||
|
github.com/Shopify/toxiproxy v2.1.4+incompatible // indirect
|
||||||
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 // indirect
|
||||||
github.com/go-audio/audio v0.0.0-20181013203223-7b2a6ca21480
|
github.com/go-audio/audio v0.0.0-20181013203223-7b2a6ca21480
|
||||||
github.com/go-audio/wav v0.0.0-20181013172942-de841e69b884
|
github.com/go-audio/wav v0.0.0-20181013172942-de841e69b884
|
||||||
github.com/mewkiz/flac v1.0.5
|
github.com/mewkiz/flac v1.0.5
|
||||||
|
github.com/sergi/go-diff v1.0.0 // indirect
|
||||||
github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e
|
github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
240
revid/audio.go
240
revid/audio.go
|
@ -3,21 +3,20 @@ package revid
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/yobert/alsa"
|
"github.com/yobert/alsa"
|
||||||
|
|
||||||
"bitbucket.org/ausocean/av/codec/pcm"
|
"bitbucket.org/ausocean/av/codec/pcm"
|
||||||
"bitbucket.org/ausocean/iot/pi/netsender"
|
"bitbucket.org/ausocean/iot/pi/smartlogger"
|
||||||
"bitbucket.org/ausocean/iot/pi/sds"
|
|
||||||
"bitbucket.org/ausocean/utils/logger"
|
"bitbucket.org/ausocean/utils/logger"
|
||||||
"bitbucket.org/ausocean/utils/ring"
|
"bitbucket.org/ausocean/utils/ring"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultRate = 48000
|
logPath = "/var/log/netsender"
|
||||||
|
defaultSampRate = 48000
|
||||||
defaultPeriod = 5 // seconds
|
defaultPeriod = 5 // seconds
|
||||||
defaultChannels = 2
|
defaultChannels = 2
|
||||||
defaultBits = 16
|
defaultBits = 16
|
||||||
|
@ -26,6 +25,8 @@ const (
|
||||||
rbNextTimeout = 100 * time.Millisecond
|
rbNextTimeout = 100 * time.Millisecond
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var log *logger.Logger
|
||||||
|
|
||||||
// audioInput holds everything we need to know about the audio input stream.
|
// audioInput holds everything we need to know about the audio input stream.
|
||||||
// NB: At 44100 Hz frame rate, 2 channels and 16-bit samples, a period of 5 seconds
|
// NB: At 44100 Hz frame rate, 2 channels and 16-bit samples, a period of 5 seconds
|
||||||
// results in PCM data chunks of 882000 bytes! A longer period exceeds datastore's 1MB blob limit.
|
// results in PCM data chunks of 882000 bytes! A longer period exceeds datastore's 1MB blob limit.
|
||||||
|
@ -37,7 +38,7 @@ type audioInput struct {
|
||||||
// internals
|
// internals
|
||||||
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 //TODO: change this to output stream, doesn't have to be ring buffer
|
rb *ring.Buffer // our buffer
|
||||||
vs int // our "var sum" to track var changes
|
vs int // our "var sum" to track var changes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,19 +51,29 @@ type parameters struct {
|
||||||
bits int // sample bit size, 16 by default
|
bits int // sample bit size, 16 by default
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAudioInput() {
|
// NewAudioInput starts recording audio and returns an AudioInput which the audio can be read from.
|
||||||
|
func NewAudioInput() io.Reader {
|
||||||
|
logLevel := int(logger.Debug)
|
||||||
|
|
||||||
|
validLogLevel := true
|
||||||
|
if logLevel < int(logger.Debug) || logLevel > int(logger.Fatal) {
|
||||||
|
logLevel = int(logger.Info)
|
||||||
|
validLogLevel = false
|
||||||
|
}
|
||||||
|
|
||||||
|
logSender := smartlogger.New(logPath)
|
||||||
|
log = logger.New(int8(logLevel), &logSender.LogRoller)
|
||||||
|
log.Log(logger.Info, "log-netsender: Logger Initialized")
|
||||||
|
if !validLogLevel {
|
||||||
|
log.Log(logger.Error, "Invalid log level was defaulted to Info")
|
||||||
|
}
|
||||||
|
|
||||||
var ac audioInput
|
var ac audioInput
|
||||||
|
|
||||||
// Get audio params and store the current var sum.
|
ac.setParams()
|
||||||
vars, err := ac.ns.Vars()
|
|
||||||
if err != nil {
|
|
||||||
log.Log(logger.Warning, "netsender.Vars failed; using defaults", "error", err.Error())
|
|
||||||
}
|
|
||||||
ac.params(vars)
|
|
||||||
ac.vs = ac.ns.VarSum()
|
|
||||||
|
|
||||||
// Open the requested audio device.
|
// Open the requested audio device.
|
||||||
err = ac.open()
|
err := ac.open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Log(logger.Fatal, "alsa.open failed", "error", err.Error())
|
log.Log(logger.Fatal, "alsa.open failed", "error", err.Error())
|
||||||
}
|
}
|
||||||
|
@ -75,70 +86,20 @@ func NewAudioInput() {
|
||||||
|
|
||||||
go ac.input()
|
go ac.input()
|
||||||
|
|
||||||
ac.output()
|
return ac
|
||||||
|
|
||||||
return stream
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// params extracts audio params from corresponding NetReceiver vars and returns true if anything has changed.
|
func (ac *audioInput) setParams() {
|
||||||
// See audioInput for a description of the params and their limits.
|
|
||||||
func (ac *audioInput) params(vars map[string]string) bool {
|
|
||||||
// We are the only writers to this field
|
|
||||||
// so we don't need to lock here.
|
|
||||||
p := ac.parameters
|
p := ac.parameters
|
||||||
changed := false
|
|
||||||
|
|
||||||
mode := vars["mode"]
|
p.rate = defaultSampRate
|
||||||
if p.mode != mode {
|
p.period = defaultPeriod
|
||||||
p.mode = mode
|
p.channels = defaultChannels
|
||||||
changed = true
|
p.bits = defaultBits
|
||||||
}
|
|
||||||
source := vars["source"]
|
|
||||||
if p.source != source {
|
|
||||||
p.source = source
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
val, err := strconv.Atoi(vars["rate"])
|
|
||||||
if err != nil {
|
|
||||||
val = defaultRate
|
|
||||||
}
|
|
||||||
if p.rate != val {
|
|
||||||
p.rate = val
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
val, err = strconv.Atoi(vars["period"])
|
|
||||||
if err != nil || val < 1 || 5 < val {
|
|
||||||
val = defaultPeriod
|
|
||||||
}
|
|
||||||
if p.period != val {
|
|
||||||
p.period = val
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
val, err = strconv.Atoi(vars["channels"])
|
|
||||||
if err != nil || (val != 1 && val != 2) {
|
|
||||||
val = defaultChannels
|
|
||||||
}
|
|
||||||
if p.channels != val {
|
|
||||||
p.channels = val
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
val, err = strconv.Atoi(vars["bits"])
|
|
||||||
if err != nil || (val != 16 && val != 32) {
|
|
||||||
val = defaultBits
|
|
||||||
}
|
|
||||||
if p.bits != val {
|
|
||||||
p.bits = val
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if changed {
|
ac.mu.Lock()
|
||||||
ac.mu.Lock()
|
ac.parameters = p
|
||||||
ac.parameters = p
|
ac.mu.Unlock()
|
||||||
ac.mu.Unlock()
|
|
||||||
log.Log(logger.Debug, "Params changed")
|
|
||||||
}
|
|
||||||
log.Log(logger.Debug, "Parameters", "mode", p.mode, "source", p.source, "rate", p.rate, "period", p.period, "channels", p.channels, "bits", p.bits)
|
|
||||||
return changed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// open or re-open the recording device with the given name and prepare it to record.
|
// open or re-open the recording device with the given name and prepare it to record.
|
||||||
|
@ -212,12 +173,12 @@ func (ac *audioInput) 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 {
|
||||||
log.Log(logger.Warning, "No available device sample-rates are divisible by the requested rate. Default rate will be used. Resampling may fail.", "rateRequested", ac.rate)
|
log.Log(logger.Warning, "Unable to sample at requested rate, default used.", "rateRequested", ac.rate)
|
||||||
_, err = ac.dev.NegotiateRate(defaultRate)
|
_, err = ac.dev.NegotiateRate(defaultSampRate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Log(logger.Debug, "Sample rate set", "rate", defaultRate)
|
log.Log(logger.Debug, "Sample rate set", "rate", defaultSampRate)
|
||||||
}
|
}
|
||||||
|
|
||||||
var fmt alsa.FormatType
|
var fmt alsa.FormatType
|
||||||
|
@ -294,131 +255,10 @@ func (ac *audioInput) input() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// output continously reads audio from the ringbuffer and sends it to NetReceiver via poll requests.
|
|
||||||
// When "B0" is configured as one of the NetReceiver inputs, audio data is posted as "B0".
|
|
||||||
// When "B0" is not an input, the poll request happens without any audio data
|
|
||||||
// (although other inputs may still be present via URL parameters).
|
|
||||||
// When paused, polling continues but without sending audio (B0) data.
|
|
||||||
// Sending is throttled so as to complete one pass of this loop approximately every audio period,
|
|
||||||
// since cycling more frequently is pointless.
|
|
||||||
// Finally while audio data is sent every audio period, other data is reported only every monitor period.
|
|
||||||
// This function also handles NetReceiver configuration requests and updating of NetReceiver vars.
|
|
||||||
func (ac *audioInput) output() {
|
|
||||||
// Calculate the size of the output data based on wanted channels and rate.
|
|
||||||
outLen := (((len(ac.ab.Data) / ac.ab.Format.Channels) * ac.channels) / ac.ab.Format.Rate) * ac.rate
|
|
||||||
buf := make([]byte, outLen)
|
|
||||||
|
|
||||||
mime := "audio/x-wav;codec=pcm;rate=" + strconv.Itoa(ac.rate) + ";channels=" + strconv.Itoa(ac.channels) + ";bits=" + strconv.Itoa(ac.bits)
|
|
||||||
ip := ac.ns.Param("ip")
|
|
||||||
mp, err := strconv.Atoi(ac.ns.Param("mp"))
|
|
||||||
if err != nil {
|
|
||||||
log.Log(logger.Fatal, "mp not an integer")
|
|
||||||
}
|
|
||||||
|
|
||||||
report := true // Report non-audio data.
|
|
||||||
reported := time.Now() // When we last did so.
|
|
||||||
|
|
||||||
for {
|
|
||||||
var reconfig bool
|
|
||||||
start := time.Now()
|
|
||||||
audio := false
|
|
||||||
var pins []netsender.Pin
|
|
||||||
|
|
||||||
if ac.mode == "Paused" {
|
|
||||||
|
|
||||||
// Only send X data when paused (if any).
|
|
||||||
if report {
|
|
||||||
pins = netsender.MakePins(ip, "X")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
n, err := read(ac.rb, buf)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if n == 0 {
|
|
||||||
goto sleep
|
|
||||||
}
|
|
||||||
if n != len(buf) {
|
|
||||||
log.Log(logger.Error, "Unexpected length from read", "length", n)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if report {
|
|
||||||
pins = netsender.MakePins(ip, "")
|
|
||||||
} else {
|
|
||||||
pins = netsender.MakePins(ip, "B")
|
|
||||||
}
|
|
||||||
for i, pin := range pins {
|
|
||||||
if pin.Name == "B0" {
|
|
||||||
audio = true
|
|
||||||
pins[i].Value = n
|
|
||||||
pins[i].Data = buf
|
|
||||||
pins[i].MimeType = mime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(report || audio) {
|
|
||||||
goto sleep // nothing to do
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate X pins, if any.
|
|
||||||
for i, pin := range pins {
|
|
||||||
if pin.Name[0] == 'X' {
|
|
||||||
err := sds.ReadSystem(&pins[i])
|
|
||||||
if err != nil {
|
|
||||||
log.Log(logger.Warning, "sds.ReadSystem failed", "error", err.Error())
|
|
||||||
// Pin.Value defaults to -1 upon error, so OK to continue.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_, reconfig, err = ac.ns.Send(netsender.RequestPoll, pins)
|
|
||||||
if err != nil {
|
|
||||||
log.Log(logger.Debug, "netsender.Send failed", "error", err.Error())
|
|
||||||
goto sleep
|
|
||||||
}
|
|
||||||
if report {
|
|
||||||
reported = start
|
|
||||||
report = false
|
|
||||||
}
|
|
||||||
if reconfig {
|
|
||||||
err = ac.ns.Config()
|
|
||||||
if err != nil {
|
|
||||||
log.Log(logger.Warning, "netsender.Config failed", "error", err.Error())
|
|
||||||
goto sleep
|
|
||||||
}
|
|
||||||
ip = ac.ns.Param("ip")
|
|
||||||
mp, err = strconv.Atoi(ac.ns.Param("mp"))
|
|
||||||
if err != nil {
|
|
||||||
log.Log(logger.Fatal, "mp not an integer")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ac.vs != ac.ns.VarSum() {
|
|
||||||
vars, err := ac.ns.Vars()
|
|
||||||
if err != nil {
|
|
||||||
log.Log(logger.Error, "netsender.Vars failed", "error", err.Error())
|
|
||||||
goto sleep
|
|
||||||
}
|
|
||||||
ac.params(vars) // ToDo: re-open device if audio params have changed.
|
|
||||||
ac.vs = ac.ns.VarSum()
|
|
||||||
}
|
|
||||||
|
|
||||||
sleep:
|
|
||||||
pause := ac.period*1000 - int(time.Since(start).Seconds()*1000)
|
|
||||||
if pause > 0 {
|
|
||||||
time.Sleep(time.Duration(pause) * time.Millisecond)
|
|
||||||
}
|
|
||||||
if time.Since(reported).Seconds() >= float64(mp) {
|
|
||||||
report = true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// read reads a full PCM chunk from the ringbuffer, returning the number of bytes read upon success.
|
// 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.
|
// Any errors returned are unexpected and should be considered fatal.
|
||||||
func read(rb *ring.Buffer, buf []byte) (int, error) {
|
func (ac audioInput) Read(p []byte) (n int, err error) {
|
||||||
chunk, err := rb.Next(rbNextTimeout)
|
chunk, err := ac.rb.Next(rbNextTimeout)
|
||||||
switch err {
|
switch err {
|
||||||
case nil:
|
case nil:
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
|
@ -432,7 +272,7 @@ func read(rb *ring.Buffer, buf []byte) (int, error) {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
n, err := io.ReadFull(rb, buf[:chunk.Len()])
|
n, err = io.ReadFull(ac.rb, p[:chunk.Len()])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Log(logger.Error, "Unexpected error from ring.Read", "error", err.Error())
|
log.Log(logger.Error, "Unexpected error from ring.Read", "error", err.Error())
|
||||||
return n, err
|
return n, err
|
||||||
|
|
|
@ -62,6 +62,7 @@ type Config struct {
|
||||||
Height uint
|
Height uint
|
||||||
Width uint
|
Width uint
|
||||||
FrameRate uint
|
FrameRate uint
|
||||||
|
Rate uint
|
||||||
HttpAddress string
|
HttpAddress string
|
||||||
Quantization uint
|
Quantization uint
|
||||||
IntraRefreshPeriod uint
|
IntraRefreshPeriod uint
|
||||||
|
@ -138,6 +139,7 @@ const (
|
||||||
defaultOutput = Http
|
defaultOutput = Http
|
||||||
defaultPacketization = Flv
|
defaultPacketization = Flv
|
||||||
defaultFrameRate = 25
|
defaultFrameRate = 25
|
||||||
|
defaultRate = 25
|
||||||
defaultWidth = 1280
|
defaultWidth = 1280
|
||||||
defaultHeight = 720
|
defaultHeight = 720
|
||||||
defaultIntraRefreshPeriod = 100
|
defaultIntraRefreshPeriod = 100
|
||||||
|
|
|
@ -668,10 +668,12 @@ func (r *Revid) setupInputForFile() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// startMic is used to start capturing audio from an audio device and processing it.
|
// startAudioInput is used to start capturing audio from an audio device and processing it.
|
||||||
func startMic() {
|
func (r *Revid) startAudioInput() error {
|
||||||
|
|
||||||
go processFrom(stream, r.config.Rate)
|
ai := NewAudioInput()
|
||||||
|
|
||||||
|
go r.processFrom(ai, time.Second/time.Duration(r.config.Rate))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue