This commit is contained in:
David Sutton 2024-03-28 07:16:42 +10:30
parent 94d1f1a156
commit d66172915e
10 changed files with 156 additions and 322 deletions

View File

@ -282,7 +282,7 @@ func (ac *audioClient) open() error {
// to fix this 8000 and 16000 must be removed from this slice.
rates := [8]int{8000, 16000, 32000, 44100, 48000, 88200, 96000, 192000}
foundRate := false
for r := range rates {
for _, r := range rates {
if r < ac.rate {
continue
}

View File

@ -28,6 +28,7 @@ package alsa
import (
"bytes"
"context"
"errors"
"fmt"
"io"
@ -97,6 +98,7 @@ type Config struct {
BitDepth uint
RecPeriod float64
Codec string
Title string
}
// New initializes and returns an ALSA device which has its logger set as the given logger.
@ -133,12 +135,14 @@ func (d *ALSA) Setup(c config.Config) error {
errs = append(errs, errInvalidCodec)
c.InputCodec = defaultCodec
}
d.Config = Config{
SampleRate: c.SampleRate,
Channels: c.Channels,
BitDepth: c.BitDepth,
RecPeriod: c.RecPeriod,
Codec: c.InputCodec,
Title: c.AlsaTitle,
}
// Open the requested audio device.
@ -224,6 +228,8 @@ func (d *ALSA) open() error {
d.l.Debug("closing device", "title", d.title)
d.dev.Close()
d.dev = nil
} else {
d.title = d.Config.Title
}
// Open sound card and open recording device.
@ -281,7 +287,7 @@ func (d *ALSA) open() error {
var rate int
foundRate := false
for r := range rates {
for _, r := range rates {
if r < int(d.SampleRate) {
continue
}
@ -336,9 +342,13 @@ func (d *ALSA) open() error {
bytesPerSecond := rate * channels * (bitdepth / 8)
wantPeriodSize := int(float64(bytesPerSecond) * wantPeriod)
nearWantPeriodSize := nearestPowerOfTwo(wantPeriodSize)
periodSize, err := d.dev.NegotiatePeriodSize(nearWantPeriodSize)
if err != nil {
return err
}
// At least two period sizes should fit within the buffer.
bufSize, err := d.dev.NegotiateBufferSize(nearWantPeriodSize * 2)
bufSize, err := d.dev.NegotiateBufferSize(periodSize * 2)
if err != nil {
return err
}
@ -355,56 +365,15 @@ func (d *ALSA) open() error {
// input continously records audio and writes it to the ringbuffer.
// Re-opens the device and tries again if the ASLA device returns an error.
func (d *ALSA) input() {
for {
// Check mode.
d.mu.Lock()
mode := d.mode
d.mu.Unlock()
switch mode {
case paused:
time.Sleep(time.Duration(d.RecPeriod) * time.Second)
continue
case stopped:
if d.dev != nil {
d.l.Debug("closing ALSA device", "title", d.title)
d.dev.Close()
d.dev = nil
}
err := d.buf.Close()
if err != nil {
d.l.Error("unable to close pool buffer", "error", err)
}
return
}
// Get context and assign cancel function.
ctx, cancel := context.WithCancel(context.Background())
// Read from audio device.
d.l.Debug("recording audio for period", "seconds", d.RecPeriod)
err := d.dev.Read(d.pb.Data)
if err != nil {
d.l.Debug("read failed", "error", err.Error())
err = d.open() // re-open
if err != nil {
d.l.Fatal("reopening device failed", "error", err.Error())
return
}
continue
}
// Check device mode.
go checkMode(d, cancel)
// Process audio.
d.l.Debug("processing audio")
toWrite := d.formatBuffer()
// Read from audio device.
go tickerRead(d, ctx)
// Write audio to ringbuffer.
n, err := d.buf.Write(toWrite.Data)
switch err {
case nil:
d.l.Debug("wrote audio to ringbuffer", "length", n)
case pool.ErrDropped:
d.l.Warning("old audio data overwritten")
default:
d.l.Error("unexpected ringbuffer error", "error", err.Error())
}
}
}
// Read reads from the ringbuffer, returning the number of bytes read upon success.
@ -443,6 +412,77 @@ func (d *ALSA) Read(p []byte) (int, error) {
return n, nil
}
// checkMode is intended to run as a goroutine to call a cancel function on the context
// once the mode has changed from normal to stopped.
func checkMode(d *ALSA, cancel func()) {
d.l.Debug("starting checkMode loop")
ticker := time.NewTicker(time.Duration(d.RecPeriod) * time.Second)
for range ticker.C {
// Check mode.
d.mu.Lock()
mode := d.mode
d.mu.Unlock()
switch mode {
case paused:
time.Sleep(time.Duration(d.RecPeriod) * time.Second)
continue
case stopped:
cancel()
if d.dev != nil {
d.l.Debug("closing ALSA device", "title", d.title)
d.dev.Close()
d.dev = nil
}
err := d.buf.Close()
if err != nil {
d.l.Error("unable to close pool buffer", "error", err)
}
return
}
}
}
// tickerRead uses a timer.ticker to call Read on the device at precise intervals to ensure that reads are
// happening at the right time.
func tickerRead(d *ALSA, ctx context.Context) {
d.l.Debug("starting ticker read loop")
ticker := time.NewTicker(time.Duration(d.RecPeriod)*time.Second - time.Duration(30*time.Millisecond))
for {
select {
case <-ctx.Done():
d.l.Debug("context cancelled")
return
case <-ticker.C:
d.l.Debug("recording audio for period", "seconds", d.RecPeriod)
err := d.dev.Read(d.pb.Data)
if err != nil {
d.l.Debug("read failed", "error", err.Error())
err = d.open() // re-open
if err != nil {
d.l.Fatal("reopening device failed", "error", err.Error())
return
}
continue
}
// Process audio.
d.l.Debug("processing audio")
toWrite := d.formatBuffer()
// Write audio to ringbuffer.
n, err := d.buf.Write(toWrite.Data)
switch err {
case nil:
d.l.Debug("wrote audio to ringbuffer", "length", n)
case pool.ErrDropped:
d.l.Warning("old audio data overwritten")
default:
d.l.Error("unexpected ringbuffer error", "error", err.Error())
}
}
}
}
// formatBuffer returns audio that has been converted to the desired format.
func (d *ALSA) formatBuffer() pcm.Buffer {
var err error

Binary file not shown.

View File

@ -1,33 +1,8 @@
2024/03/04 16:27:36 recording device is: ALC892 Analog
2024/03/04 16:27:36 Number of channels is: 2
2024/03/04 16:27:36 rate is: 44100
2024/03/04 16:27:36 buffer size is: 1024
2024/03/04 16:27:36 bytes per frame: 8
2024/03/04 16:27:36 device state: PREPARED
2024/03/04 16:27:36 2 channels, 44100 hz, S16_LE
2024/03/04 16:27:36 prepared and ready to record
2024/03/04 16:27:44 read error: ioctl read (24 bytes) 0x4151 failed: broken pipe
2024/03/04 16:27:44 trying to restart device
2024/03/04 16:27:44 device state: XRUN
2024/03/04 16:27:44 device state: PREPARED
2024/03/04 16:27:44 restart successful
2024/03/04 16:27:47 read error: ioctl read (24 bytes) 0x4151 failed: broken pipe
2024/03/04 16:27:47 trying to restart device
2024/03/04 16:27:47 device state: XRUN
2024/03/04 16:27:47 device state: PREPARED
2024/03/04 16:27:47 restart successful
2024/03/04 16:27:49 read error: ioctl read (24 bytes) 0x4151 failed: broken pipe
2024/03/04 16:27:49 trying to restart device
2024/03/04 16:27:49 device state: XRUN
2024/03/04 16:27:49 device state: PREPARED
2024/03/04 16:27:49 restart successful
2024/03/04 16:27:52 read error: ioctl read (24 bytes) 0x4151 failed: broken pipe
2024/03/04 16:27:52 trying to restart device
2024/03/04 16:27:52 device state: XRUN
2024/03/04 16:27:52 device state: PREPARED
2024/03/04 16:27:52 restart successful
2024/03/04 16:27:55 read error: ioctl read (24 bytes) 0x4151 failed: broken pipe
2024/03/04 16:27:55 trying to restart device
2024/03/04 16:27:55 device state: XRUN
2024/03/04 16:27:55 device state: PREPARED
2024/03/04 16:27:55 restart successful
{"level":"debug","time":"2024-03-25T16:18:23.128+1030","caller":"i2s/main.go:41","message":"setting up new device"}
{"level":"debug","time":"2024-03-25T16:18:23.128+1030","caller":"alsa/alsa.go:235","message":"opening sound card"}
{"level":"debug","time":"2024-03-25T16:18:23.128+1030","caller":"alsa/alsa.go:242","message":"finding audio device"}
{"level":"debug","time":"2024-03-25T16:18:23.128+1030","caller":"alsa/alsa.go:262","message":"opening ALSA device","title":"USB Audio"}
{"level":"debug","time":"2024-03-25T16:18:23.128+1030","caller":"i2s/main.go:44","message":"failed to setup device","err":"failed to open device: device is unable to record with requested number of channels: Channel count negotiation failure: Requested value 2 is not supported by hardware: Must be 1"}
{"level":"debug","time":"2024-03-25T16:18:23.128+1030","caller":"i2s/main.go:55","message":"starting device"}
{"level":"debug","time":"2024-03-25T16:18:23.128+1030","caller":"i2s/main.go:58","message":"could not start device","err":"invalid mode: 0"}
{"level":"debug","time":"2024-03-25T16:18:23.128+1030","caller":"alsa/alsa.go:381","message":"alsa: getting next chunk ready"}

View File

@ -1,268 +1,79 @@
package main
import (
"bytes"
"fmt"
"log"
"os"
"strings"
"sync"
"time"
"bitbucket.org/ausocean/utils/ring"
yalsa "github.com/yobert/alsa"
"bitbucket.org/ausocean/av/codec/codecutil"
"bitbucket.org/ausocean/av/device/alsa"
"bitbucket.org/ausocean/av/revid/config"
"bitbucket.org/ausocean/utils/logging"
)
const i2sDevName = "simple-card_codec_link snd-soc-dummy-dai-0"
// type CircBuffer struct {
// Buf []byte
// Head, Tail int
// mu sync.Mutex
// }
// func (c *CircBuffer) nextWrite() {
// c.Head++
// if c.Head == len(c.Buf) {
// c.Head = 0
// }
// }
// func (c *CircBuffer) nextRead() {
// c.Tail++
// if c.Tail == len(c.Buf)-1 {
// c.Tail = 0
// }
// }
// func (c *CircBuffer) Write(p []byte) {
// for i := 0; i < len(p); i++ {
// c.mu.Lock()
// c.Buf[c.Head] = p[i]
// c.nextWrite()
// c.mu.Unlock()
// }
// }
// func (c *CircBuffer) Read(p []byte) (int, error) {
// var diff int
// if c.Head > c.Tail {
// diff = c.Head - c.Tail
// } else {
// diff = c.Head - c.Tail + len(c.Buf)
// }
// if diff > len(p) {
// diff = len(p)
// }
// for i := 0; i < diff; i++ {
// c.mu.Lock()
// p[i] = c.Buf[c.Tail]
// c.nextRead()
// c.mu.Unlock()
// }
// return diff + 1, nil
// }
func main() {
logger := log.Default()
// Setup logging
logFile, err := os.Create("i2s.log")
if err != nil {
log.Println("failed to create log file:", err)
log.Println("couldnt open log file:", err)
}
l := logging.New(logging.Debug, logFile, false)
// Set minimum fields required in config.
c := config.Config{
BitDepth: 16,
Channels: 2,
SampleRate: 48000,
RecPeriod: 1,
InputCodec: codecutil.PCM,
AlsaTitle: "USB Audio",
}
// Set file to write audio to.
output, err := os.Create("output.pcm")
if err != nil {
l.Error("could not create audio file", "error", err)
return
}
logger.SetOutput(logFile)
// Set capture type.
var i2s bool = false
// Find devices.
cards, err := yalsa.OpenCards()
if err != nil {
log.Println("failed to open cards:", err)
}
var allDevices []*yalsa.Device
for _, card := range cards {
devices, err := card.Devices()
if err != nil {
log.Println("could not get Devices:", err)
}
allDevices = append(allDevices, devices...)
}
var dev *yalsa.Device
if i2s {
// For I2S we know the name of the device, so select that card.
for _, device := range allDevices {
if device.Title == i2sDevName {
dev = device
}
}
} else {
// Otherwise, use the first recording device we find.
for _, card := range cards {
devices, err := card.Devices()
if err != nil {
log.Println(err)
return
}
for _, device := range devices {
if device.Type != yalsa.PCM {
continue
}
if device.Record && dev == nil {
dev = device
}
}
}
if dev == nil {
log.Println("No recording device found")
return
}
}
log.Println("recording device is:", dev.Title)
// Setup device.
err = dev.Open()
d := alsa.New(l)
l.Debug("setting up new device")
err = d.Setup(c)
if err != nil {
log.Println("open failed:", err)
return
l.Debug("failed to setup device", "err", err)
}
defer dev.Close()
channels, err := dev.NegotiateChannels(2)
// Create a new lexer to read from the device buffer.
// lexer, err := codecutil.NewByteLexer(d.DataSize())
// if err != nil {
// l.Error("could not make new lexer", "error", err)
// return
// }
// Start recording from device.
l.Debug("starting device")
err = d.Start()
if err != nil {
log.Println("failed to negotiate channels:", err)
return
}
log.Println("Number of channels is:", channels)
rate, err := dev.NegotiateRate(44100)
if err != nil {
log.Println("rate negotiation failed:", rate)
return
}
log.Println("rate is:", rate)
bufferSize, err := dev.NegotiateBufferSize(1024)
if err != nil {
log.Println("buffer size negotiation failed", err)
return
}
log.Println("buffer size is:", bufferSize)
log.Println("bytes per frame:", dev.BytesPerFrame())
// circ := &CircBuffer{Buf: make([]byte, bufferSize*dev.BytesPerFrame()*4), Head: 0, Tail: 0}
rBuf := ring.NewBuffer(8, 16*bufferSize*dev.BytesPerFrame(), 2*time.Second)
err = prepare(dev, false)
if err != nil {
log.Println("Prepare failed:", err)
return
l.Debug("could not start device", "err", err)
}
log.Println(dev.BufferFormat())
log.Println("prepared and ready to record")
// Record audio.
wg := new(sync.WaitGroup)
wg.Add(1)
go recordAudio(wg, rBuf, bufferSize, dev)
// Write audio to file.
file, err := os.Create("test.pcm")
if err != nil {
log.Println("couldn't create file:", err)
return
}
go writeAudio(wg, file, rBuf, bufferSize)
wg.Wait()
}
func prepare(dev *yalsa.Device, restart bool) error {
if restart {
getDeviceStatus(dev)
dev.Close()
err := dev.Open()
if err != nil {
return err
}
}
err := dev.Prepare()
if err != nil {
return err
}
getDeviceStatus(dev)
return nil
}
func recordAudio(wg *sync.WaitGroup, buf *ring.Buffer, size int, dev *yalsa.Device) {
// Create a buffer for single read.
p := make([]byte, size*dev.BytesPerFrame())
var err error
// Start lexing to output file.
// lexer.Lex(output, d, 1)
buf := make([]byte, 4*48000)
for {
err = dev.Read(p)
n, err := d.Read(buf)
if err != nil {
log.Println("read error:", err)
log.Println("trying to restart device")
err = prepare(dev, true)
if err != nil {
log.Println("failed to restart device:", err)
wg.Done()
return
}
log.Println("restart successful")
l.Debug("unable to read from device buffer", "error", err)
continue
}
// Write into CircBuffer.
buf.Write(p)
}
}
l.Debug("read bytes from device buffer", "bytes read", n)
func writeAudio(wg *sync.WaitGroup, file *os.File, buf *ring.Buffer, size int) {
// Create a locally scoped bytes array.
p := make([]byte, size*8)
var err error
var n, m int
for {
n, err = buf.Read(p)
// log.Println(n)
if err.Error() == "EOF" {
continue
} else if err != nil {
log.Println("failed to read from ring buffer:", err)
continue
}
if n == 0 {
log.Println("read nothing")
continue
}
m, err = file.Write(p)
n, err = output.Write(buf)
if err != nil {
log.Println("failed write to file:", err)
wg.Done()
} else if n != m {
log.Printf("read and write not equal, n: %d, m: %d", n, m)
l.Debug("unable to write to file", "error", err)
continue
}
log.Printf("read %d bytes to file", m)
l.Debug("wrote bytes to audio file", "bytes written", n)
}
}
func getDeviceStatus(dev *yalsa.Device) {
// Get card numbers
cardN := string(strings.Split(dev.Path, "C")[1][0])
devN := string(strings.Split(dev.Path, "D")[1][0])
file, err := os.ReadFile(fmt.Sprintf("/proc/asound/card%s/pcm%sc/sub0/status", cardN, devN))
if err != nil {
log.Println("could not read status:", err)
return
}
state := strings.Split(string(bytes.Trim(file, "state: ")), "\n")[0]
log.Println("device state:", state)
}

View File

2
go.mod
View File

@ -38,4 +38,6 @@ require (
go.uber.org/zap v1.10.0 // indirect
golang.org/x/image v0.0.0-20210216034530-4410531fe030 // indirect
golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect
periph.io/x/conn/v3 v3.7.0 // indirect
periph.io/x/host/v3 v3.8.2 // indirect
)

4
go.sum
View File

@ -170,4 +170,8 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
periph.io/x/conn/v3 v3.7.0 h1:f1EXLn4pkf7AEWwkol2gilCNZ0ElY+bxS4WE2PQXfrA=
periph.io/x/conn/v3 v3.7.0/go.mod h1:ypY7UVxgDbP9PJGwFSVelRRagxyXYfttVh7hJZUHEhg=
periph.io/x/host/v3 v3.8.2 h1:ayKUDzgUCN0g8+/xM9GTkWaOBhSLVcVHGTfjAOi8OsQ=
periph.io/x/host/v3 v3.8.2/go.mod h1:yFL76AesNHR68PboofSWYaQTKmvPXsQH2Apvp/ls/K4=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -270,6 +270,9 @@ type Config struct {
// TransformMatrix describes the projective transformation matrix to extract a target from the
// video data for turbidty calculations.
TransformMatrix []float64
// AlsaTitle describes the ALSA device title. This is used to specify which recording device to use.
AlsaTitle string
}
// Validate checks for any errors in the config fields and defaults settings

View File

@ -322,7 +322,6 @@ func (s *mtsSender) output() {
continue
case pool.ErrTimeout:
s.log.Warning("mtsSender: pool buffer read timeout")
s.log.Debug("chunk equal nil", "bool", chunk == nil)
continue
default:
s.log.Error("unexpected error", "error", err.Error())