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..316a7594 --- /dev/null +++ b/internal/alsa/alsa_linux.go @@ -0,0 +1,83 @@ +//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.RangeRates() + c1, c2 := dev.RangeChannels() + source := &api.Source{ + Name: info.ID, + Info: fmt.Sprintf("Formats: %s, Rates: %d-%d, Channels: %d-%d", formats, r1, r2, c1, c2), + URL: "alsa:device?audio=" + path, + } + if !strings.Contains(source.Name, info.Name) { + source.Name += ", " + info.Name + } + 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/internal/streams/play.go b/internal/streams/play.go index 9bec7258..d72c5e0c 100644 --- a/internal/streams/play.go +++ b/internal/streams/play.go @@ -140,10 +140,12 @@ func matchMedia(prod core.Producer, cons core.Consumer) bool { track, err := prod.GetTrack(prodMedia, prodCodec) if err != nil { + log.Warn().Err(err).Msg("[streams] can't get track") continue } if err = cons.AddTrack(consMedia, consCodec, track); err != nil { + log.Warn().Err(err).Msg("[streams] can't add track") continue } 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/aac/aac.go b/pkg/aac/aac.go index 5ce4e82d..dc961fc4 100644 --- a/pkg/aac/aac.go +++ b/pkg/aac/aac.go @@ -53,7 +53,7 @@ func ConfigToCodec(conf []byte) *core.Codec { codec.ClockRate = rd.ReadBits(24) } - codec.Channels = rd.ReadBits16(4) + codec.Channels = rd.ReadBits8(4) return codec } diff --git a/pkg/aac/adts.go b/pkg/aac/adts.go index 94a13ad7..6688d319 100644 --- a/pkg/aac/adts.go +++ b/pkg/aac/adts.go @@ -28,7 +28,7 @@ func ADTSToCodec(b []byte) *core.Codec { objType := rd.ReadBits8(2) + 1 // Profile, the MPEG-4 Audio Object Type minus 1 sampleRateIdx := rd.ReadBits8(4) // MPEG-4 Sampling Frequency Index _ = rd.ReadBit() // Private bit, guaranteed never to be used by MPEG, set to 0 when encoding, ignore when decoding - channels := rd.ReadBits16(3) // MPEG-4 Channel Configuration + channels := rd.ReadBits8(3) // MPEG-4 Channel Configuration //_ = rd.ReadBit() // Originality, set to 1 to signal originality of the audio and 0 otherwise //_ = rd.ReadBit() // Home, set to 1 to signal home usage of the audio and 0 otherwise @@ -43,7 +43,7 @@ func ADTSToCodec(b []byte) *core.Codec { wr := bits.NewWriter(nil) wr.WriteBits8(objType, 5) wr.WriteBits8(sampleRateIdx, 4) - wr.WriteBits16(channels, 4) + wr.WriteBits8(channels, 4) conf := wr.Bytes() codec := &core.Codec{ 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..54a7d679 --- /dev/null +++ b/pkg/alsa/capture_linux.go @@ -0,0 +1,90 @@ +package alsa + +import ( + "github.com/AlexxIT/go2rtc/pkg/alsa/device" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/pcm" + "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 +} + +func (c *Capture) Start() error { + dst := c.Medias[0].Codecs[0] + src := &core.Codec{ + Name: dst.Name, + ClockRate: c.dev.GetRateNear(dst.ClockRate), + Channels: c.dev.GetChannelsNear(dst.Channels), + } + + if err := c.dev.SetHWParams(device.SNDRV_PCM_FORMAT_S16_LE, src.ClockRate, src.Channels); err != nil { + return err + } + + transcode := transcodeFunc(dst, src) + frameBytes := int(pcm.BytesPerFrame(src)) + + var ts uint32 + + // readBufferSize for 20ms interval + readBufferSize := 20 * frameBytes * int(src.ClockRate) / 1000 + 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: transcode(b[:n]), + } + c.Receivers[0].WriteRTP(pkt) + + ts += uint32(n / frameBytes) + } +} + +func transcodeFunc(dst, src *core.Codec) func([]byte) []byte { + if dst.ClockRate == src.ClockRate && dst.Channels == src.Channels { + return func(b []byte) []byte { + return b + } + } + return pcm.Transcode(dst, src) +} 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..ecccc17b --- /dev/null +++ b/pkg/alsa/device/device_linux.go @@ -0,0 +1,231 @@ +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) CheckFormat(format byte) bool { + return d.checkMask(SNDRV_PCM_HW_PARAM_FORMAT, uint32(format)) +} + +func (d *Device) ListFormats() (formats []byte) { + for i := byte(0); i <= 28; i++ { + if d.CheckFormat(i) { + formats = append(formats, i) + } + } + return +} + +func (d *Device) RangeRates() (uint32, uint32) { + return d.getInterval(SNDRV_PCM_HW_PARAM_RATE) +} + +func (d *Device) RangeChannels() (byte, byte) { + minCh, maxCh := d.getInterval(SNDRV_PCM_HW_PARAM_CHANNELS) + return byte(minCh), byte(maxCh) +} + +func (d *Device) GetRateNear(rate uint32) uint32 { + r1, r2 := d.RangeRates() + if rate < r1 { + return r1 + } + if rate > r2 { + return r2 + } + return rate +} + +func (d *Device) GetChannelsNear(channels byte) byte { + c1, c2 := d.RangeChannels() + if channels < c1 { + return c1 + } + if channels > c2 { + return c2 + } + return channels +} + +const bufferSize = 4096 + +func (d *Device) SetHWParams(format byte, rate uint32, channels byte) error { + d.setInterval(SNDRV_PCM_HW_PARAM_CHANNELS, uint32(channels)) + d.setInterval(SNDRV_PCM_HW_PARAM_RATE, rate) + 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..2e4c57b4 --- /dev/null +++ b/pkg/alsa/open_linux.go @@ -0,0 +1,44 @@ +package alsa + +import ( + "errors" + "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 + } + + if !dev.CheckFormat(device.SNDRV_PCM_FORMAT_S16_LE) { + _ = dev.Close() + return nil, errors.New("alsa: format S16LE not supported") + } + + 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..7fb214d3 --- /dev/null +++ b/pkg/alsa/playback_linux.go @@ -0,0 +1,84 @@ +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 { + src := track.Codec + dst := &core.Codec{ + Name: core.CodecPCML, + ClockRate: p.dev.GetRateNear(src.ClockRate), + Channels: p.dev.GetChannelsNear(src.Channels), + } + sender := core.NewSender(media, dst) + + sender.Handler = func(pkt *rtp.Packet) { + if n, err := p.dev.Write(pkt.Payload); err == nil { + p.Send += n + } + } + + if sender.Handler = pcm.TranscodeHandler(dst, src, sender.Handler); sender.Handler == nil { + return fmt.Errorf("alsa: can't convert %s to %s", src, dst) + } + + // typical card support: + // - Formats: S16_LE, S32_LE + // - ClockRates: 8000 - 192000 + // - Channels: 2 - 10 + err := p.dev.SetHWParams(device.SNDRV_PCM_FORMAT_S16_LE, dst.ClockRate, byte(dst.Channels)) + 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/core/codec.go b/pkg/core/codec.go index b138df28..708839b3 100644 --- a/pkg/core/codec.go +++ b/pkg/core/codec.go @@ -13,7 +13,7 @@ import ( type Codec struct { Name string // H264, PCMU, PCMA, opus... ClockRate uint32 // 90000, 8000, 16000... - Channels uint16 // 0, 1, 2 + Channels uint8 // 0, 1, 2 FmtpLine string PayloadType uint8 } diff --git a/pkg/core/media.go b/pkg/core/media.go index a700bb62..367d8cb8 100644 --- a/pkg/core/media.go +++ b/pkg/core/media.go @@ -139,7 +139,7 @@ func MarshalSDP(name string, medias []*Media) ([]byte, error) { Protos: []string{"RTP", "AVP"}, }, } - md.WithCodec(codec.PayloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine) + md.WithCodec(codec.PayloadType, name, codec.ClockRate, uint16(codec.Channels), codec.FmtpLine) if media.Direction != "" { md.WithPropertyAttribute(media.Direction) diff --git a/pkg/homekit/helpers.go b/pkg/homekit/helpers.go index a1719671..f5a17319 100644 --- a/pkg/homekit/helpers.go +++ b/pkg/homekit/helpers.go @@ -50,7 +50,7 @@ func audioToMedia(codecs []camera.AudioCodec) *core.Media { mediaCodec := &core.Codec{ Name: audioCodecs[codec.CodecType], ClockRate: audioSampleRates[sampleRate], - Channels: uint16(param.Channels), + Channels: param.Channels, } if mediaCodec.Name == core.CodecELD { diff --git a/pkg/ioctl/README.md b/pkg/ioctl/README.md new file mode 100644 index 00000000..41f82dff --- /dev/null +++ b/pkg/ioctl/README.md @@ -0,0 +1,3 @@ +# IOCTL + +This is just an example how Linux IOCTL constants works. diff --git a/pkg/ioctl/ioctl.go b/pkg/ioctl/ioctl.go new file mode 100644 index 00000000..0f21e17f --- /dev/null +++ b/pkg/ioctl/ioctl.go @@ -0,0 +1,28 @@ +package ioctl + +import ( + "bytes" +) + +func Str(b []byte) string { + if i := bytes.IndexByte(b, 0); i >= 0 { + return string(b[:i]) + } + return string(b) +} + +func io(mode byte, type_ byte, number byte, size uint16) uintptr { + return uintptr(mode)<<30 | uintptr(size)<<16 | uintptr(type_)<<8 | uintptr(number) +} + +func IOR(type_ byte, number byte, size uint16) uintptr { + return io(read, type_, number, size) +} + +func IOW(type_ byte, number byte, size uint16) uintptr { + return io(write, type_, number, size) +} + +func IORW(type_ byte, number byte, size uint16) uintptr { + return io(read|write, type_, number, size) +} diff --git a/pkg/ioctl/ioctl_be.go b/pkg/ioctl/ioctl_be.go new file mode 100644 index 00000000..60de9c42 --- /dev/null +++ b/pkg/ioctl/ioctl_be.go @@ -0,0 +1,8 @@ +//go:build arm || arm64 || 386 || amd64 + +package ioctl + +const ( + write = 1 + read = 2 +) diff --git a/pkg/ioctl/ioctl_le.go b/pkg/ioctl/ioctl_le.go new file mode 100644 index 00000000..3bdb1f62 --- /dev/null +++ b/pkg/ioctl/ioctl_le.go @@ -0,0 +1,8 @@ +//go:build mipsle + +package ioctl + +const ( + read = 1 + write = 2 +) diff --git a/pkg/ioctl/ioctl_linux.go b/pkg/ioctl/ioctl_linux.go new file mode 100644 index 00000000..ed38f6ac --- /dev/null +++ b/pkg/ioctl/ioctl_linux.go @@ -0,0 +1,14 @@ +package ioctl + +import ( + "syscall" + "unsafe" +) + +func Ioctl(fd int, req uint, arg unsafe.Pointer) error { + _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg)) + if err != 0 { + return err + } + return nil +} diff --git a/pkg/ioctl/ioctl_test.go b/pkg/ioctl/ioctl_test.go new file mode 100644 index 00000000..52657e64 --- /dev/null +++ b/pkg/ioctl/ioctl_test.go @@ -0,0 +1,16 @@ +package ioctl + +import ( + "runtime" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIOR(t *testing.T) { + // #define SNDRV_PCM_IOCTL_INFO _IOR('A', 0x01, struct snd_pcm_info) + if runtime.GOARCH == "arm64" { + c := IOR('A', 0x01, 288) + require.Equal(t, uintptr(0x81204101), c) + } +} diff --git a/pkg/mp4/muxer.go b/pkg/mp4/muxer.go index 5533a9a3..b371f684 100644 --- a/pkg/mp4/muxer.go +++ b/pkg/mp4/muxer.go @@ -89,12 +89,12 @@ func (m *Muxer) GetInit() ([]byte, error) { } mv.WriteAudioTrack( - uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, b, + uint32(i+1), codec.Name, codec.ClockRate, uint16(codec.Channels), b, ) case core.CodecOpus, core.CodecMP3, core.CodecPCMA, core.CodecPCMU, core.CodecFLAC: mv.WriteAudioTrack( - uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, nil, + uint32(i+1), codec.Name, codec.ClockRate, uint16(codec.Channels), nil, ) } } diff --git a/pkg/pcm/handlers.go b/pkg/pcm/handlers.go new file mode 100644 index 00000000..39075199 --- /dev/null +++ b/pkg/pcm/handlers.go @@ -0,0 +1,99 @@ +package pcm + +import ( + "sync" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" +) + +// RepackG711 - Repack G.711 PCMA/PCMU into frames of size 1024 +// 1. Fixes WebRTC audio quality issue (monotonic timestamp) +// 2. Fixes Reolink Doorbell backchannel issue (zero timestamp) +// https://github.com/AlexxIT/go2rtc/issues/331 +func RepackG711(zeroTS bool, handler core.HandlerFunc) core.HandlerFunc { + const PacketSize = 1024 + + var buf []byte + var seq uint16 + var ts uint32 + + // fix https://github.com/AlexxIT/go2rtc/issues/432 + var mu sync.Mutex + + return func(packet *rtp.Packet) { + mu.Lock() + + buf = append(buf, packet.Payload...) + if len(buf) < PacketSize { + mu.Unlock() + return + } + + pkt := &rtp.Packet{ + Header: rtp.Header{ + Version: 2, + Marker: true, // should be true + PayloadType: packet.PayloadType, // will be owerwriten + SequenceNumber: seq, + SSRC: packet.SSRC, + }, + Payload: buf[:PacketSize], + } + + seq++ + + // don't know if zero TS important for Reolink Doorbell + // don't have this strange devices for tests + if !zeroTS { + pkt.Timestamp = ts + ts += PacketSize + } + + buf = buf[PacketSize:] + + mu.Unlock() + + handler(pkt) + } +} + +// LittleToBig - convert PCM little endian to PCM big endian +func LittleToBig(handler core.HandlerFunc) core.HandlerFunc { + return func(packet *rtp.Packet) { + clone := *packet + clone.Payload = FlipEndian(packet.Payload) + handler(&clone) + } +} + +func TranscodeHandler(dst, src *core.Codec, handler core.HandlerFunc) core.HandlerFunc { + var ts uint32 + k := float32(BytesPerFrame(dst)) / float32(BytesPerFrame(src)) + f := Transcode(dst, src) + + return func(packet *rtp.Packet) { + ts += uint32(k * float32(len(packet.Payload))) + + clone := *packet + clone.Payload = f(packet.Payload) + clone.Timestamp = ts + handler(&clone) + } +} + +func BytesPerFrame(codec *core.Codec) byte { + channels := byte(codec.Channels) + if channels == 0 { + channels = 1 + } + + switch codec.Name { + case core.CodecPCML, core.CodecPCM: + return 2 * channels + case core.CodecPCMU, core.CodecPCMA: + return channels + } + + return 0 +} diff --git a/pkg/pcm/pcm.go b/pkg/pcm/pcm.go index 60062b62..6872c503 100644 --- a/pkg/pcm/pcm.go +++ b/pkg/pcm/pcm.go @@ -1,200 +1,187 @@ package pcm -import ( - "sync" +import "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/pion/rtp" -) - -// ResampleToG711 - convert PCMA/PCM/PCML to PCMA and PCMU to PCMU with decreasing sample rate -func ResampleToG711(codec *core.Codec, sampleRate uint32, handler core.HandlerFunc) core.HandlerFunc { - n := float32(codec.ClockRate) / float32(sampleRate) - - if codec.Channels == 2 { - n *= 2 // hacky way for support two channels audio - } - - switch codec.Name { - case core.CodecPCMA: - return DownsampleByte(PCMAtoPCM, PCMtoPCMA, n, handler) - case core.CodecPCMU: - return DownsampleByte(PCMUtoPCM, PCMtoPCMU, n, handler) - case core.CodecPCM, core.CodecPCML: - if n == 1 { - handler = ResamplePCM(PCMtoPCMA, handler) - } else { - handler = DownsamplePCM(PCMtoPCMA, n, handler) - } - - if codec.Name == core.CodecPCML { - return LittleToBig(handler) - } - - return handler - } - - panic(core.Caller()) -} - -// DownsampleByte - convert PCMA/PCMU to PCMA/PCMU with decreasing sample rate (N times) -func DownsampleByte( - toPCM func(byte) int16, fromPCM func(int16) byte, n float32, handler core.HandlerFunc, -) core.HandlerFunc { +func Downsample(k float32) func([]int16) []int16 { var sampleN, sampleSum float32 - var ts uint32 - - return func(packet *rtp.Packet) { - samples := len(packet.Payload) - newLen := uint32((float32(samples) + sampleN) / n) - - oldSamples := packet.Payload - newSamples := make([]byte, newLen) + return func(src []int16) (dst []int16) { var i int - for _, sample := range oldSamples { - sampleSum += float32(toPCM(sample)) - if sampleN++; sampleN >= n { - newSamples[i] = fromPCM(int16(sampleSum / n)) + dst = make([]int16, int((float32(len(src))+sampleN)/k)) + for _, sample := range src { + sampleSum += float32(sample) + sampleN++ + if sampleN >= k { + dst[i] = int16(sampleSum / k) i++ sampleSum = 0 - sampleN -= n + sampleN -= k } } - - ts += newLen - - clone := *packet - clone.Payload = newSamples - clone.Timestamp = ts - handler(&clone) + return } } -// LittleToBig - conver PCM little endian to PCM big endian -func LittleToBig(handler core.HandlerFunc) core.HandlerFunc { - return func(packet *rtp.Packet) { - size := len(packet.Payload) - b := make([]byte, size) - for i := 0; i < size; i += 2 { - b[i] = packet.Payload[i+1] - b[i+1] = packet.Payload[i] - } +func Upsample(k float32) func([]int16) []int16 { + var sampleN float32 - clone := *packet - clone.Payload = b - handler(&clone) - } -} + return func(src []int16) (dst []int16) { + var i int + dst = make([]int16, int(k*float32(len(src)))) + for _, sample := range src { + sampleN += k + for sampleN > 0 { + dst[i] = sample + i++ -// ResamplePCM - convert PCM to PCMA/PCMU with same sample rate -func ResamplePCM(fromPCM func(int16) byte, handler core.HandlerFunc) core.HandlerFunc { - var ts uint32 - - return func(packet *rtp.Packet) { - len1 := len(packet.Payload) - len2 := len1 / 2 - - oldSamples := packet.Payload - newSamples := make([]byte, len2) - - var i2 int - for i1 := 0; i1 < len1; i1 += 2 { - sample := int16(uint16(oldSamples[i1])<<8 | uint16(oldSamples[i1+1])) - newSamples[i2] = fromPCM(sample) - i2++ - } - - ts += uint32(len2) - - clone := *packet - clone.Payload = newSamples - clone.Timestamp = ts - handler(&clone) - } -} - -// DownsamplePCM - convert PCM to PCMA/PCMU with decreasing sample rate (N times) -func DownsamplePCM(fromPCM func(int16) byte, n float32, handler core.HandlerFunc) core.HandlerFunc { - var sampleN, sampleSum float32 - var ts uint32 - - return func(packet *rtp.Packet) { - samples := len(packet.Payload) / 2 - newLen := uint32((float32(samples) + sampleN) / n) - - oldSamples := packet.Payload - newSamples := make([]byte, newLen) - - var i2 int - for i1 := 0; i1 < len(packet.Payload); i1 += 2 { - sampleSum += float32(int16(uint16(oldSamples[i1])<<8 | uint16(oldSamples[i1+1]))) - if sampleN++; sampleN >= n { - newSamples[i2] = fromPCM(int16(sampleSum / n)) - i2++ - - sampleSum = 0 - sampleN -= n + sampleN -= 1 } } - - ts += newLen - - clone := *packet - clone.Payload = newSamples - clone.Timestamp = ts - handler(&clone) + return } } -// RepackG711 - Repack G.711 PCMA/PCMU into frames of size 1024 -// 1. Fixes WebRTC audio quality issue (monotonic timestamp) -// 2. Fixes Reolink Doorbell backchannel issue (zero timestamp) -// https://github.com/AlexxIT/go2rtc/issues/331 -func RepackG711(zeroTS bool, handler core.HandlerFunc) core.HandlerFunc { - const PacketSize = 1024 +func FlipEndian(src []byte) (dst []byte) { + var i, j int + n := len(src) + dst = make([]byte, n) + for i < n { + x := src[i] + i++ + dst[j] = src[i] + j++ + i++ + dst[j] = x + j++ + } + return +} - var buf []byte - var seq uint16 - var ts uint32 +func Transcode(dst, src *core.Codec) func([]byte) []byte { + var reader func([]byte) []int16 + var writer func([]int16) []byte + var filters []func([]int16) []int16 - // fix https://github.com/AlexxIT/go2rtc/issues/432 - var mu sync.Mutex - - return func(packet *rtp.Packet) { - mu.Lock() - - buf = append(buf, packet.Payload...) - if len(buf) < PacketSize { - mu.Unlock() + switch src.Name { + case core.CodecPCML: + reader = func(src []byte) (dst []int16) { + var i, j int + n := len(src) + dst = make([]int16, n/2) + for i < n { + lo := src[i] + i++ + hi := src[i] + i++ + dst[j] = int16(hi)<<8 | int16(lo) + j++ + } return } - - pkt := &rtp.Packet{ - Header: rtp.Header{ - Version: 2, - Marker: true, // should be true - PayloadType: packet.PayloadType, // will be owerwriten - SequenceNumber: seq, - SSRC: packet.SSRC, - }, - Payload: buf[:PacketSize], + case core.CodecPCM: + reader = func(src []byte) (dst []int16) { + var i, j int + n := len(src) + dst = make([]int16, n/2) + for i < n { + hi := src[i] + i++ + lo := src[i] + i++ + dst[j] = int16(hi)<<8 | int16(lo) + j++ + } + return } - - seq++ - - // don't know if zero TS important for Reolink Doorbell - // don't have this strange devices for tests - if !zeroTS { - pkt.Timestamp = ts - ts += PacketSize + case core.CodecPCMU: + reader = func(src []byte) (dst []int16) { + var i int + dst = make([]int16, len(src)) + for _, sample := range src { + dst[i] = PCMUtoPCM(sample) + i++ + } + return } + case core.CodecPCMA: + reader = func(src []byte) (dst []int16) { + var i int + dst = make([]int16, len(src)) + for _, sample := range src { + dst[i] = PCMAtoPCM(sample) + i++ + } + return + } + } - buf = buf[PacketSize:] + if src.Channels > 1 { + filters = append(filters, Downsample(float32(src.Channels))) + } - mu.Unlock() + if src.ClockRate > dst.ClockRate { + filters = append(filters, Downsample(float32(src.ClockRate)/float32(dst.ClockRate))) + } else if src.ClockRate < dst.ClockRate { + filters = append(filters, Upsample(float32(dst.ClockRate)/float32(src.ClockRate))) + } - handler(pkt) + if dst.Channels > 1 { + filters = append(filters, Upsample(float32(dst.Channels))) + } + + switch dst.Name { + case core.CodecPCML: + writer = func(src []int16) (dst []byte) { + var i int + dst = make([]byte, len(src)*2) + for _, sample := range src { + dst[i] = byte(sample) + i++ + dst[i] = byte(sample >> 8) + i++ + } + return + } + case core.CodecPCM: + writer = func(src []int16) (dst []byte) { + var i int + dst = make([]byte, len(src)*2) + for _, sample := range src { + dst[i] = byte(sample >> 8) + i++ + dst[i] = byte(sample) + i++ + } + return + } + case core.CodecPCMU: + writer = func(src []int16) (dst []byte) { + var i int + dst = make([]byte, len(src)) + for _, sample := range src { + dst[i] = PCMtoPCMU(sample) + i++ + } + return + } + case core.CodecPCMA: + writer = func(src []int16) (dst []byte) { + var i int + dst = make([]byte, len(src)) + for _, sample := range src { + dst[i] = PCMtoPCMA(sample) + i++ + } + return + } + } + + return func(b []byte) []byte { + samples := reader(b) + for _, filter := range filters { + samples = filter(samples) + } + return writer(samples) } } diff --git a/pkg/pcm/pcm_test.go b/pkg/pcm/pcm_test.go new file mode 100644 index 00000000..2832be63 --- /dev/null +++ b/pkg/pcm/pcm_test.go @@ -0,0 +1,79 @@ +package pcm + +import ( + "encoding/hex" + "fmt" + "testing" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/stretchr/testify/require" +) + +func TestTranscode(t *testing.T) { + tests := []struct { + name string + src core.Codec + dst core.Codec + source string + expect string + }{ + { + name: "s16be->s16be", + src: core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1}, + dst: core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1}, + source: "FCCA00130343062808130B510D9E0F7610DA111113EA15BD16F2168215D41561", + expect: "FCCA00130343062808130B510D9E0F7610DA111113EA15BD16F2168215D41561", + }, + { + name: "s16be->s16le", + src: core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1}, + dst: core.Codec{Name: core.CodecPCML, ClockRate: 8000, Channels: 1}, + source: "FCCA00130343062808130B510D9E0F7610DA111113EA15BD16F2168215D41561", + expect: "CAFC1300430328061308510B9E0D760FDA101111EA13BD15F2168216D4156115", + }, + { + name: "s16be->mulaw", + src: core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1}, + dst: core.Codec{Name: core.CodecPCMU, ClockRate: 8000, Channels: 1}, + source: "FCCA00130343062808130B510D9E0F7610DA111113EA15BD16F2168215D41561", + expect: "52FDD1C5BEB8B3B0AEAEABA9A8A8A9AA", + }, + { + name: "s16be->alaw", + src: core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1}, + dst: core.Codec{Name: core.CodecPCMA, ClockRate: 8000, Channels: 1}, + source: "FCCA00130343062808130B510D9E0F7610DA111113EA15BD16F2168215D41561", + expect: "7CD4FFED95939E9B8584868083838080", + }, + { + name: "2ch->1ch", + src: core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 2}, + dst: core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1}, + source: "FCCAFCCA001300130343034306280628081308130B510B510D9E0D9E0F760F76", + expect: "FCCA00130343062808130B510D9E0F76", + }, + { + name: "1ch->2ch", + src: core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1}, + dst: core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 2}, + source: "FCCA00130343062808130B510D9E0F76", + expect: "FCCAFCCA001300130343034306280628081308130B510B510D9E0D9E0F760F76", + }, + { + name: "16khz->8khz", + src: core.Codec{Name: core.CodecPCM, ClockRate: 16000, Channels: 1}, + dst: core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1}, + source: "FCCAFCCA001300130343034306280628081308130B510B510D9E0D9E0F760F76", + expect: "FCCA00130343062808130B510D9E0F76", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f := Transcode(&test.dst, &test.src) + b, _ := hex.DecodeString(test.source) + b = f(b) + s := fmt.Sprintf("%X", b) + require.Equal(t, test.expect, s) + }) + } +} diff --git a/pkg/wav/producer.go b/pkg/wav/producer.go index 63f6d01a..b9b3a878 100644 --- a/pkg/wav/producer.go +++ b/pkg/wav/producer.go @@ -45,7 +45,7 @@ func Open(r io.Reader) (*Producer, error) { codec.Name = core.CodecPCMU } - codec.Channels = uint16(data[2]) + codec.Channels = data[2] codec.ClockRate = binary.LittleEndian.Uint32(data[4:]) } } diff --git a/pkg/webrtc/consumer.go b/pkg/webrtc/consumer.go index e9d7b2e5..ebc3a008 100644 --- a/pkg/webrtc/consumer.go +++ b/pkg/webrtc/consumer.go @@ -73,7 +73,7 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv codec.Name = core.CodecPCMA } codec.ClockRate = 8000 - sender.Handler = pcm.ResampleToG711(track.Codec, 8000, sender.Handler) + sender.Handler = pcm.TranscodeHandler(codec, track.Codec, sender.Handler) } } diff --git a/pkg/webrtc/producer.go b/pkg/webrtc/producer.go index 822b1644..32e958ee 100644 --- a/pkg/webrtc/producer.go +++ b/pkg/webrtc/producer.go @@ -21,7 +21,7 @@ func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, e RTPCodecCapability: webrtc.RTPCodecCapability{ MimeType: MimeType(codec), ClockRate: codec.ClockRate, - Channels: codec.Channels, + Channels: uint16(codec.Channels), }, PayloadType: 0, // don't know if this necessary } 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 @@ - +