Add audio handlers

This commit is contained in:
Dmitrii Okunev
2024-10-23 22:00:51 +01:00
parent 4a7d75c807
commit c68a2d04f7
13 changed files with 364 additions and 2 deletions

7
go.mod
View File

@@ -19,6 +19,8 @@ replace github.com/pion/ice/v2 => github.com/aler9/ice/v2 v2.0.0-20241006110309-
replace github.com/pion/webrtc/v3 => github.com/aler9/webrtc/v3 v3.0.0-20240610104456-eaec24056d06
replace github.com/jfreymuth/pulse v0.1.1 => github.com/xaionaro-go/pulse v0.0.0-20241023202712-7151fa00d4bb
require (
github.com/facebookincubator/go-belt v0.0.0-20240804203001-846c4409d41c
github.com/go-git/go-billy/v5 v5.5.0
@@ -64,6 +66,7 @@ require (
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/datarhei/gosrt v0.7.0 // indirect
github.com/dsnet/compress v0.0.1 // indirect
github.com/ebitengine/purego v0.8.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
@@ -110,6 +113,7 @@ require (
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49 // indirect
github.com/jezek/xgb v1.1.0 // indirect
github.com/jfreymuth/vorbis v1.0.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
@@ -198,6 +202,7 @@ require (
github.com/chai2010/webp v1.1.1
github.com/davecgh/go-spew v1.1.1
github.com/dustin/go-humanize v1.0.1
github.com/ebitengine/oto/v3 v3.3.1
github.com/getsentry/sentry-go v0.28.1
github.com/go-git/go-git/v5 v5.12.0
github.com/go-ng/xmath v0.0.0-20230704233441-028f5ea62335
@@ -206,6 +211,8 @@ require (
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
github.com/iancoleman/strcase v0.3.0
github.com/immune-gmbh/attestation-sdk v0.0.0-20230711173209-f44e4502aeca
github.com/jfreymuth/oggvorbis v1.0.5
github.com/jfreymuth/pulse v0.1.1
github.com/kbinani/screenshot v0.0.0-20230812210009-b87d31814237
github.com/lusingander/colorpicker v0.7.3
github.com/pkg/errors v0.9.1

12
go.sum
View File

@@ -176,6 +176,10 @@ github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5Jflh
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/oto/v3 v3.3.1 h1:d4McwGQuXOT0GL7bA5g9ZnaUEIEjQvG3hafzMy+T3qE=
github.com/ebitengine/oto/v3 v3.3.1/go.mod h1:MZeb/lwoC4DCOdiTIxYezrURTw7EvK/yF863+tmBI+U=
github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE=
github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
@@ -424,6 +428,10 @@ github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49 h1:Po+wkNdMmN
github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49/go.mod h1:YiutDnxPRLk5DLUFj6Rw4pRBBURZY07GFr54NdV9mQg=
github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk=
github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/jfreymuth/oggvorbis v1.0.5 h1:u+Ck+R0eLSRhgq8WTmffYnrVtSztJcYrl588DM4e3kQ=
github.com/jfreymuth/oggvorbis v1.0.5/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII=
github.com/jfreymuth/vorbis v1.0.2 h1:m1xH6+ZI4thH927pgKD8JOH4eaGRm18rEE9/0WKjvNE=
github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
@@ -659,8 +667,6 @@ github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/xaionaro-go/datacounter v1.0.4 h1:+QMZLmu73R5WGkQfUPwlXF/JFN+Weo4iuDZkiL2wVm8=
github.com/xaionaro-go/datacounter v1.0.4/go.mod h1:Sf9vBevuV6w5iE6K3qJ9pWVKcyS60clWBUSQLjt5++c=
github.com/xaionaro-go/deepcopy v0.0.0-20241022170035-e7567ec09853 h1:i8myxLpqtNsz0pXpaZhBSkLjeV5+qlgXiEEzOl8eCsk=
github.com/xaionaro-go/deepcopy v0.0.0-20241022170035-e7567ec09853/go.mod h1:FdcXb6OQaB+zrL4xtd+AW85iDYnAuZ7iP2qDGFTX6ok=
github.com/xaionaro-go/deepcopy v0.0.0-20241022201332-e9025712e972 h1:0RuOVRrOFvEvCQe95VGarA9yDgHoz2pyU22F3XYmdC8=
github.com/xaionaro-go/deepcopy v0.0.0-20241022201332-e9025712e972/go.mod h1:FdcXb6OQaB+zrL4xtd+AW85iDYnAuZ7iP2qDGFTX6ok=
github.com/xaionaro-go/fyne/v2 v2.0.0-20241020235352-fd61e4920f24 h1:eewdCRMkJmK2ipI9653XL2dE3EFS2I3GTFRadIyyEo4=
@@ -683,6 +689,8 @@ github.com/xaionaro-go/mediamtx v0.0.0-20241009124606-94c22c603970 h1:QmbvVR2Jt+
github.com/xaionaro-go/mediamtx v0.0.0-20241009124606-94c22c603970/go.mod h1:3J9s+wGt6CV4MDnoXApKEdY3kdc5sd6AYEndLJSAIYI=
github.com/xaionaro-go/obs-grpc-proxy v0.0.0-20241018162120-5faf4e7a684a h1:PyX7XpLkj+eAwrPMFMGpvZIG4zBfzAfwNhwTtbORqN0=
github.com/xaionaro-go/obs-grpc-proxy v0.0.0-20241018162120-5faf4e7a684a/go.mod h1:exSKIlCibB0ww+ABDwH+YG/iNdqVfdzXBBg5LYxkxGw=
github.com/xaionaro-go/pulse v0.0.0-20241023202712-7151fa00d4bb h1:9iHPI27CYbmJDhzEuCABQthE/DGVNvT60ybWvv3BV8w=
github.com/xaionaro-go/pulse v0.0.0-20241023202712-7151fa00d4bb/go.mod h1:cpYspI6YljhkUf1WLXLLDmeaaPFc3CnGLjDZf9dZ4no=
github.com/xaionaro-go/spinlock v0.0.0-20190309154744-55278e21e817/go.mod h1:Nb/15eS0BMty6TMuWgRQM8WCDIUlyPZagcpchHT6c9Y=
github.com/xaionaro-go/spinlock v0.0.0-20200518175509-30e6d1ce68a1 h1:1Kqw9dv2LnznIhJoMt3dNzc/ctSj6VHjyGh4YZHjpE4=
github.com/xaionaro-go/spinlock v0.0.0-20200518175509-30e6d1ce68a1/go.mod h1:UwmTXX+EpoEYHuy0rSys1Rp5PW+eVTgZSjgMVLJENKg=

59
pkg/audio/audio.go Normal file
View File

@@ -0,0 +1,59 @@
package audio
import (
"fmt"
"io"
"time"
"github.com/jfreymuth/oggvorbis"
)
const BufferSize = 200 * time.Millisecond
type Audio struct {
PlayerPCM
}
func NewAudio(playerPCM PlayerPCM) *Audio {
return &Audio{
PlayerPCM: playerPCM,
}
}
func NewAudioAuto() *Audio {
for _, factory := range []func() PlayerPCM{
NewPlayerPulse,
NewPlayerOto,
} {
player := factory()
if player.Ping() == nil {
return &Audio{
PlayerPCM: player,
}
}
}
// the default backend:
return &Audio{
PlayerPCM: NewPlayerOto(),
}
}
func (a *Audio) PlayVorbis(rawReader io.Reader) error {
oggReader, err := oggvorbis.NewReader(rawReader)
if err != nil {
return fmt.Errorf("unable to initialize a vorbis reader: %w", err)
}
err = a.PlayerPCM.PlayPCM(
uint32(oggReader.SampleRate()),
uint16(oggReader.Channels()),
PCMFormatFloat32LE,
BufferSize,
newReaderFromFloat32Reader(oggReader),
)
if err != nil {
return fmt.Errorf("unable to playback as PCM: %w", err)
}
return nil
}

30
pkg/audio/audio_unsafe.go Normal file
View File

@@ -0,0 +1,30 @@
package audio
import (
"io"
"unsafe"
)
type float32Reader interface {
Read([]float32) (int, error)
}
type readerFromFloat32Reader struct {
float32Reader
}
var _ io.Reader = (*readerFromFloat32Reader)(nil)
func newReaderFromFloat32Reader(r float32Reader) readerFromFloat32Reader {
return readerFromFloat32Reader{
float32Reader: r,
}
}
func (r readerFromFloat32Reader) Read(b []byte) (int, error) {
ptr := unsafe.SliceData(b)
f := unsafe.Slice((*float32)(unsafe.Pointer(ptr)), len(b)/4)
n, err := r.float32Reader.Read(f)
n *= int(unsafe.Sizeof(float32(0)))
return n, err
}

View File

@@ -0,0 +1,17 @@
package main
import (
"bytes"
"github.com/xaionaro-go/streamctl/pkg/audio"
"github.com/xaionaro-go/streamctl/pkg/audiotheme/defaultaudiotheme"
)
func main() {
audiotheme := defaultaudiotheme.AudioTheme()
a := audio.NewAudioAuto()
err := a.PlayVorbis(bytes.NewReader(audiotheme.ChatMessage))
if err != nil {
panic(err)
}
}

36
pkg/audio/player.go Normal file
View File

@@ -0,0 +1,36 @@
package audio
import (
"fmt"
"io"
"time"
)
type PlayerPCM interface {
Ping() error
PlayPCM(
sampleRate uint32,
channels uint16,
format PCMFormat,
bufferSize time.Duration,
reader io.Reader,
) error
}
type PCMFormat uint
const (
PCMFormatUndefined = PCMFormat(iota)
PCMFormatFloat32LE
)
func (f PCMFormat) String() string {
switch f {
case PCMFormatUndefined:
return "<undefined>"
case PCMFormatFloat32LE:
return "f32le"
default:
return fmt.Sprintf("<unexpected_value_%d>", f)
}
}

56
pkg/audio/player_oto.go Normal file
View File

@@ -0,0 +1,56 @@
package audio
import (
"fmt"
"io"
"time"
"github.com/ebitengine/oto/v3"
)
type PlayerOto struct {
}
var _ PlayerPCM = (*PlayerOto)(nil)
func NewPlayerOto() PlayerPCM {
return PlayerOto{}
}
func (PlayerOto) Ping() error {
return fmt.Errorf("not implemented: do not know how to do that, yet")
}
func (PlayerOto) PlayPCM(
sampleRate uint32,
channels uint16,
format PCMFormat,
bufferSize time.Duration,
reader io.Reader,
) error {
op := &oto.NewContextOptions{
SampleRate: int(sampleRate),
ChannelCount: int(channels),
Format: oto.FormatFloat32LE,
BufferSize: bufferSize,
}
otoCtx, readyChan, err := oto.NewContext(op)
if err != nil {
return fmt.Errorf("unable to initialize an oto context: %w", err)
}
<-readyChan
player := otoCtx.NewPlayer(reader)
player.Play()
for player.IsPlaying() {
time.Sleep(100 * time.Millisecond)
}
err = player.Close()
if err != nil {
return fmt.Errorf("unable to close the player: %w", err)
}
return nil
}

89
pkg/audio/player_pulse.go Normal file
View File

@@ -0,0 +1,89 @@
package audio
import (
"fmt"
"io"
"time"
"github.com/jfreymuth/pulse"
"github.com/jfreymuth/pulse/proto"
)
type PlayerPulse struct {
}
var _ PlayerPCM = (*PlayerPulse)(nil)
func NewPlayerPulse() PlayerPCM {
return PlayerPulse{}
}
func (PlayerPulse) Ping() error {
c, err := pulse.NewClient()
if err != nil {
return fmt.Errorf("unable to open a client to Pulse: %w", err)
}
defer c.Close()
return nil
}
func (PlayerPulse) PlayPCM(
sampleRate uint32,
channels uint16,
format PCMFormat,
bufferSize time.Duration,
rawReader io.Reader,
) error {
reader, err := newPulseReader(format, rawReader)
if err != nil {
return fmt.Errorf("unable to initialize a reader for Pulse: %w", err)
}
c, err := pulse.NewClient()
if err != nil {
return fmt.Errorf("unable to open a client to Pulse: %w", err)
}
defer c.Close()
stream, err := c.NewPlayback(reader, pulse.PlaybackLatency(bufferSize.Seconds()))
if err != nil {
return fmt.Errorf("unable to initialize a playback: %w", err)
}
stream.Start()
stream.Drain()
if stream.Error() != nil {
return fmt.Errorf("an error occurred during playback: %w", stream.Error())
}
if stream.Underflow() {
return fmt.Errorf("underflow")
}
stream.Close()
return nil
}
type pulseReader struct {
pulseFormat byte
io.Reader
}
func newPulseReader(pcmFormat PCMFormat, reader io.Reader) (*pulseReader, error) {
var pulseFormat byte
switch pcmFormat {
case PCMFormatFloat32LE:
pulseFormat = proto.FormatInt32LE
default:
return nil, fmt.Errorf("received an unexpected format: %v", pcmFormat)
}
return &pulseReader{
pulseFormat: pulseFormat,
Reader: reader,
}, nil
}
var _ pulse.Reader = (*pulseReader)(nil)
func (r pulseReader) Format() byte {
return r.pulseFormat
}

View File

@@ -0,0 +1,7 @@
package audiotheme
type VorbisFile []byte
type AudioTheme struct {
ChatMessage VorbisFile
}

View File

@@ -0,0 +1,19 @@
package defaultaudiotheme
import (
_ "embed"
"slices"
"github.com/xaionaro-go/streamctl/pkg/audiotheme"
)
var (
//go:embed resources/chat_message.ogg
chatMessage []byte
)
func AudioTheme() audiotheme.AudioTheme {
return audiotheme.AudioTheme{
ChatMessage: slices.Clone(chatMessage),
}
}

View File

@@ -0,0 +1,24 @@
package audio
import (
"bytes"
audioSubsystem "github.com/xaionaro-go/streamctl/pkg/audio"
"github.com/xaionaro-go/streamctl/pkg/audiotheme"
"github.com/xaionaro-go/streamctl/pkg/audiotheme/defaultaudiotheme"
)
type Audio struct {
playbacker audioSubsystem.Audio
audioTheme audiotheme.AudioTheme
}
func NewAudio() *Audio {
return &Audio{
audioTheme: defaultaudiotheme.AudioTheme(),
}
}
func (a *Audio) PlayChatMessage() error {
return a.playbacker.PlayVorbis(bytes.NewReader(a.audioTheme.ChatMessage))
}

View File

@@ -0,0 +1,10 @@
package main
import (
"github.com/xaionaro-go/streamctl/pkg/streampanel/audio"
)
func main() {
a := audio.NewAudio()
a.PlayChatMessage()
}