mirror of https://bitbucket.org/ausocean/av.git
pcm: refactored to be general not alsa only
Addition of new structs and helper functions for passing around pcm clips/buffers and their formats so that we don't have to import and rely on yobert/alsa code. Updated any commands and alsa package to use refactored code.
This commit is contained in:
parent
7da3778485
commit
5e472ba4c9
|
@ -46,7 +46,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/yobert/alsa"
|
yalsa "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/netsender"
|
||||||
|
@ -78,11 +78,11 @@ type audioClient struct {
|
||||||
parameters
|
parameters
|
||||||
|
|
||||||
// internals
|
// internals
|
||||||
dev *alsa.Device // audio input device
|
dev *yalsa.Device // audio input device
|
||||||
ab alsa.Buffer // ALSA's buffer
|
clip pcm.Clip // Clip to contain the recording.
|
||||||
rb *ring.Buffer // our buffer
|
rb *ring.Buffer // our buffer
|
||||||
ns *netsender.Sender // our NetSender
|
ns *netsender.Sender // our NetSender
|
||||||
vs int // our "var sum" to track var changes
|
vs int // our "var sum" to track var changes
|
||||||
}
|
}
|
||||||
|
|
||||||
type parameters struct {
|
type parameters struct {
|
||||||
|
@ -132,12 +132,26 @@ func main() {
|
||||||
// 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, "yalsa.open failed", "error", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture audio in periods of ac.period seconds, and buffer rbDuration seconds in total.
|
// Capture audio in periods of ac.period seconds, and buffer rbDuration seconds in total.
|
||||||
ac.ab = ac.dev.NewBufferDuration(time.Second * time.Duration(ac.period))
|
ab := ac.dev.NewBufferDuration(time.Second * time.Duration(ac.period))
|
||||||
recSize := (((len(ac.ab.Data) / ac.dev.BufferFormat().Channels) * ac.channels) / ac.dev.BufferFormat().Rate) * ac.rate
|
sf, err := pcm.SFFromString(ab.Format.SampleFormat.String())
|
||||||
|
if err != nil {
|
||||||
|
log.Log(logger.Error, err.Error())
|
||||||
|
}
|
||||||
|
cf := pcm.ClipFormat{
|
||||||
|
SFormat: sf,
|
||||||
|
Channels: ab.Format.Channels,
|
||||||
|
Rate: ab.Format.Rate,
|
||||||
|
}
|
||||||
|
ac.clip = pcm.Clip{
|
||||||
|
Format: cf,
|
||||||
|
Data: ab.Data,
|
||||||
|
}
|
||||||
|
|
||||||
|
recSize := (((len(ac.clip.Data) / ac.dev.BufferFormat().Channels) * ac.channels) / ac.dev.BufferFormat().Rate) * ac.rate
|
||||||
rbLen := rbDuration / ac.period
|
rbLen := rbDuration / ac.period
|
||||||
ac.rb = ring.NewBuffer(rbLen, recSize, rbTimeout)
|
ac.rb = ring.NewBuffer(rbLen, recSize, rbTimeout)
|
||||||
|
|
||||||
|
@ -217,11 +231,11 @@ func (ac *audioClient) open() error {
|
||||||
}
|
}
|
||||||
log.Log(logger.Debug, "opening", "source", ac.source)
|
log.Log(logger.Debug, "opening", "source", ac.source)
|
||||||
|
|
||||||
cards, err := alsa.OpenCards()
|
cards, err := yalsa.OpenCards()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer alsa.CloseCards(cards)
|
defer yalsa.CloseCards(cards)
|
||||||
|
|
||||||
for _, card := range cards {
|
for _, card := range cards {
|
||||||
devices, err := card.Devices()
|
devices, err := card.Devices()
|
||||||
|
@ -229,7 +243,7 @@ func (ac *audioClient) open() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, dev := range devices {
|
for _, dev := range devices {
|
||||||
if dev.Type != alsa.PCM || !dev.Record {
|
if dev.Type != yalsa.PCM || !dev.Record {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if dev.Title == ac.source || ac.source == "" {
|
if dev.Title == ac.source || ac.source == "" {
|
||||||
|
@ -287,12 +301,12 @@ func (ac *audioClient) open() error {
|
||||||
log.Log(logger.Debug, "sample rate set", "rate", defaultFrameRate)
|
log.Log(logger.Debug, "sample rate set", "rate", defaultFrameRate)
|
||||||
}
|
}
|
||||||
|
|
||||||
var fmt alsa.FormatType
|
var fmt yalsa.FormatType
|
||||||
switch ac.bits {
|
switch ac.bits {
|
||||||
case 16:
|
case 16:
|
||||||
fmt = alsa.S16_LE
|
fmt = yalsa.S16_LE
|
||||||
case 32:
|
case 32:
|
||||||
fmt = alsa.S32_LE
|
fmt = yalsa.S32_LE
|
||||||
default:
|
default:
|
||||||
return errors.New("unsupported sample bits")
|
return errors.New("unsupported sample bits")
|
||||||
}
|
}
|
||||||
|
@ -318,7 +332,7 @@ func (ac *audioClient) open() error {
|
||||||
// Re-opens the device and tries again if ASLA returns an error.
|
// Re-opens the device and tries again if ASLA returns an error.
|
||||||
// Spends a lot of time sleeping in Paused mode.
|
// Spends a lot of time sleeping in Paused mode.
|
||||||
// ToDo: Currently, reading audio and writing to the ringbuffer are synchronous.
|
// ToDo: Currently, reading audio and writing to the ringbuffer are synchronous.
|
||||||
// Need a way to asynchronously read from the ALSA buffer, i.e., _while_ it is recording to avoid any gaps.
|
// Need a way to asynchronously read from the clip, i.e., _while_ it is recording to avoid any gaps.
|
||||||
func (ac *audioClient) input() {
|
func (ac *audioClient) input() {
|
||||||
for {
|
for {
|
||||||
ac.mu.Lock()
|
ac.mu.Lock()
|
||||||
|
@ -330,14 +344,14 @@ func (ac *audioClient) input() {
|
||||||
}
|
}
|
||||||
log.Log(logger.Debug, "recording audio for period", "seconds", ac.period)
|
log.Log(logger.Debug, "recording audio for period", "seconds", ac.period)
|
||||||
ac.mu.Lock()
|
ac.mu.Lock()
|
||||||
err := ac.dev.Read(ac.ab.Data)
|
err := ac.dev.Read(ac.clip.Data)
|
||||||
ac.mu.Unlock()
|
ac.mu.Unlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Log(logger.Debug, "device.Read failed", "error", err.Error())
|
log.Log(logger.Debug, "device.Read failed", "error", err.Error())
|
||||||
ac.mu.Lock()
|
ac.mu.Lock()
|
||||||
err = ac.open() // re-open
|
err = ac.open() // re-open
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Log(logger.Fatal, "alsa.open failed", "error", err.Error())
|
log.Log(logger.Fatal, "yalsa.open failed", "error", err.Error())
|
||||||
}
|
}
|
||||||
ac.mu.Unlock()
|
ac.mu.Unlock()
|
||||||
continue
|
continue
|
||||||
|
@ -372,7 +386,7 @@ func (ac *audioClient) input() {
|
||||||
// This function also handles NetReceiver configuration requests and updating of NetReceiver vars.
|
// This function also handles NetReceiver configuration requests and updating of NetReceiver vars.
|
||||||
func (ac *audioClient) output() {
|
func (ac *audioClient) output() {
|
||||||
// Calculate the size of the output data based on wanted channels and rate.
|
// 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
|
outLen := (((len(ac.clip.Data) / ac.clip.Format.Channels) * ac.channels) / ac.clip.Format.Rate) * ac.rate
|
||||||
buf := make([]byte, outLen)
|
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)
|
mime := "audio/x-wav;codec=pcm;rate=" + strconv.Itoa(ac.rate) + ";channels=" + strconv.Itoa(ac.channels) + ";bits=" + strconv.Itoa(ac.bits)
|
||||||
|
@ -509,9 +523,9 @@ func read(rb *ring.Buffer, buf []byte) (int, error) {
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatBuffer returns an ALSA buffer that has the recording data from the ac's original ALSA buffer but stored
|
// formatBuffer returns a Clip that has the recording data from the ac's original Clip but stored
|
||||||
// in the desired format specified by the ac's parameters.
|
// in the desired format specified by the ac's parameters.
|
||||||
func (ac *audioClient) formatBuffer() alsa.Buffer {
|
func (ac *audioClient) formatBuffer() pcm.Clip {
|
||||||
var err error
|
var err error
|
||||||
ac.mu.Lock()
|
ac.mu.Lock()
|
||||||
wantChannels := ac.channels
|
wantChannels := ac.channels
|
||||||
|
@ -519,17 +533,17 @@ func (ac *audioClient) formatBuffer() alsa.Buffer {
|
||||||
ac.mu.Unlock()
|
ac.mu.Unlock()
|
||||||
|
|
||||||
// If nothing needs to be changed, return the original.
|
// If nothing needs to be changed, return the original.
|
||||||
if ac.ab.Format.Channels == wantChannels && ac.ab.Format.Rate == wantRate {
|
if ac.clip.Format.Channels == wantChannels && ac.clip.Format.Rate == wantRate {
|
||||||
return ac.ab
|
return ac.clip
|
||||||
}
|
}
|
||||||
|
|
||||||
formatted := alsa.Buffer{Format: ac.ab.Format}
|
formatted := pcm.Clip{Format: ac.clip.Format}
|
||||||
bufCopied := false
|
bufCopied := false
|
||||||
if ac.ab.Format.Channels != wantChannels {
|
if ac.clip.Format.Channels != wantChannels {
|
||||||
|
|
||||||
// Convert channels.
|
// Convert channels.
|
||||||
if ac.ab.Format.Channels == 2 && wantChannels == 1 {
|
if ac.clip.Format.Channels == 2 && wantChannels == 1 {
|
||||||
if formatted, err = pcm.StereoToMono(ac.ab); err != nil {
|
if formatted, err = pcm.StereoToMono(ac.clip); err != nil {
|
||||||
log.Log(logger.Warning, "channel conversion failed, audio has remained stereo", "error", err.Error())
|
log.Log(logger.Warning, "channel conversion failed, audio has remained stereo", "error", err.Error())
|
||||||
} else {
|
} else {
|
||||||
formatted.Format.Channels = 1
|
formatted.Format.Channels = 1
|
||||||
|
@ -538,13 +552,13 @@ func (ac *audioClient) formatBuffer() alsa.Buffer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ac.ab.Format.Rate != wantRate {
|
if ac.clip.Format.Rate != wantRate {
|
||||||
|
|
||||||
// Convert rate.
|
// Convert rate.
|
||||||
if bufCopied {
|
if bufCopied {
|
||||||
formatted, err = pcm.Resample(formatted, wantRate)
|
formatted, err = pcm.Resample(formatted, wantRate)
|
||||||
} else {
|
} else {
|
||||||
formatted, err = pcm.Resample(ac.ab, wantRate)
|
formatted, err = pcm.Resample(ac.clip, wantRate)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Log(logger.Warning, "rate conversion failed, audio has remained original rate", "error", err.Error())
|
log.Log(logger.Warning, "rate conversion failed, audio has remained original rate", "error", err.Error())
|
||||||
|
|
240
codec/pcm/pcm.go
240
codec/pcm/pcm.go
|
@ -32,105 +32,151 @@ import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/yobert/alsa"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Resample takes alsa.Buffer b and resamples the pcm audio data to 'rate' Hz and returns an alsa.Buffer with the resampled data.
|
// SampleFormat is the format that a PCM Clip's samples can be in.
|
||||||
|
type SampleFormat int
|
||||||
|
|
||||||
|
// Used to represent an unknown format.
|
||||||
|
const (
|
||||||
|
Unknown SampleFormat = -1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Common sample formats that are used.
|
||||||
|
const (
|
||||||
|
S8 SampleFormat = iota
|
||||||
|
U8
|
||||||
|
S16_LE
|
||||||
|
S16_BE
|
||||||
|
U16_LE
|
||||||
|
U16_BE
|
||||||
|
S24_LE
|
||||||
|
S24_BE
|
||||||
|
U24_LE
|
||||||
|
U24_BE
|
||||||
|
S32_LE
|
||||||
|
S32_BE
|
||||||
|
U32_LE
|
||||||
|
U32_BE
|
||||||
|
FLOAT_LE
|
||||||
|
FLOAT_BE
|
||||||
|
FLOAT64_LE
|
||||||
|
FLOAT64_BE
|
||||||
|
// There are many more:
|
||||||
|
// https://linux.die.net/man/1/arecord
|
||||||
|
// https://trac.ffmpeg.org/wiki/audio%20types
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClipFormat contains the format for a PCM Clip.
|
||||||
|
type ClipFormat struct {
|
||||||
|
SFormat SampleFormat
|
||||||
|
Rate int
|
||||||
|
Channels int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clip contains a clip of PCM data and the format that it is in.
|
||||||
|
type Clip struct {
|
||||||
|
Format ClipFormat
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resample takes Clip c and resamples the pcm audio data to 'rate' Hz and returns a Clip with the resampled data.
|
||||||
// Notes:
|
// Notes:
|
||||||
// - Currently only downsampling is implemented and b's rate must be divisible by 'rate' or an error will occur.
|
// - Currently only downsampling is implemented and c's rate must be divisible by 'rate' or an error will occur.
|
||||||
// - If the number of bytes in b.Data is not divisible by the decimation factor (ratioFrom), the remaining bytes will
|
// - If the number of bytes in c.Data is not divisible by the decimation factor (ratioFrom), the remaining bytes will
|
||||||
// not be included in the result. Eg. input of length 480002 downsampling 6:1 will result in output length 80000.
|
// not be included in the result. Eg. input of length 480002 downsampling 6:1 will result in output length 80000.
|
||||||
func Resample(b alsa.Buffer, rate int) (alsa.Buffer, error) {
|
func Resample(c Clip, rate int) (Clip, error) {
|
||||||
if b.Format.Rate == rate {
|
if c.Format.Rate == rate {
|
||||||
return b, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
if b.Format.Rate < 0 {
|
if c.Format.Rate < 0 {
|
||||||
return alsa.Buffer{}, fmt.Errorf("Unable to convert from: %v Hz", b.Format.Rate)
|
return Clip{}, fmt.Errorf("Unable to convert from: %v Hz", c.Format.Rate)
|
||||||
}
|
}
|
||||||
if rate < 0 {
|
if rate < 0 {
|
||||||
return alsa.Buffer{}, fmt.Errorf("Unable to convert to: %v Hz", rate)
|
return Clip{}, fmt.Errorf("Unable to convert to: %v Hz", rate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The number of bytes in a sample.
|
// The number of bytes in a sample.
|
||||||
var sampleLen int
|
var sampleLen int
|
||||||
switch b.Format.SampleFormat {
|
switch c.Format.SFormat {
|
||||||
case alsa.S32_LE:
|
case S32_LE:
|
||||||
sampleLen = 4 * b.Format.Channels
|
sampleLen = 4 * c.Format.Channels
|
||||||
case alsa.S16_LE:
|
case S16_LE:
|
||||||
sampleLen = 2 * b.Format.Channels
|
sampleLen = 2 * c.Format.Channels
|
||||||
default:
|
default:
|
||||||
return alsa.Buffer{}, fmt.Errorf("Unhandled ALSA format: %v", b.Format.SampleFormat)
|
return Clip{}, fmt.Errorf("Unhandled ALSA format: %v", c.Format.SFormat)
|
||||||
}
|
}
|
||||||
inPcmLen := len(b.Data)
|
inPcmLen := len(c.Data)
|
||||||
|
|
||||||
// Calculate sample rate ratio ratioFrom:ratioTo.
|
// Calculate sample rate ratio ratioFrom:ratioTo.
|
||||||
rateGcd := gcd(rate, b.Format.Rate)
|
rateGcd := gcd(rate, c.Format.Rate)
|
||||||
ratioFrom := b.Format.Rate / rateGcd
|
ratioFrom := c.Format.Rate / rateGcd
|
||||||
ratioTo := rate / rateGcd
|
ratioTo := rate / rateGcd
|
||||||
|
|
||||||
// ratioTo = 1 is the only number that will result in an even sampling.
|
// ratioTo = 1 is the only number that will result in an even sampling.
|
||||||
if ratioTo != 1 {
|
if ratioTo != 1 {
|
||||||
return alsa.Buffer{}, fmt.Errorf("unhandled from:to rate ratio %v:%v: 'to' must be 1", ratioFrom, ratioTo)
|
return Clip{}, fmt.Errorf("unhandled from:to rate ratio %v:%v: 'to' must be 1", ratioFrom, ratioTo)
|
||||||
}
|
}
|
||||||
|
|
||||||
newLen := inPcmLen / ratioFrom
|
newLen := inPcmLen / ratioFrom
|
||||||
resampled := make([]byte, 0, newLen)
|
resampled := make([]byte, 0, newLen)
|
||||||
|
|
||||||
// For each new sample to be generated, loop through the respective 'ratioFrom' samples in 'b.Data' to add them
|
// For each new sample to be generated, loop through the respective 'ratioFrom' samples in 'c.Data' to add them
|
||||||
// up and average them. The result is the new sample.
|
// up and average them. The result is the new sample.
|
||||||
bAvg := make([]byte, sampleLen)
|
bAvg := make([]byte, sampleLen)
|
||||||
for i := 0; i < newLen/sampleLen; i++ {
|
for i := 0; i < newLen/sampleLen; i++ {
|
||||||
var sum int
|
var sum int
|
||||||
for j := 0; j < ratioFrom; j++ {
|
for j := 0; j < ratioFrom; j++ {
|
||||||
switch b.Format.SampleFormat {
|
switch c.Format.SFormat {
|
||||||
case alsa.S32_LE:
|
case S32_LE:
|
||||||
sum += int(int32(binary.LittleEndian.Uint32(b.Data[(i*ratioFrom*sampleLen)+(j*sampleLen) : (i*ratioFrom*sampleLen)+((j+1)*sampleLen)])))
|
sum += int(int32(binary.LittleEndian.Uint32(c.Data[(i*ratioFrom*sampleLen)+(j*sampleLen) : (i*ratioFrom*sampleLen)+((j+1)*sampleLen)])))
|
||||||
case alsa.S16_LE:
|
case S16_LE:
|
||||||
sum += int(int16(binary.LittleEndian.Uint16(b.Data[(i*ratioFrom*sampleLen)+(j*sampleLen) : (i*ratioFrom*sampleLen)+((j+1)*sampleLen)])))
|
sum += int(int16(binary.LittleEndian.Uint16(c.Data[(i*ratioFrom*sampleLen)+(j*sampleLen) : (i*ratioFrom*sampleLen)+((j+1)*sampleLen)])))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
avg := sum / ratioFrom
|
avg := sum / ratioFrom
|
||||||
switch b.Format.SampleFormat {
|
switch c.Format.SFormat {
|
||||||
case alsa.S32_LE:
|
case S32_LE:
|
||||||
binary.LittleEndian.PutUint32(bAvg, uint32(avg))
|
binary.LittleEndian.PutUint32(bAvg, uint32(avg))
|
||||||
case alsa.S16_LE:
|
case S16_LE:
|
||||||
binary.LittleEndian.PutUint16(bAvg, uint16(avg))
|
binary.LittleEndian.PutUint16(bAvg, uint16(avg))
|
||||||
}
|
}
|
||||||
resampled = append(resampled, bAvg...)
|
resampled = append(resampled, bAvg...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a new alsa.Buffer with resampled data.
|
// Return a new Clip with resampled data.
|
||||||
return alsa.Buffer{
|
return Clip{
|
||||||
Format: alsa.BufferFormat{
|
Format: ClipFormat{
|
||||||
Channels: b.Format.Channels,
|
Channels: c.Format.Channels,
|
||||||
SampleFormat: b.Format.SampleFormat,
|
SFormat: c.Format.SFormat,
|
||||||
Rate: rate,
|
Rate: rate,
|
||||||
},
|
},
|
||||||
Data: resampled,
|
Data: resampled,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StereoToMono returns raw mono audio data generated from only the left channel from
|
// StereoToMono returns raw mono audio data generated from only the left channel from
|
||||||
// the given stereo recording (ALSA buffer)
|
// the given stereo Clip
|
||||||
func StereoToMono(b alsa.Buffer) (alsa.Buffer, error) {
|
func StereoToMono(c Clip) (Clip, error) {
|
||||||
if b.Format.Channels == 1 {
|
if c.Format.Channels == 1 {
|
||||||
return b, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
if b.Format.Channels != 2 {
|
if c.Format.Channels != 2 {
|
||||||
return alsa.Buffer{}, fmt.Errorf("Audio is not stereo or mono, it has %v channels", b.Format.Channels)
|
return Clip{}, fmt.Errorf("Audio is not stereo or mono, it has %v channels", c.Format.Channels)
|
||||||
}
|
}
|
||||||
|
|
||||||
var stereoSampleBytes int
|
var stereoSampleBytes int
|
||||||
switch b.Format.SampleFormat {
|
switch c.Format.SFormat {
|
||||||
case alsa.S32_LE:
|
case S32_LE:
|
||||||
stereoSampleBytes = 8
|
stereoSampleBytes = 8
|
||||||
case alsa.S16_LE:
|
case S16_LE:
|
||||||
stereoSampleBytes = 4
|
stereoSampleBytes = 4
|
||||||
default:
|
default:
|
||||||
return alsa.Buffer{}, fmt.Errorf("Unhandled ALSA format %v", b.Format.SampleFormat)
|
return Clip{}, fmt.Errorf("Unhandled sample format %v", c.Format.SFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
recLength := len(b.Data)
|
recLength := len(c.Data)
|
||||||
mono := make([]byte, recLength/2)
|
mono := make([]byte, recLength/2)
|
||||||
|
|
||||||
// Convert to mono: for each byte in the stereo recording, if it's in the first half of a stereo sample
|
// Convert to mono: for each byte in the stereo recording, if it's in the first half of a stereo sample
|
||||||
|
@ -138,17 +184,17 @@ func StereoToMono(b alsa.Buffer) (alsa.Buffer, error) {
|
||||||
var inc int
|
var inc int
|
||||||
for i := 0; i < recLength; i++ {
|
for i := 0; i < recLength; i++ {
|
||||||
if i%stereoSampleBytes < stereoSampleBytes/2 {
|
if i%stereoSampleBytes < stereoSampleBytes/2 {
|
||||||
mono[inc] = b.Data[i]
|
mono[inc] = c.Data[i]
|
||||||
inc++
|
inc++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a new alsa.Buffer with resampled data.
|
// Return a new Clip with resampled data.
|
||||||
return alsa.Buffer{
|
return Clip{
|
||||||
Format: alsa.BufferFormat{
|
Format: ClipFormat{
|
||||||
Channels: 1,
|
Channels: 1,
|
||||||
SampleFormat: b.Format.SampleFormat,
|
SFormat: c.Format.SFormat,
|
||||||
Rate: b.Format.Rate,
|
Rate: c.Format.Rate,
|
||||||
},
|
},
|
||||||
Data: mono,
|
Data: mono,
|
||||||
}, nil
|
}, nil
|
||||||
|
@ -162,3 +208,91 @@ func gcd(a, b int) int {
|
||||||
}
|
}
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String returns the string representation of a SampleFormat.
|
||||||
|
func (f SampleFormat) String() string {
|
||||||
|
switch f {
|
||||||
|
case S8:
|
||||||
|
return "S8"
|
||||||
|
case U8:
|
||||||
|
return "U8"
|
||||||
|
case S16_LE:
|
||||||
|
return "S16_LE"
|
||||||
|
case S16_BE:
|
||||||
|
return "S16_BE"
|
||||||
|
case U16_LE:
|
||||||
|
return "U16_LE"
|
||||||
|
case U16_BE:
|
||||||
|
return "U16_BE"
|
||||||
|
case S24_LE:
|
||||||
|
return "S24_LE"
|
||||||
|
case S24_BE:
|
||||||
|
return "S24_BE"
|
||||||
|
case U24_LE:
|
||||||
|
return "U24_LE"
|
||||||
|
case U24_BE:
|
||||||
|
return "U24_BE"
|
||||||
|
case S32_LE:
|
||||||
|
return "S32_LE"
|
||||||
|
case S32_BE:
|
||||||
|
return "S32_BE"
|
||||||
|
case U32_LE:
|
||||||
|
return "U32_LE"
|
||||||
|
case U32_BE:
|
||||||
|
return "U32_BE"
|
||||||
|
case FLOAT_LE:
|
||||||
|
return "FLOAT_LE"
|
||||||
|
case FLOAT_BE:
|
||||||
|
return "FLOAT_BE"
|
||||||
|
case FLOAT64_LE:
|
||||||
|
return "FLOAT64_LE"
|
||||||
|
case FLOAT64_BE:
|
||||||
|
return "FLOAT64_BE"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("Invalid FormatType (%d)", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SFFromString takes a string representing a sample format and returns the corresponding SampleFormat.
|
||||||
|
func SFFromString(s string) (SampleFormat, error) {
|
||||||
|
switch s {
|
||||||
|
case "S8":
|
||||||
|
return S8, nil
|
||||||
|
case "U8":
|
||||||
|
return U8, nil
|
||||||
|
case "S16_LE":
|
||||||
|
return S16_LE, nil
|
||||||
|
case "S16_BE":
|
||||||
|
return S16_BE, nil
|
||||||
|
case "U16_LE":
|
||||||
|
return U16_LE, nil
|
||||||
|
case "U16_BE":
|
||||||
|
return U16_BE, nil
|
||||||
|
case "S24_LE":
|
||||||
|
return S24_LE, nil
|
||||||
|
case "S24_BE":
|
||||||
|
return S24_BE, nil
|
||||||
|
case "U24_LE":
|
||||||
|
return U24_LE, nil
|
||||||
|
case "U24_BE":
|
||||||
|
return U24_BE, nil
|
||||||
|
case "S32_LE":
|
||||||
|
return S32_LE, nil
|
||||||
|
case "S32_BE":
|
||||||
|
return S32_BE, nil
|
||||||
|
case "U32_LE":
|
||||||
|
return U32_LE, nil
|
||||||
|
case "U32_BE":
|
||||||
|
return U32_BE, nil
|
||||||
|
case "FLOAT_LE":
|
||||||
|
return FLOAT_LE, nil
|
||||||
|
case "FLOAT_BE":
|
||||||
|
return FLOAT_BE, nil
|
||||||
|
case "FLOAT64_LE":
|
||||||
|
return FLOAT64_LE, nil
|
||||||
|
case "FLOAT64_BE":
|
||||||
|
return FLOAT64_BE, nil
|
||||||
|
default:
|
||||||
|
return Unknown, errors.Errorf("Unknown FormatType (%d)", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -31,8 +31,6 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/yobert/alsa"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestResample tests the Resample function using a pcm file that contains audio of a freq. sweep.
|
// TestResample tests the Resample function using a pcm file that contains audio of a freq. sweep.
|
||||||
|
@ -47,19 +45,19 @@ func TestResample(t *testing.T) {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
format := alsa.BufferFormat{
|
format := ClipFormat{
|
||||||
Channels: 1,
|
Channels: 1,
|
||||||
Rate: 48000,
|
Rate: 48000,
|
||||||
SampleFormat: alsa.S16_LE,
|
SFormat: S16_LE,
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := alsa.Buffer{
|
clip := Clip{
|
||||||
Format: format,
|
Format: format,
|
||||||
Data: inPcm,
|
Data: inPcm,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resample pcm.
|
// Resample pcm.
|
||||||
resampled, err := Resample(buf, 8000)
|
resampled, err := Resample(clip, 8000)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -88,19 +86,19 @@ func TestStereoToMono(t *testing.T) {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
format := alsa.BufferFormat{
|
format := ClipFormat{
|
||||||
Channels: 2,
|
Channels: 2,
|
||||||
Rate: 44100,
|
Rate: 44100,
|
||||||
SampleFormat: alsa.S16_LE,
|
SFormat: S16_LE,
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := alsa.Buffer{
|
clip := Clip{
|
||||||
Format: format,
|
Format: format,
|
||||||
Data: inPcm,
|
Data: inPcm,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert audio.
|
// Convert audio.
|
||||||
mono, err := StereoToMono(buf)
|
mono, err := StereoToMono(clip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,7 @@ type ALSA struct {
|
||||||
mu sync.Mutex // Provides synchronisation when changing modes concurrently.
|
mu sync.Mutex // Provides synchronisation when changing modes concurrently.
|
||||||
title string // Name of audio title, or empty for the default title.
|
title string // Name of audio title, or empty for the default title.
|
||||||
dev *yalsa.Device // ALSA device's Audio input device.
|
dev *yalsa.Device // ALSA device's Audio input device.
|
||||||
ab yalsa.Buffer // ALSA device's buffer.
|
clip pcm.Clip // Clip to contain the recording.
|
||||||
rb *ring.Buffer // Our buffer.
|
rb *ring.Buffer // Our buffer.
|
||||||
chunkSize int // This is the number of bytes that will be stored in rb at a time.
|
chunkSize int // This is the number of bytes that will be stored in rb at a time.
|
||||||
Config // Configuration parameters for this device.
|
Config // Configuration parameters for this device.
|
||||||
|
@ -133,10 +133,24 @@ func (d *ALSA) Set(c config.Config) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup the device to record with desired period.
|
// Setup the device to record with desired period.
|
||||||
d.ab = d.dev.NewBufferDuration(time.Duration(d.RecPeriod * float64(time.Second)))
|
ab := d.dev.NewBufferDuration(time.Duration(d.RecPeriod * float64(time.Second)))
|
||||||
|
sf, err := pcm.SFFromString(ab.Format.SampleFormat.String())
|
||||||
|
if err != nil {
|
||||||
|
d.l.Log(logger.Error, pkg+err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cf := pcm.ClipFormat{
|
||||||
|
SFormat: sf,
|
||||||
|
Channels: ab.Format.Channels,
|
||||||
|
Rate: ab.Format.Rate,
|
||||||
|
}
|
||||||
|
d.clip = pcm.Clip{
|
||||||
|
Format: cf,
|
||||||
|
Data: ab.Data,
|
||||||
|
}
|
||||||
|
|
||||||
// Account for channel conversion.
|
// Account for channel conversion.
|
||||||
chunkSize := float64(len(d.ab.Data) / d.dev.BufferFormat().Channels * d.Channels)
|
chunkSize := float64(len(d.clip.Data) / d.dev.BufferFormat().Channels * d.Channels)
|
||||||
|
|
||||||
// Account for resampling.
|
// Account for resampling.
|
||||||
chunkSize = (chunkSize / float64(d.dev.BufferFormat().Rate)) * float64(d.SampleRate)
|
chunkSize = (chunkSize / float64(d.dev.BufferFormat().Rate)) * float64(d.SampleRate)
|
||||||
|
@ -372,7 +386,7 @@ func (d *ALSA) input() {
|
||||||
|
|
||||||
// Read from audio device.
|
// Read from audio device.
|
||||||
d.l.Log(logger.Debug, pkg+"recording audio for period", "seconds", d.RecPeriod)
|
d.l.Log(logger.Debug, pkg+"recording audio for period", "seconds", d.RecPeriod)
|
||||||
err := d.dev.Read(d.ab.Data)
|
err := d.dev.Read(d.clip.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.l.Log(logger.Debug, pkg+"read failed", "error", err.Error())
|
d.l.Log(logger.Debug, pkg+"read failed", "error", err.Error())
|
||||||
err = d.open() // re-open
|
err = d.open() // re-open
|
||||||
|
@ -414,26 +428,26 @@ func (d *ALSA) Read(p []byte) (int, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatBuffer returns audio that has been converted to the desired format.
|
// formatBuffer returns audio that has been converted to the desired format.
|
||||||
func (d *ALSA) formatBuffer() yalsa.Buffer {
|
func (d *ALSA) formatBuffer() pcm.Clip {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// If nothing needs to be changed, return the original.
|
// If nothing needs to be changed, return the original.
|
||||||
if d.ab.Format.Channels == d.Channels && d.ab.Format.Rate == d.SampleRate {
|
if d.clip.Format.Channels == d.Channels && d.clip.Format.Rate == d.SampleRate {
|
||||||
return d.ab
|
return d.clip
|
||||||
}
|
}
|
||||||
var formatted yalsa.Buffer
|
var formatted pcm.Clip
|
||||||
if d.ab.Format.Channels != d.Channels {
|
if d.clip.Format.Channels != d.Channels {
|
||||||
// Convert channels.
|
// Convert channels.
|
||||||
// TODO(Trek): Make this work for conversions other than stereo to mono.
|
// TODO(Trek): Make this work for conversions other than stereo to mono.
|
||||||
if d.ab.Format.Channels == 2 && d.Channels == 1 {
|
if d.clip.Format.Channels == 2 && d.Channels == 1 {
|
||||||
formatted, err = pcm.StereoToMono(d.ab)
|
formatted, err = pcm.StereoToMono(d.clip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.l.Log(logger.Fatal, pkg+"channel conversion failed", "error", err.Error())
|
d.l.Log(logger.Fatal, pkg+"channel conversion failed", "error", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.ab.Format.Rate != d.SampleRate {
|
if d.clip.Format.Rate != d.SampleRate {
|
||||||
// Convert rate.
|
// Convert rate.
|
||||||
formatted, err = pcm.Resample(formatted, d.SampleRate)
|
formatted, err = pcm.Resample(formatted, d.SampleRate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -32,7 +32,6 @@ import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"bitbucket.org/ausocean/av/codec/pcm"
|
"bitbucket.org/ausocean/av/codec/pcm"
|
||||||
"github.com/yobert/alsa"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// This program accepts an input pcm file and outputs a resampled pcm file.
|
// This program accepts an input pcm file and outputs a resampled pcm file.
|
||||||
|
@ -43,7 +42,7 @@ func main() {
|
||||||
var from = *flag.Int("from", 48000, "sample rate of input file")
|
var from = *flag.Int("from", 48000, "sample rate of input file")
|
||||||
var to = *flag.Int("to", 8000, "sample rate of output file")
|
var to = *flag.Int("to", 8000, "sample rate of output file")
|
||||||
var channels = *flag.Int("ch", 1, "number of channels in input file")
|
var channels = *flag.Int("ch", 1, "number of channels in input file")
|
||||||
var sf = *flag.String("sf", "S16_LE", "sample format of input audio, eg. S16_LE")
|
var SFString = *flag.String("sf", "S16_LE", "sample format of input audio, eg. S16_LE")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// Read pcm.
|
// Read pcm.
|
||||||
|
@ -53,29 +52,29 @@ func main() {
|
||||||
}
|
}
|
||||||
fmt.Println("Read", len(inPcm), "bytes from file", inPath)
|
fmt.Println("Read", len(inPcm), "bytes from file", inPath)
|
||||||
|
|
||||||
var sampleFormat alsa.FormatType
|
var sf pcm.SampleFormat
|
||||||
switch sf {
|
switch SFString {
|
||||||
case "S32_LE":
|
case "S32_LE":
|
||||||
sampleFormat = alsa.S32_LE
|
sf = pcm.S32_LE
|
||||||
case "S16_LE":
|
case "S16_LE":
|
||||||
sampleFormat = alsa.S16_LE
|
sf = pcm.S16_LE
|
||||||
default:
|
default:
|
||||||
log.Fatalf("Unhandled ALSA format: %v", sf)
|
log.Fatalf("Unhandled ALSA format: %v", SFString)
|
||||||
}
|
}
|
||||||
|
|
||||||
format := alsa.BufferFormat{
|
format := pcm.ClipFormat{
|
||||||
Channels: channels,
|
Channels: channels,
|
||||||
Rate: from,
|
Rate: from,
|
||||||
SampleFormat: sampleFormat,
|
SFormat: sf,
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := alsa.Buffer{
|
clip := pcm.Clip{
|
||||||
Format: format,
|
Format: format,
|
||||||
Data: inPcm,
|
Data: inPcm,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resample audio.
|
// Resample audio.
|
||||||
resampled, err := pcm.Resample(buf, to)
|
resampled, err := pcm.Resample(clip, to)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,6 @@ import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"bitbucket.org/ausocean/av/codec/pcm"
|
"bitbucket.org/ausocean/av/codec/pcm"
|
||||||
"github.com/yobert/alsa"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// This program accepts an input pcm file and outputs a resampled pcm file.
|
// This program accepts an input pcm file and outputs a resampled pcm file.
|
||||||
|
@ -40,7 +39,7 @@ import (
|
||||||
func main() {
|
func main() {
|
||||||
var inPath = *flag.String("in", "data.pcm", "file path of input data")
|
var inPath = *flag.String("in", "data.pcm", "file path of input data")
|
||||||
var outPath = *flag.String("out", "mono.pcm", "file path of output")
|
var outPath = *flag.String("out", "mono.pcm", "file path of output")
|
||||||
var sf = *flag.String("sf", "S16_LE", "sample format of input audio, eg. S16_LE")
|
var SFString = *flag.String("sf", "S16_LE", "sample format of input audio, eg. S16_LE")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// Read pcm.
|
// Read pcm.
|
||||||
|
@ -50,28 +49,28 @@ func main() {
|
||||||
}
|
}
|
||||||
fmt.Println("Read", len(inPcm), "bytes from file", inPath)
|
fmt.Println("Read", len(inPcm), "bytes from file", inPath)
|
||||||
|
|
||||||
var sampleFormat alsa.FormatType
|
var sf pcm.SampleFormat
|
||||||
switch sf {
|
switch SFString {
|
||||||
case "S32_LE":
|
case "S32_LE":
|
||||||
sampleFormat = alsa.S32_LE
|
sf = pcm.S32_LE
|
||||||
case "S16_LE":
|
case "S16_LE":
|
||||||
sampleFormat = alsa.S16_LE
|
sf = pcm.S16_LE
|
||||||
default:
|
default:
|
||||||
log.Fatalf("Unhandled ALSA format: %v", sf)
|
log.Fatalf("Unhandled sample format: %v", SFString)
|
||||||
}
|
}
|
||||||
|
|
||||||
format := alsa.BufferFormat{
|
format := pcm.ClipFormat{
|
||||||
Channels: 2,
|
Channels: 2,
|
||||||
SampleFormat: sampleFormat,
|
SFormat: sf,
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := alsa.Buffer{
|
clip := pcm.Clip{
|
||||||
Format: format,
|
Format: format,
|
||||||
Data: inPcm,
|
Data: inPcm,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert audio.
|
// Convert audio.
|
||||||
mono, err := pcm.StereoToMono(buf)
|
mono, err := pcm.StereoToMono(clip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue