From 7415776e4d79c275bf1c9722178448426aaac127 Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 20 Mar 2025 20:58:38 +0300 Subject: [PATCH] Add support alsa source --- internal/alsa/alsa.go | 7 ++ internal/alsa/alsa_linux.go | 80 ++++++++++++ internal/ffmpeg/ffmpeg.go | 1 + internal/ffmpeg/producer.go | 3 + main.go | 2 + pkg/README.md | 1 + pkg/alsa/README.md | 23 ++++ pkg/alsa/capture_linux.go | 89 ++++++++++++++ pkg/alsa/device/asound_32bit.go | 148 ++++++++++++++++++++++ pkg/alsa/device/asound_64bit.go | 148 ++++++++++++++++++++++ pkg/alsa/device/asound_arch.c | 163 ++++++++++++++++++++++++ pkg/alsa/device/asound_mipsle.go | 146 ++++++++++++++++++++++ pkg/alsa/device/device_linux.go | 205 +++++++++++++++++++++++++++++++ pkg/alsa/device/ioctl_linux.go | 26 ++++ pkg/alsa/open_linux.go | 39 ++++++ pkg/alsa/playback_linux.go | 94 ++++++++++++++ pkg/pcm/pcm.go | 77 ++++++++++++ www/add.html | 14 ++- 18 files changed, 1265 insertions(+), 1 deletion(-) create mode 100644 internal/alsa/alsa.go create mode 100644 internal/alsa/alsa_linux.go create mode 100644 pkg/alsa/README.md create mode 100644 pkg/alsa/capture_linux.go create mode 100644 pkg/alsa/device/asound_32bit.go create mode 100644 pkg/alsa/device/asound_64bit.go create mode 100644 pkg/alsa/device/asound_arch.c create mode 100644 pkg/alsa/device/asound_mipsle.go create mode 100644 pkg/alsa/device/device_linux.go create mode 100644 pkg/alsa/device/ioctl_linux.go create mode 100644 pkg/alsa/open_linux.go create mode 100644 pkg/alsa/playback_linux.go diff --git a/internal/alsa/alsa.go b/internal/alsa/alsa.go new file mode 100644 index 00000000..7886c74f --- /dev/null +++ b/internal/alsa/alsa.go @@ -0,0 +1,7 @@ +//go:build !(linux && (386 || amd64 || arm || arm64 || mipsle)) + +package alsa + +func Init() { + // not supported +} diff --git a/internal/alsa/alsa_linux.go b/internal/alsa/alsa_linux.go new file mode 100644 index 00000000..1fbdda95 --- /dev/null +++ b/internal/alsa/alsa_linux.go @@ -0,0 +1,80 @@ +//go:build linux && (386 || amd64 || arm || arm64 || mipsle) + +package alsa + +import ( + "fmt" + "net/http" + "os" + "strconv" + "strings" + + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/pkg/alsa" + "github.com/AlexxIT/go2rtc/pkg/alsa/device" +) + +func Init() { + streams.HandleFunc("alsa", alsa.Open) + + api.HandleFunc("api/alsa", apiAlsa) +} + +func apiAlsa(w http.ResponseWriter, r *http.Request) { + files, err := os.ReadDir("/dev/snd/") + if err != nil { + return + } + + var sources []*api.Source + + for _, file := range files { + if !strings.HasPrefix(file.Name(), "pcm") { + continue + } + + path := "/dev/snd/" + file.Name() + + dev, err := device.Open(path) + if err != nil { + continue + } + + info, err := dev.Info() + if err == nil { + formats := formatsToString(dev.ListFormats()) + r1, r2 := dev.RangeSampleRates() + c1, c2 := dev.RangeChannels() + source := &api.Source{ + Name: info.ID + " / " + info.Name + " / " + info.SubName, + Info: fmt.Sprintf("Formats: %s, Rates: %d-%d, Channels: %d-%d", formats, r1, r2, c1, c2), + URL: "alsa:device?audio=" + path, + } + sources = append(sources, source) + } + + _ = dev.Close() + } + + api.ResponseSources(w, sources) +} + +func formatsToString(formats []byte) string { + var s string + for i, format := range formats { + if i > 0 { + s += " " + } + switch format { + case 2: + s += "s16le" + case 10: + s += "s32le" + default: + s += strconv.Itoa(int(format)) + } + + } + return s +} diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index 8eba0a0b..e3b0c161 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -113,6 +113,7 @@ var defaults = map[string]string{ "pcm/48000": "-c:a pcm_s16be -ar:a 48000 -ac:a 1", "pcml": "-c:a pcm_s16le -ar:a 8000 -ac:a 1", "pcml/8000": "-c:a pcm_s16le -ar:a 8000 -ac:a 1", + "pcml/16000": "-c:a pcm_s16le -ar:a 16000 -ac:a 1", "pcml/44100": "-c:a pcm_s16le -ar:a 44100 -ac:a 1", // hardware Intel and AMD on Linux diff --git a/internal/ffmpeg/producer.go b/internal/ffmpeg/producer.go index d132d253..97cf3d5c 100644 --- a/internal/ffmpeg/producer.go +++ b/internal/ffmpeg/producer.go @@ -42,6 +42,7 @@ func NewProducer(url string) (core.Producer, error) { Codecs: []*core.Codec{ // OPUS will always marked as OPUS/48000/2 {Name: core.CodecOpus, ClockRate: 48000, Channels: 2}, + {Name: core.CodecPCML, ClockRate: 16000}, {Name: core.CodecPCM, ClockRate: 16000}, {Name: core.CodecPCMA, ClockRate: 16000}, {Name: core.CodecPCMU, ClockRate: 16000}, @@ -97,6 +98,8 @@ func (p *Producer) newURL() string { s += "#audio=opus" case core.CodecAAC: s += "#audio=aac/16000" + case core.CodecPCML: + s += "#audio=pcml/16000" case core.CodecPCM: s += "#audio=pcm/" + strconv.Itoa(int(codec.ClockRate)) case core.CodecPCMA: diff --git a/main.go b/main.go index 0f36cafb..f8aba89e 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "github.com/AlexxIT/go2rtc/internal/alsa" "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/api/ws" "github.com/AlexxIT/go2rtc/internal/app" @@ -90,6 +91,7 @@ func main() { gopro.Init() // gopro source doorbird.Init() // doorbird source v4l2.Init() // v4l2 source + alsa.Init() // alsa source flussonic.Init() eseecloud.Init() diff --git a/pkg/README.md b/pkg/README.md index b12f0a70..e2759638 100644 --- a/pkg/README.md +++ b/pkg/README.md @@ -13,6 +13,7 @@ Some formats and protocols go2rtc supports exclusively. They have no equivalent | Format | Source protocols | Ingress protocols | Recevers codecs | Senders codecs | Example | |--------------|------------------|-------------------|------------------------------|--------------------|---------------| | adts | http,tcp,pipe | http | aac | | `http:` | +| alsa | pipe | | | pcm | `alsa:` | | bubble | http | | h264,hevc,pcm_alaw | | `bubble:` | | dvrip | tcp | | h264,hevc,pcm_alaw,pcm_mulaw | pcm_alaw | `dvrip:` | | flv | http,tcp,pipe | http | h264,aac | | `http:` | diff --git a/pkg/alsa/README.md b/pkg/alsa/README.md new file mode 100644 index 00000000..b644af11 --- /dev/null +++ b/pkg/alsa/README.md @@ -0,0 +1,23 @@ +## Build + +```shell +x86_64-linux-gnu-gcc -w -static asound_arch.c -o asound_amd64 +i686-linux-gnu-gcc -w -static asound_arch.c -o asound_i386 +aarch64-linux-gnu-gcc -w -static asound_arch.c -o asound_arm64 +arm-linux-gnueabihf-gcc -w -static asound_arch.c -o asound_arm +mipsel-linux-gnu-gcc -w -static asound_arch.c -o asound_mipsle -D_TIME_BITS=32 +``` + +## Useful links + +- https://github.com/torvalds/linux/blob/master/include/uapi/sound/asound.h +- https://github.com/yobert/alsa +- https://github.com/Narsil/alsa-go +- https://github.com/alsa-project/alsa-lib +- https://github.com/anisse/alsa +- https://github.com/tinyalsa/tinyalsa + +**Broken pipe** + +- https://stackoverflow.com/questions/26545139/alsa-cannot-recovery-from-underrun-prepare-failed-broken-pipe +- https://klipspringer.avadeaux.net/alsa-broken-pipe-errors/ diff --git a/pkg/alsa/capture_linux.go b/pkg/alsa/capture_linux.go new file mode 100644 index 00000000..b1a9c1eb --- /dev/null +++ b/pkg/alsa/capture_linux.go @@ -0,0 +1,89 @@ +package alsa + +import ( + "github.com/AlexxIT/go2rtc/pkg/alsa/device" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" +) + +type Capture struct { + core.Connection + dev *device.Device + closed core.Waiter +} + +func newCapture(dev *device.Device) (*Capture, error) { + medias := []*core.Media{ + { + Kind: core.KindAudio, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + {Name: core.CodecPCML, ClockRate: 16000}, + }, + }, + } + return &Capture{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "alsa", + Medias: medias, + Transport: dev, + }, + dev: dev, + }, nil +} + +// readBufferSize - 20ms * 2 bytes per sample * 16000 frames per second * 2 channels / 1000ms per second +const readBufferSize = 20 * 2 * 16000 * 2 / 1000 + +// bytesPerFrame - 2 bytes per sample * 2 channels +const bytesPerFrame = 2 * 2 + +func (c *Capture) Start() error { + if err := c.dev.SetHWParams(device.SNDRV_PCM_FORMAT_S16_LE, 16000, 2); err != nil { + return err + } + + var ts uint32 + + b := make([]byte, readBufferSize) + for { + n, err := c.dev.Read(b) + if err != nil { + return err + } + + c.Recv += n + + if len(c.Receivers) == 0 { + continue + } + + pkt := &rtp.Packet{ + Header: rtp.Header{ + Version: 2, + Marker: true, + Timestamp: ts, + }, + Payload: stereoToMono(b[:n]), + } + c.Receivers[0].WriteRTP(pkt) + + ts += uint32(n / bytesPerFrame) + } +} + +func stereoToMono(stereo []byte) (mono []byte) { + n := len(stereo) + mono = make([]byte, n/2) + var i, j int + for i < n { + mono[j] = stereo[i] + j++ + i++ + mono[j] = stereo[i] + j++ + i += 3 + } + return +} diff --git a/pkg/alsa/device/asound_32bit.go b/pkg/alsa/device/asound_32bit.go new file mode 100644 index 00000000..428c876a --- /dev/null +++ b/pkg/alsa/device/asound_32bit.go @@ -0,0 +1,148 @@ +//go:build 386 || arm + +package device + +type unsigned_char = byte +type signed_int = int32 +type unsigned_int = uint32 +type signed_long = int64 +type unsigned_long = uint64 +type __u32 = uint32 +type void__user = uintptr + +const ( + SNDRV_PCM_STREAM_PLAYBACK = 0 + SNDRV_PCM_STREAM_CAPTURE = 1 + + SNDRV_PCM_ACCESS_MMAP_INTERLEAVED = 0 + SNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED = 1 + SNDRV_PCM_ACCESS_MMAP_COMPLEX = 2 + SNDRV_PCM_ACCESS_RW_INTERLEAVED = 3 + SNDRV_PCM_ACCESS_RW_NONINTERLEAVED = 4 + + SNDRV_PCM_FORMAT_S8 = 0 + SNDRV_PCM_FORMAT_U8 = 1 + SNDRV_PCM_FORMAT_S16_LE = 2 + SNDRV_PCM_FORMAT_S16_BE = 3 + SNDRV_PCM_FORMAT_U16_LE = 4 + SNDRV_PCM_FORMAT_U16_BE = 5 + SNDRV_PCM_FORMAT_S24_LE = 6 + SNDRV_PCM_FORMAT_S24_BE = 7 + SNDRV_PCM_FORMAT_U24_LE = 8 + SNDRV_PCM_FORMAT_U24_BE = 9 + SNDRV_PCM_FORMAT_S32_LE = 10 + SNDRV_PCM_FORMAT_S32_BE = 11 + SNDRV_PCM_FORMAT_U32_LE = 12 + SNDRV_PCM_FORMAT_U32_BE = 13 + SNDRV_PCM_FORMAT_FLOAT_LE = 14 + SNDRV_PCM_FORMAT_FLOAT_BE = 15 + SNDRV_PCM_FORMAT_FLOAT64_LE = 16 + SNDRV_PCM_FORMAT_FLOAT64_BE = 17 + SNDRV_PCM_FORMAT_MU_LAW = 20 + SNDRV_PCM_FORMAT_A_LAW = 21 + SNDRV_PCM_FORMAT_MPEG = 23 + + SNDRV_PCM_IOCTL_PVERSION = 0x80044100 + SNDRV_PCM_IOCTL_INFO = 0x81204101 + SNDRV_PCM_IOCTL_HW_REFINE = 0xc25c4110 + SNDRV_PCM_IOCTL_HW_PARAMS = 0xc25c4111 + SNDRV_PCM_IOCTL_SW_PARAMS = 0xc0684113 + SNDRV_PCM_IOCTL_PREPARE = 0x00004140 + SNDRV_PCM_IOCTL_WRITEI_FRAMES = 0x400c4150 + SNDRV_PCM_IOCTL_READI_FRAMES = 0x800c4151 +) + +type snd_pcm_info struct { // size 288 + device unsigned_int // offset 0, size 4 + subdevice unsigned_int // offset 4, size 4 + stream signed_int // offset 8, size 4 + card signed_int // offset 12, size 4 + id [64]unsigned_char // offset 16, size 64 + name [80]unsigned_char // offset 80, size 80 + subname [32]unsigned_char // offset 160, size 32 + dev_class signed_int // offset 192, size 4 + dev_subclass signed_int // offset 196, size 4 + subdevices_count unsigned_int // offset 200, size 4 + subdevices_avail unsigned_int // offset 204, size 4 + pad1 [16]unsigned_char + reserved [64]unsigned_char // offset 224, size 64 +} + +type snd_pcm_uframes_t = unsigned_long +type snd_pcm_sframes_t = signed_long + +type snd_xferi struct { // size 12 + result snd_pcm_sframes_t // offset 0, size 4 + buf void__user // offset 4, size 4 + frames snd_pcm_uframes_t // offset 8, size 4 +} + +const ( + SNDRV_PCM_HW_PARAM_ACCESS = 0 + SNDRV_PCM_HW_PARAM_FORMAT = 1 + SNDRV_PCM_HW_PARAM_SUBFORMAT = 2 + SNDRV_PCM_HW_PARAM_FIRST_MASK = 0 + SNDRV_PCM_HW_PARAM_LAST_MASK = 2 + + SNDRV_PCM_HW_PARAM_SAMPLE_BITS = 8 + SNDRV_PCM_HW_PARAM_FRAME_BITS = 9 + SNDRV_PCM_HW_PARAM_CHANNELS = 10 + SNDRV_PCM_HW_PARAM_RATE = 11 + SNDRV_PCM_HW_PARAM_PERIOD_TIME = 12 + SNDRV_PCM_HW_PARAM_PERIOD_SIZE = 13 + SNDRV_PCM_HW_PARAM_PERIOD_BYTES = 14 + SNDRV_PCM_HW_PARAM_PERIODS = 15 + SNDRV_PCM_HW_PARAM_BUFFER_TIME = 16 + SNDRV_PCM_HW_PARAM_BUFFER_SIZE = 17 + SNDRV_PCM_HW_PARAM_BUFFER_BYTES = 18 + SNDRV_PCM_HW_PARAM_TICK_TIME = 19 + SNDRV_PCM_HW_PARAM_FIRST_INTERVAL = 8 + SNDRV_PCM_HW_PARAM_LAST_INTERVAL = 19 + + SNDRV_MASK_MAX = 256 + + SNDRV_PCM_TSTAMP_NONE = 0 + SNDRV_PCM_TSTAMP_ENABLE = 1 +) + +type snd_mask struct { // size 32 + bits [(SNDRV_MASK_MAX + 31) / 32]__u32 // offset 0, size 32 +} + +type snd_interval struct { // size 12 + min unsigned_int // offset 0, size 4 + max unsigned_int // offset 4, size 4 + bit unsigned_int +} + +type snd_pcm_hw_params struct { // size 604 + flags unsigned_int // offset 0, size 4 + masks [SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK + 1]snd_mask // offset 4, size 96 + mres [5]snd_mask // offset 100, size 160 + intervals [SNDRV_PCM_HW_PARAM_LAST_INTERVAL - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1]snd_interval // offset 260, size 144 + ires [9]snd_interval // offset 404, size 108 + rmask unsigned_int // offset 512, size 4 + cmask unsigned_int // offset 516, size 4 + info unsigned_int // offset 520, size 4 + msbits unsigned_int // offset 524, size 4 + rate_num unsigned_int // offset 528, size 4 + rate_den unsigned_int // offset 532, size 4 + fifo_size snd_pcm_uframes_t // offset 536, size 4 + reserved [64]unsigned_char // offset 540, size 64 +} + +type snd_pcm_sw_params struct { // size 104 + tstamp_mode signed_int // offset 0, size 4 + period_step unsigned_int // offset 4, size 4 + sleep_min unsigned_int // offset 8, size 4 + avail_min snd_pcm_uframes_t // offset 12, size 4 + xfer_align snd_pcm_uframes_t // offset 16, size 4 + start_threshold snd_pcm_uframes_t // offset 20, size 4 + stop_threshold snd_pcm_uframes_t // offset 24, size 4 + silence_threshold snd_pcm_uframes_t // offset 28, size 4 + silence_size snd_pcm_uframes_t // offset 32, size 4 + boundary snd_pcm_uframes_t // offset 36, size 4 + proto unsigned_int // offset 40, size 4 + tstamp_type unsigned_int // offset 44, size 4 + reserved [56]unsigned_char // offset 48, size 56 +} diff --git a/pkg/alsa/device/asound_64bit.go b/pkg/alsa/device/asound_64bit.go new file mode 100644 index 00000000..14d0069c --- /dev/null +++ b/pkg/alsa/device/asound_64bit.go @@ -0,0 +1,148 @@ +//go:build amd64 || arm64 + +package device + +type unsigned_char = byte +type signed_int = int32 +type unsigned_int = uint32 +type signed_long = int64 +type unsigned_long = uint64 +type __u32 = uint32 +type void__user = uintptr + +const ( + SNDRV_PCM_STREAM_PLAYBACK = 0 + SNDRV_PCM_STREAM_CAPTURE = 1 + + SNDRV_PCM_ACCESS_MMAP_INTERLEAVED = 0 + SNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED = 1 + SNDRV_PCM_ACCESS_MMAP_COMPLEX = 2 + SNDRV_PCM_ACCESS_RW_INTERLEAVED = 3 + SNDRV_PCM_ACCESS_RW_NONINTERLEAVED = 4 + + SNDRV_PCM_FORMAT_S8 = 0 + SNDRV_PCM_FORMAT_U8 = 1 + SNDRV_PCM_FORMAT_S16_LE = 2 + SNDRV_PCM_FORMAT_S16_BE = 3 + SNDRV_PCM_FORMAT_U16_LE = 4 + SNDRV_PCM_FORMAT_U16_BE = 5 + SNDRV_PCM_FORMAT_S24_LE = 6 + SNDRV_PCM_FORMAT_S24_BE = 7 + SNDRV_PCM_FORMAT_U24_LE = 8 + SNDRV_PCM_FORMAT_U24_BE = 9 + SNDRV_PCM_FORMAT_S32_LE = 10 + SNDRV_PCM_FORMAT_S32_BE = 11 + SNDRV_PCM_FORMAT_U32_LE = 12 + SNDRV_PCM_FORMAT_U32_BE = 13 + SNDRV_PCM_FORMAT_FLOAT_LE = 14 + SNDRV_PCM_FORMAT_FLOAT_BE = 15 + SNDRV_PCM_FORMAT_FLOAT64_LE = 16 + SNDRV_PCM_FORMAT_FLOAT64_BE = 17 + SNDRV_PCM_FORMAT_MU_LAW = 20 + SNDRV_PCM_FORMAT_A_LAW = 21 + SNDRV_PCM_FORMAT_MPEG = 23 + + SNDRV_PCM_IOCTL_PVERSION = 0x80044100 + SNDRV_PCM_IOCTL_INFO = 0x81204101 + SNDRV_PCM_IOCTL_HW_REFINE = 0xc2604110 + SNDRV_PCM_IOCTL_HW_PARAMS = 0xc2604111 + SNDRV_PCM_IOCTL_SW_PARAMS = 0xc0884113 + SNDRV_PCM_IOCTL_PREPARE = 0x00004140 + SNDRV_PCM_IOCTL_WRITEI_FRAMES = 0x40184150 + SNDRV_PCM_IOCTL_READI_FRAMES = 0x80184151 +) + +type snd_pcm_info struct { // size 288 + device unsigned_int // offset 0, size 4 + subdevice unsigned_int // offset 4, size 4 + stream signed_int // offset 8, size 4 + card signed_int // offset 12, size 4 + id [64]unsigned_char // offset 16, size 64 + name [80]unsigned_char // offset 80, size 80 + subname [32]unsigned_char // offset 160, size 32 + dev_class signed_int // offset 192, size 4 + dev_subclass signed_int // offset 196, size 4 + subdevices_count unsigned_int // offset 200, size 4 + subdevices_avail unsigned_int // offset 204, size 4 + pad1 [16]unsigned_char + reserved [64]unsigned_char // offset 224, size 64 +} + +type snd_pcm_uframes_t = unsigned_long +type snd_pcm_sframes_t = signed_long + +type snd_xferi struct { // size 24 + result snd_pcm_sframes_t // offset 0, size 8 + buf void__user // offset 8, size 8 + frames snd_pcm_uframes_t // offset 16, size 8 +} + +const ( + SNDRV_PCM_HW_PARAM_ACCESS = 0 + SNDRV_PCM_HW_PARAM_FORMAT = 1 + SNDRV_PCM_HW_PARAM_SUBFORMAT = 2 + SNDRV_PCM_HW_PARAM_FIRST_MASK = 0 + SNDRV_PCM_HW_PARAM_LAST_MASK = 2 + + SNDRV_PCM_HW_PARAM_SAMPLE_BITS = 8 + SNDRV_PCM_HW_PARAM_FRAME_BITS = 9 + SNDRV_PCM_HW_PARAM_CHANNELS = 10 + SNDRV_PCM_HW_PARAM_RATE = 11 + SNDRV_PCM_HW_PARAM_PERIOD_TIME = 12 + SNDRV_PCM_HW_PARAM_PERIOD_SIZE = 13 + SNDRV_PCM_HW_PARAM_PERIOD_BYTES = 14 + SNDRV_PCM_HW_PARAM_PERIODS = 15 + SNDRV_PCM_HW_PARAM_BUFFER_TIME = 16 + SNDRV_PCM_HW_PARAM_BUFFER_SIZE = 17 + SNDRV_PCM_HW_PARAM_BUFFER_BYTES = 18 + SNDRV_PCM_HW_PARAM_TICK_TIME = 19 + SNDRV_PCM_HW_PARAM_FIRST_INTERVAL = 8 + SNDRV_PCM_HW_PARAM_LAST_INTERVAL = 19 + + SNDRV_MASK_MAX = 256 + + SNDRV_PCM_TSTAMP_NONE = 0 + SNDRV_PCM_TSTAMP_ENABLE = 1 +) + +type snd_mask struct { // size 32 + bits [(SNDRV_MASK_MAX + 31) / 32]__u32 // offset 0, size 32 +} + +type snd_interval struct { // size 12 + min unsigned_int // offset 0, size 4 + max unsigned_int // offset 4, size 4 + bit unsigned_int +} + +type snd_pcm_hw_params struct { // size 608 + flags unsigned_int // offset 0, size 4 + masks [SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK + 1]snd_mask // offset 4, size 96 + mres [5]snd_mask // offset 100, size 160 + intervals [SNDRV_PCM_HW_PARAM_LAST_INTERVAL - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1]snd_interval // offset 260, size 144 + ires [9]snd_interval // offset 404, size 108 + rmask unsigned_int // offset 512, size 4 + cmask unsigned_int // offset 516, size 4 + info unsigned_int // offset 520, size 4 + msbits unsigned_int // offset 524, size 4 + rate_num unsigned_int // offset 528, size 4 + rate_den unsigned_int // offset 532, size 4 + fifo_size snd_pcm_uframes_t // offset 536, size 8 + reserved [64]unsigned_char // offset 544, size 64 +} + +type snd_pcm_sw_params struct { // size 136 + tstamp_mode signed_int // offset 0, size 4 + period_step unsigned_int // offset 4, size 4 + sleep_min unsigned_int // offset 8, size 4 + avail_min snd_pcm_uframes_t // offset 16, size 8 + xfer_align snd_pcm_uframes_t // offset 24, size 8 + start_threshold snd_pcm_uframes_t // offset 32, size 8 + stop_threshold snd_pcm_uframes_t // offset 40, size 8 + silence_threshold snd_pcm_uframes_t // offset 48, size 8 + silence_size snd_pcm_uframes_t // offset 56, size 8 + boundary snd_pcm_uframes_t // offset 64, size 8 + proto unsigned_int // offset 72, size 4 + tstamp_type unsigned_int // offset 76, size 4 + reserved [56]unsigned_char // offset 80, size 56 +} diff --git a/pkg/alsa/device/asound_arch.c b/pkg/alsa/device/asound_arch.c new file mode 100644 index 00000000..0f895fb1 --- /dev/null +++ b/pkg/alsa/device/asound_arch.c @@ -0,0 +1,163 @@ +#include +#include +#include +#include + +#define print_line(text) printf("%s\n", text) +#define print_hex_const(name) printf("\t%s = 0x%08lx\n", #name, name) +#define print_int_const(con) printf("\t%s = %d\n", #con, con) + +#define print_struct_header(str) printf("type %s struct { // size %lu\n", #str, sizeof(struct str)) +#define print_struct_member(str, mem, typ) printf("\t%s %s // offset %lu, size %lu\n", #mem == "type" ? "typ" : #mem, typ, offsetof(struct str, mem), sizeof((struct str){0}.mem)) + +// https://github.com/torvalds/linux/blob/master/include/uapi/sound/asound.h +int main() { + print_line("package device\n"); + + print_line("type unsigned_char = byte"); + print_line("type signed_int = int32"); + print_line("type unsigned_int = uint32"); + print_line("type signed_long = int64"); + print_line("type unsigned_long = uint64"); + print_line("type __u32 = uint32"); + print_line("type void__user = uintptr\n"); + + print_line("const ("); + print_int_const(SNDRV_PCM_STREAM_PLAYBACK); + print_int_const(SNDRV_PCM_STREAM_CAPTURE); + print_line(""); + print_int_const(SNDRV_PCM_ACCESS_MMAP_INTERLEAVED); + print_int_const(SNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED); + print_int_const(SNDRV_PCM_ACCESS_MMAP_COMPLEX); + print_int_const(SNDRV_PCM_ACCESS_RW_INTERLEAVED); + print_int_const(SNDRV_PCM_ACCESS_RW_NONINTERLEAVED); + print_line(""); + print_int_const(SNDRV_PCM_FORMAT_S8); + print_int_const(SNDRV_PCM_FORMAT_U8); + print_int_const(SNDRV_PCM_FORMAT_S16_LE); + print_int_const(SNDRV_PCM_FORMAT_S16_BE); + print_int_const(SNDRV_PCM_FORMAT_U16_LE); + print_int_const(SNDRV_PCM_FORMAT_U16_BE); + print_int_const(SNDRV_PCM_FORMAT_S24_LE); + print_int_const(SNDRV_PCM_FORMAT_S24_BE); + print_int_const(SNDRV_PCM_FORMAT_U24_LE); + print_int_const(SNDRV_PCM_FORMAT_U24_BE); + print_int_const(SNDRV_PCM_FORMAT_S32_LE); + print_int_const(SNDRV_PCM_FORMAT_S32_BE); + print_int_const(SNDRV_PCM_FORMAT_U32_LE); + print_int_const(SNDRV_PCM_FORMAT_U32_BE); + print_int_const(SNDRV_PCM_FORMAT_FLOAT_LE); + print_int_const(SNDRV_PCM_FORMAT_FLOAT_BE); + print_int_const(SNDRV_PCM_FORMAT_FLOAT64_LE); + print_int_const(SNDRV_PCM_FORMAT_FLOAT64_BE); + print_int_const(SNDRV_PCM_FORMAT_MU_LAW); + print_int_const(SNDRV_PCM_FORMAT_A_LAW); + print_int_const(SNDRV_PCM_FORMAT_MPEG); + print_line(""); + print_hex_const(SNDRV_PCM_IOCTL_PVERSION); // A 0x00 + print_hex_const(SNDRV_PCM_IOCTL_INFO); // A 0x01 + print_hex_const(SNDRV_PCM_IOCTL_HW_REFINE); // A 0x10 + print_hex_const(SNDRV_PCM_IOCTL_HW_PARAMS); // A 0x11 + print_hex_const(SNDRV_PCM_IOCTL_SW_PARAMS); // A 0x13 + print_hex_const(SNDRV_PCM_IOCTL_PREPARE); // A 0x40 + print_hex_const(SNDRV_PCM_IOCTL_WRITEI_FRAMES); // A 0x50 + print_hex_const(SNDRV_PCM_IOCTL_READI_FRAMES); // A 0x51 + print_line(")\n"); + + print_struct_header(snd_pcm_info); + print_struct_member(snd_pcm_info, device, "unsigned_int"); + print_struct_member(snd_pcm_info, subdevice, "unsigned_int"); + print_struct_member(snd_pcm_info, stream, "signed_int"); + print_struct_member(snd_pcm_info, card, "signed_int"); + print_struct_member(snd_pcm_info, id, "[64]unsigned_char"); + print_struct_member(snd_pcm_info, name, "[80]unsigned_char"); + print_struct_member(snd_pcm_info, subname, "[32]unsigned_char"); + print_struct_member(snd_pcm_info, dev_class, "signed_int"); + print_struct_member(snd_pcm_info, dev_subclass, "signed_int"); + print_struct_member(snd_pcm_info, subdevices_count, "unsigned_int"); + print_struct_member(snd_pcm_info, subdevices_avail, "unsigned_int"); + print_line("\tpad1 [16]unsigned_char"); + print_struct_member(snd_pcm_info, reserved, "[64]unsigned_char"); + print_line("}\n"); + + print_line("type snd_pcm_uframes_t = unsigned_long"); + print_line("type snd_pcm_sframes_t = signed_long\n"); + + print_struct_header(snd_xferi); + print_struct_member(snd_xferi, result, "snd_pcm_sframes_t"); + print_struct_member(snd_xferi, buf, "void__user"); + print_struct_member(snd_xferi, frames, "snd_pcm_uframes_t"); + print_line("}\n"); + + print_line("const ("); + print_int_const(SNDRV_PCM_HW_PARAM_ACCESS); + print_int_const(SNDRV_PCM_HW_PARAM_FORMAT); + print_int_const(SNDRV_PCM_HW_PARAM_SUBFORMAT); + print_int_const(SNDRV_PCM_HW_PARAM_FIRST_MASK); + print_int_const(SNDRV_PCM_HW_PARAM_LAST_MASK); + print_line(""); + print_int_const(SNDRV_PCM_HW_PARAM_SAMPLE_BITS); + print_int_const(SNDRV_PCM_HW_PARAM_FRAME_BITS); + print_int_const(SNDRV_PCM_HW_PARAM_CHANNELS); + print_int_const(SNDRV_PCM_HW_PARAM_RATE); + print_int_const(SNDRV_PCM_HW_PARAM_PERIOD_TIME); + print_int_const(SNDRV_PCM_HW_PARAM_PERIOD_SIZE); + print_int_const(SNDRV_PCM_HW_PARAM_PERIOD_BYTES); + print_int_const(SNDRV_PCM_HW_PARAM_PERIODS); + print_int_const(SNDRV_PCM_HW_PARAM_BUFFER_TIME); + print_int_const(SNDRV_PCM_HW_PARAM_BUFFER_SIZE); + print_int_const(SNDRV_PCM_HW_PARAM_BUFFER_BYTES); + print_int_const(SNDRV_PCM_HW_PARAM_TICK_TIME); + print_int_const(SNDRV_PCM_HW_PARAM_FIRST_INTERVAL); + print_int_const(SNDRV_PCM_HW_PARAM_LAST_INTERVAL); + print_line(""); + print_int_const(SNDRV_MASK_MAX); + print_line(""); + print_int_const(SNDRV_PCM_TSTAMP_NONE); + print_int_const(SNDRV_PCM_TSTAMP_ENABLE); + print_line(")\n"); + + print_struct_header(snd_mask); + print_struct_member(snd_mask, bits, "[(SNDRV_MASK_MAX+31)/32]__u32"); + print_line("}\n"); + + print_struct_header(snd_interval); + print_struct_member(snd_interval, min, "unsigned_int"); + print_struct_member(snd_interval, max, "unsigned_int"); + print_line("\tbit unsigned_int"); + print_line("}\n"); + + print_struct_header(snd_pcm_hw_params); + print_struct_member(snd_pcm_hw_params, flags, "unsigned_int"); + print_struct_member(snd_pcm_hw_params, masks, "[SNDRV_PCM_HW_PARAM_LAST_MASK-SNDRV_PCM_HW_PARAM_FIRST_MASK+1]snd_mask"); + print_struct_member(snd_pcm_hw_params, mres, "[5]snd_mask"); + print_struct_member(snd_pcm_hw_params, intervals, "[SNDRV_PCM_HW_PARAM_LAST_INTERVAL-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL+1]snd_interval"); + print_struct_member(snd_pcm_hw_params, ires, "[9]snd_interval"); + print_struct_member(snd_pcm_hw_params, rmask, "unsigned_int"); + print_struct_member(snd_pcm_hw_params, cmask, "unsigned_int"); + print_struct_member(snd_pcm_hw_params, info, "unsigned_int"); + print_struct_member(snd_pcm_hw_params, msbits, "unsigned_int"); + print_struct_member(snd_pcm_hw_params, rate_num, "unsigned_int"); + print_struct_member(snd_pcm_hw_params, rate_den, "unsigned_int"); + print_struct_member(snd_pcm_hw_params, fifo_size, "snd_pcm_uframes_t"); + print_struct_member(snd_pcm_hw_params, reserved, "[64]unsigned_char"); + print_line("}\n"); + + print_struct_header(snd_pcm_sw_params); + print_struct_member(snd_pcm_sw_params, tstamp_mode, "signed_int"); + print_struct_member(snd_pcm_sw_params, period_step, "unsigned_int"); + print_struct_member(snd_pcm_sw_params, sleep_min, "unsigned_int"); + print_struct_member(snd_pcm_sw_params, avail_min, "snd_pcm_uframes_t"); + print_struct_member(snd_pcm_sw_params, xfer_align, "snd_pcm_uframes_t"); + print_struct_member(snd_pcm_sw_params, start_threshold, "snd_pcm_uframes_t"); + print_struct_member(snd_pcm_sw_params, stop_threshold, "snd_pcm_uframes_t"); + print_struct_member(snd_pcm_sw_params, silence_threshold, "snd_pcm_uframes_t"); + print_struct_member(snd_pcm_sw_params, silence_size, "snd_pcm_uframes_t"); + print_struct_member(snd_pcm_sw_params, boundary, "snd_pcm_uframes_t"); + print_struct_member(snd_pcm_sw_params, proto, "unsigned_int"); + print_struct_member(snd_pcm_sw_params, tstamp_type, "unsigned_int"); + print_struct_member(snd_pcm_sw_params, reserved, "[56]unsigned_char"); + print_line("}\n"); + + return 0; +} \ No newline at end of file diff --git a/pkg/alsa/device/asound_mipsle.go b/pkg/alsa/device/asound_mipsle.go new file mode 100644 index 00000000..743c89dd --- /dev/null +++ b/pkg/alsa/device/asound_mipsle.go @@ -0,0 +1,146 @@ +package device + +type unsigned_char = byte +type signed_int = int32 +type unsigned_int = uint32 +type signed_long = int64 +type unsigned_long = uint64 +type __u32 = uint32 +type void__user = uintptr + +const ( + SNDRV_PCM_STREAM_PLAYBACK = 0 + SNDRV_PCM_STREAM_CAPTURE = 1 + + SNDRV_PCM_ACCESS_MMAP_INTERLEAVED = 0 + SNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED = 1 + SNDRV_PCM_ACCESS_MMAP_COMPLEX = 2 + SNDRV_PCM_ACCESS_RW_INTERLEAVED = 3 + SNDRV_PCM_ACCESS_RW_NONINTERLEAVED = 4 + + SNDRV_PCM_FORMAT_S8 = 0 + SNDRV_PCM_FORMAT_U8 = 1 + SNDRV_PCM_FORMAT_S16_LE = 2 + SNDRV_PCM_FORMAT_S16_BE = 3 + SNDRV_PCM_FORMAT_U16_LE = 4 + SNDRV_PCM_FORMAT_U16_BE = 5 + SNDRV_PCM_FORMAT_S24_LE = 6 + SNDRV_PCM_FORMAT_S24_BE = 7 + SNDRV_PCM_FORMAT_U24_LE = 8 + SNDRV_PCM_FORMAT_U24_BE = 9 + SNDRV_PCM_FORMAT_S32_LE = 10 + SNDRV_PCM_FORMAT_S32_BE = 11 + SNDRV_PCM_FORMAT_U32_LE = 12 + SNDRV_PCM_FORMAT_U32_BE = 13 + SNDRV_PCM_FORMAT_FLOAT_LE = 14 + SNDRV_PCM_FORMAT_FLOAT_BE = 15 + SNDRV_PCM_FORMAT_FLOAT64_LE = 16 + SNDRV_PCM_FORMAT_FLOAT64_BE = 17 + SNDRV_PCM_FORMAT_MU_LAW = 20 + SNDRV_PCM_FORMAT_A_LAW = 21 + SNDRV_PCM_FORMAT_MPEG = 23 + + SNDRV_PCM_IOCTL_PVERSION = 0x40044100 + SNDRV_PCM_IOCTL_INFO = 0x41204101 + SNDRV_PCM_IOCTL_HW_REFINE = 0xc25c4110 + SNDRV_PCM_IOCTL_HW_PARAMS = 0xc25c4111 + SNDRV_PCM_IOCTL_SW_PARAMS = 0xc0684113 + SNDRV_PCM_IOCTL_PREPARE = 0x20004140 + SNDRV_PCM_IOCTL_WRITEI_FRAMES = 0x800c4150 + SNDRV_PCM_IOCTL_READI_FRAMES = 0x400c4151 +) + +type snd_pcm_info struct { // size 288 + device unsigned_int // offset 0, size 4 + subdevice unsigned_int // offset 4, size 4 + stream signed_int // offset 8, size 4 + card signed_int // offset 12, size 4 + id [64]unsigned_char // offset 16, size 64 + name [80]unsigned_char // offset 80, size 80 + subname [32]unsigned_char // offset 160, size 32 + dev_class signed_int // offset 192, size 4 + dev_subclass signed_int // offset 196, size 4 + subdevices_count unsigned_int // offset 200, size 4 + subdevices_avail unsigned_int // offset 204, size 4 + pad1 [16]unsigned_char + reserved [64]unsigned_char // offset 224, size 64 +} + +type snd_pcm_uframes_t = unsigned_long +type snd_pcm_sframes_t = signed_long + +type snd_xferi struct { // size 12 + result snd_pcm_sframes_t // offset 0, size 4 + buf void__user // offset 4, size 4 + frames snd_pcm_uframes_t // offset 8, size 4 +} + +const ( + SNDRV_PCM_HW_PARAM_ACCESS = 0 + SNDRV_PCM_HW_PARAM_FORMAT = 1 + SNDRV_PCM_HW_PARAM_SUBFORMAT = 2 + SNDRV_PCM_HW_PARAM_FIRST_MASK = 0 + SNDRV_PCM_HW_PARAM_LAST_MASK = 2 + + SNDRV_PCM_HW_PARAM_SAMPLE_BITS = 8 + SNDRV_PCM_HW_PARAM_FRAME_BITS = 9 + SNDRV_PCM_HW_PARAM_CHANNELS = 10 + SNDRV_PCM_HW_PARAM_RATE = 11 + SNDRV_PCM_HW_PARAM_PERIOD_TIME = 12 + SNDRV_PCM_HW_PARAM_PERIOD_SIZE = 13 + SNDRV_PCM_HW_PARAM_PERIOD_BYTES = 14 + SNDRV_PCM_HW_PARAM_PERIODS = 15 + SNDRV_PCM_HW_PARAM_BUFFER_TIME = 16 + SNDRV_PCM_HW_PARAM_BUFFER_SIZE = 17 + SNDRV_PCM_HW_PARAM_BUFFER_BYTES = 18 + SNDRV_PCM_HW_PARAM_TICK_TIME = 19 + SNDRV_PCM_HW_PARAM_FIRST_INTERVAL = 8 + SNDRV_PCM_HW_PARAM_LAST_INTERVAL = 19 + + SNDRV_MASK_MAX = 256 + + SNDRV_PCM_TSTAMP_NONE = 0 + SNDRV_PCM_TSTAMP_ENABLE = 1 +) + +type snd_mask struct { // size 32 + bits [(SNDRV_MASK_MAX + 31) / 32]__u32 // offset 0, size 32 +} + +type snd_interval struct { // size 12 + min unsigned_int // offset 0, size 4 + max unsigned_int // offset 4, size 4 + bit unsigned_int +} + +type snd_pcm_hw_params struct { // size 604 + flags unsigned_int // offset 0, size 4 + masks [SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK + 1]snd_mask // offset 4, size 96 + mres [5]snd_mask // offset 100, size 160 + intervals [SNDRV_PCM_HW_PARAM_LAST_INTERVAL - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1]snd_interval // offset 260, size 144 + ires [9]snd_interval // offset 404, size 108 + rmask unsigned_int // offset 512, size 4 + cmask unsigned_int // offset 516, size 4 + info unsigned_int // offset 520, size 4 + msbits unsigned_int // offset 524, size 4 + rate_num unsigned_int // offset 528, size 4 + rate_den unsigned_int // offset 532, size 4 + fifo_size snd_pcm_uframes_t // offset 536, size 4 + reserved [64]unsigned_char // offset 540, size 64 +} + +type snd_pcm_sw_params struct { // size 104 + tstamp_mode signed_int // offset 0, size 4 + period_step unsigned_int // offset 4, size 4 + sleep_min unsigned_int // offset 8, size 4 + avail_min snd_pcm_uframes_t // offset 12, size 4 + xfer_align snd_pcm_uframes_t // offset 16, size 4 + start_threshold snd_pcm_uframes_t // offset 20, size 4 + stop_threshold snd_pcm_uframes_t // offset 24, size 4 + silence_threshold snd_pcm_uframes_t // offset 28, size 4 + silence_size snd_pcm_uframes_t // offset 32, size 4 + boundary snd_pcm_uframes_t // offset 36, size 4 + proto unsigned_int // offset 40, size 4 + tstamp_type unsigned_int // offset 44, size 4 + reserved [56]unsigned_char // offset 48, size 56 +} diff --git a/pkg/alsa/device/device_linux.go b/pkg/alsa/device/device_linux.go new file mode 100644 index 00000000..d041eda6 --- /dev/null +++ b/pkg/alsa/device/device_linux.go @@ -0,0 +1,205 @@ +package device + +import ( + "fmt" + "syscall" + "unsafe" +) + +type Device struct { + fd uintptr + path string + + hwparams snd_pcm_hw_params + frameBytes int // sample size * channels +} + +func Open(path string) (*Device, error) { + // important to use nonblock because can get lock + fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_NONBLOCK, 0) + if err != nil { + return nil, err + } + + // important to remove nonblock because better to handle reads and writes + if err = syscall.SetNonblock(fd, false); err != nil { + return nil, err + } + + d := &Device{fd: uintptr(fd), path: path} + d.init() + + // load all supported formats, channels, rates, etc. + if err = ioctl(d.fd, SNDRV_PCM_IOCTL_HW_REFINE, &d.hwparams); err != nil { + _ = d.Close() + return nil, err + } + + d.setMask(SNDRV_PCM_HW_PARAM_ACCESS, SNDRV_PCM_ACCESS_RW_INTERLEAVED) + + return d, nil +} + +func (d *Device) Close() error { + return syscall.Close(int(d.fd)) +} + +func (d *Device) IsCapture() bool { + // path: /dev/snd/pcmC0D0c, where p - playback, c - capture + return d.path[len(d.path)-1] == 'c' +} + +type Info struct { + Card int + Device int + SubDevice int + Stream int + ID string + Name string + SubName string +} + +func (d *Device) Info() (*Info, error) { + var info snd_pcm_info + if err := ioctl(d.fd, SNDRV_PCM_IOCTL_INFO, &info); err != nil { + return nil, err + } + return &Info{ + Card: int(info.card), + Device: int(info.device), + SubDevice: int(info.subdevice), + Stream: int(info.stream), + ID: str(info.id[:]), + Name: str(info.name[:]), + SubName: str(info.subname[:]), + }, nil +} + +func (d *Device) ListFormats() (formats []byte) { + for i := byte(0); i <= 28; i++ { + if d.checkMask(SNDRV_PCM_HW_PARAM_FORMAT, uint32(i)) { + formats = append(formats, i) + } + } + return +} + +func (d *Device) RangeChannels() (byte, byte) { + minCh, maxCh := d.getInterval(SNDRV_PCM_HW_PARAM_CHANNELS) + return byte(minCh), byte(maxCh) +} + +func (d *Device) RangeSampleRates() (uint32, uint32) { + return d.getInterval(SNDRV_PCM_HW_PARAM_RATE) +} + +const bufferSize = 4096 + +func (d *Device) SetHWParams(format byte, sampleRate uint32, channels byte) error { + d.setInterval(SNDRV_PCM_HW_PARAM_CHANNELS, uint32(channels)) + d.setInterval(SNDRV_PCM_HW_PARAM_RATE, sampleRate) + d.setMask(SNDRV_PCM_HW_PARAM_FORMAT, uint32(format)) + //d.setMask(SNDRV_PCM_HW_PARAM_SUBFORMAT, 0) + + // important for smooth playback + d.setInterval(SNDRV_PCM_HW_PARAM_BUFFER_SIZE, bufferSize) + //d.setInterval(SNDRV_PCM_HW_PARAM_PERIOD_SIZE, 2000) + + if err := ioctl(d.fd, SNDRV_PCM_IOCTL_HW_PARAMS, &d.hwparams); err != nil { + return fmt.Errorf("[alsa] set hw_params: %w", err) + } + + _, i := d.getInterval(SNDRV_PCM_HW_PARAM_FRAME_BITS) + d.frameBytes = int(i / 8) + + _, periods := d.getInterval(SNDRV_PCM_HW_PARAM_PERIODS) + _, periodSize := d.getInterval(SNDRV_PCM_HW_PARAM_PERIOD_SIZE) + threshold := snd_pcm_uframes_t(periods * periodSize) // same as bufferSize + + swparams := snd_pcm_sw_params{ + //tstamp_mode: SNDRV_PCM_TSTAMP_ENABLE, + period_step: 1, + avail_min: 1, // start as soon as possible + stop_threshold: threshold, + } + + if d.IsCapture() { + swparams.start_threshold = 1 + } else { + swparams.start_threshold = threshold + } + + if err := ioctl(d.fd, SNDRV_PCM_IOCTL_SW_PARAMS, &swparams); err != nil { + return fmt.Errorf("[alsa] set sw_params: %w", err) + } + + if err := ioctl(d.fd, SNDRV_PCM_IOCTL_PREPARE, nil); err != nil { + return fmt.Errorf("[alsa] prepare: %w", err) + } + + return nil +} + +func (d *Device) Write(b []byte) (n int, err error) { + xfer := &snd_xferi{ + buf: uintptr(unsafe.Pointer(&b[0])), + frames: snd_pcm_uframes_t(len(b) / d.frameBytes), + } + err = ioctl(d.fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, xfer) + if err == syscall.EPIPE { + // auto handle underrun state + // https://stackoverflow.com/questions/59396728/how-to-properly-handle-xrun-in-alsa-programming-when-playing-audio-with-snd-pcm + err = ioctl(d.fd, SNDRV_PCM_IOCTL_PREPARE, nil) + } + n = int(xfer.result) * d.frameBytes + return +} + +func (d *Device) Read(b []byte) (n int, err error) { + xfer := &snd_xferi{ + buf: uintptr(unsafe.Pointer(&b[0])), + frames: snd_pcm_uframes_t(len(b) / d.frameBytes), + } + err = ioctl(d.fd, SNDRV_PCM_IOCTL_READI_FRAMES, xfer) + n = int(xfer.result) * d.frameBytes + return +} + +func (d *Device) init() { + for i := range d.hwparams.masks { + d.hwparams.masks[i].bits[0] = 0xFFFFFFFF + d.hwparams.masks[i].bits[1] = 0xFFFFFFFF + } + for i := range d.hwparams.intervals { + d.hwparams.intervals[i].max = 0xFFFFFFFF + } + + d.hwparams.rmask = 0xFFFFFFFF + d.hwparams.cmask = 0 + d.hwparams.info = 0xFFFFFFFF +} + +func (d *Device) setInterval(param, val uint32) { + d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min = val + d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].max = val + d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].bit = 0b0100 // integer +} + +func (d *Device) setIntervalMin(param, val uint32) { + d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min = val +} + +func (d *Device) getInterval(param uint32) (uint32, uint32) { + return d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min, + d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].max +} + +func (d *Device) setMask(mask, val uint32) { + d.hwparams.masks[mask].bits[0] = 0 + d.hwparams.masks[mask].bits[1] = 0 + d.hwparams.masks[mask].bits[val>>5] = 1 << (val & 0x1F) +} + +func (d *Device) checkMask(mask, val uint32) bool { + return d.hwparams.masks[mask].bits[val>>5]&(1<<(val&0x1F)) > 0 +} diff --git a/pkg/alsa/device/ioctl_linux.go b/pkg/alsa/device/ioctl_linux.go new file mode 100644 index 00000000..1277a601 --- /dev/null +++ b/pkg/alsa/device/ioctl_linux.go @@ -0,0 +1,26 @@ +package device + +import ( + "bytes" + "reflect" + "syscall" +) + +func ioctl(fd, req uintptr, arg any) error { + var ptr uintptr + if arg != nil { + ptr = reflect.ValueOf(arg).Pointer() + } + _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, req, ptr) + if err != 0 { + return err + } + return nil +} + +func str(b []byte) string { + if i := bytes.IndexByte(b, 0); i >= 0 { + return string(b[:i]) + } + return string(b) +} diff --git a/pkg/alsa/open_linux.go b/pkg/alsa/open_linux.go new file mode 100644 index 00000000..ccdaf73a --- /dev/null +++ b/pkg/alsa/open_linux.go @@ -0,0 +1,39 @@ +package alsa + +import ( + "fmt" + "net/url" + + "github.com/AlexxIT/go2rtc/pkg/alsa/device" + "github.com/AlexxIT/go2rtc/pkg/core" +) + +func Open(rawURL string) (core.Producer, error) { + // Example (ffmpeg source compatible): + // alsa:device?audio=/dev/snd/pcmC0D0p + // TODO: ?audio=default + // TODO: ?audio=hw:0,0 + // TODO: &sample_rate=48000&channels=2 + // TODO: &backchannel=1 + u, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + + path := u.Query().Get("audio") + dev, err := device.Open(path) + if err != nil { + return nil, err + } + + switch path[len(path)-1] { + case 'p': // playback + return newPlayback(dev) + case 'c': // capture + return newCapture(dev) + } + + _ = dev.Close() + + return nil, fmt.Errorf("alsa: unknown path: %s", path) +} diff --git a/pkg/alsa/playback_linux.go b/pkg/alsa/playback_linux.go new file mode 100644 index 00000000..80c41890 --- /dev/null +++ b/pkg/alsa/playback_linux.go @@ -0,0 +1,94 @@ +package alsa + +import ( + "fmt" + + "github.com/AlexxIT/go2rtc/pkg/alsa/device" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/pcm" + "github.com/pion/rtp" +) + +type Playback struct { + core.Connection + dev *device.Device + closed core.Waiter +} + +func newPlayback(dev *device.Device) (*Playback, error) { + medias := []*core.Media{ + { + Kind: core.KindAudio, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecPCML}, // support ffmpeg producer (auto transcode) + {Name: core.CodecPCMA, ClockRate: 8000}, // support webrtc producer + }, + }, + } + return &Playback{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "alsa", + Medias: medias, + Transport: dev, + }, + dev: dev, + }, nil +} + +func (p *Playback) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { + return nil, core.ErrCantGetTrack +} + +func (p *Playback) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { + in := track.Codec + + // support probe + if in.Name == core.CodecAny { + in = &core.Codec{ + Name: core.CodecPCML, + ClockRate: 16000, + Channels: 2, + } + } + + out := &core.Codec{ + Name: core.CodecPCML, + ClockRate: in.ClockRate, + Channels: 2, + } + sender := core.NewSender(media, out) + + sender.Handler = func(pkt *rtp.Packet) { + if n, err := p.dev.Write(pkt.Payload); err == nil { + p.Send += n + } + } + + if sender.Handler = pcm.Convert(in, out, sender.Handler); sender.Handler == nil { + return fmt.Errorf("alsa: can't convert %s to %s", in, out) + } + + // typical card support: + // - Formats: S16_LE, S32_LE + // - ClockRates: 8000 - 192000 + // - Channels: 2 - 10 + err := p.dev.SetHWParams(device.SNDRV_PCM_FORMAT_S16_LE, out.ClockRate, 2) + if err != nil { + return err + } + + sender.HandleRTP(track) + p.Senders = append(p.Senders, sender) + return nil +} + +func (p *Playback) Start() (err error) { + return p.closed.Wait() +} + +func (p *Playback) Stop() error { + p.closed.Done(nil) + return p.Connection.Stop() +} diff --git a/pkg/pcm/pcm.go b/pkg/pcm/pcm.go index 60062b62..a7556d16 100644 --- a/pkg/pcm/pcm.go +++ b/pkg/pcm/pcm.go @@ -198,3 +198,80 @@ func RepackG711(zeroTS bool, handler core.HandlerFunc) core.HandlerFunc { handler(pkt) } } + +func Convert(in, out *core.Codec, handler core.HandlerFunc) core.HandlerFunc { + if in.Name == out.Name && in.Channels == out.Channels && in.ClockRate == out.ClockRate { + return handler + } + + switch { + case in.Name == core.CodecPCML && in.Channels <= 1 && + out.Name == core.CodecPCML && out.Channels == 2: + return func(pkt *core.Packet) { + n := len(pkt.Payload) + payload := make([]byte, 2*n) + for i, j := 0, 0; i < n; { + hi := pkt.Payload[i] + i++ + lo := pkt.Payload[i] + i++ + payload[j] = hi + j++ + payload[j] = lo + j++ + payload[j] = hi + j++ + payload[j] = lo + j++ + } + pkt.Payload = payload + handler(pkt) + } + + case in.Name == core.CodecPCM && in.Channels <= 1 && + out.Name == core.CodecPCML && out.Channels == 2: + return func(pkt *core.Packet) { + n := len(pkt.Payload) + payload := make([]byte, 2*n) + for i, j := 0, 0; i < n; { + hi := pkt.Payload[i] + i++ + lo := pkt.Payload[i] + i++ + payload[j] = lo + j++ + payload[j] = hi + j++ + payload[j] = lo + j++ + payload[j] = hi + j++ + } + pkt.Payload = payload + handler(pkt) + } + + case in.Name == core.CodecPCMA && in.Channels <= 1 && + out.Name == core.CodecPCML && out.Channels == 2: + return func(pkt *core.Packet) { + payload := make([]byte, 4*len(pkt.Payload)) + var i int + for _, b := range pkt.Payload { + s16 := PCMAtoPCM(b) + hi := byte(s16 >> 8) + lo := byte(s16) + payload[i] = hi + i++ + payload[i] = lo + i++ + payload[i] = hi + i++ + payload[i] = lo + i++ + } + pkt.Payload = payload + handler(pkt) + } + } + return nil +} diff --git a/www/add.html b/www/add.html index cec8ed36..c8808736 100644 --- a/www/add.html +++ b/www/add.html @@ -84,6 +84,18 @@ + +
+
+
+ + +
@@ -341,7 +353,7 @@ - +