From bc4cea580b248b8111422af957d436f1510e1c76 Mon Sep 17 00:00:00 2001 From: minie4 Date: Sat, 24 Feb 2024 23:03:18 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20Implement=20working=20Audio=20Mi?= =?UTF-8?q?xer=20backend=20with=20Jack=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + api.go | 297 +++++++++++++++++++++++++++++++++++++ config.go | 72 +++++++++ go.mod | 36 +++++ go.sum | 90 +++++++++++ jack.go | 62 ++++++++ main.go | 34 +++++ routing.go | 198 +++++++++++++++++++++++++ sample_configs/config.toml | 7 + sample_configs/ports.toml | 0 10 files changed, 799 insertions(+) create mode 100644 .gitignore create mode 100644 api.go create mode 100644 config.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 jack.go create mode 100644 main.go create mode 100644 routing.go create mode 100644 sample_configs/config.toml create mode 100644 sample_configs/ports.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..42692a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +data/ +go-mix +apidoc/environments/ \ No newline at end of file diff --git a/api.go b/api.go new file mode 100644 index 0000000..3d3ff15 --- /dev/null +++ b/api.go @@ -0,0 +1,297 @@ +package main + +import ( + "fmt" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +func initApi() { + gin.SetMode(gin.ReleaseMode) + router := gin.Default() + router.SetTrustedProxies(nil) + + router.GET("/api/ports", func(c *gin.Context) { + c.JSON(200, portConfig) + }) + router.GET("/api/port", handleGetPort) + router.POST("/api/ports", handleCreatePort) + router.DELETE("/api/ports", handleDeletePort) + + router.GET("/api/state", handleGetState) + router.PUT("/api/state", handleSetState) + + router.POST("/api/routes", handleCreateRoute) + router.PUT("/api/routes", handleSetRoute) + router.DELETE("/api/routes", handleDeleteRoute) + + router.Run(fmt.Sprintf("%s:%d", config.Web.Bind, config.Web.Port)) +} + +func handleGetPort(c *gin.Context) { + id := c.Query("UUID") + if id == "" { + c.JSON(400, gin.H{"success": false, "error": "`UUID` parameter missing!"}) + return + } + + port, portIndex, _ := findPort(id) + if portIndex < 0 { + c.JSON(400, gin.H{"success": false, "error": "Port with provided UUID does not exist!"}) + return + } + + c.JSON(200, port) +} + +type CreatePortRequest struct { + Name string + Backend string + Channels uint8 + Type string +} + +func handleCreatePort(c *gin.Context) { + var body CreatePortRequest + if err := c.BindJSON(&body); err != nil { + c.JSON(500, gin.H{"success": false, "error": "Parsing request body failed!"}) + return + } + if body.Name == "" || body.Backend == "" || body.Channels == 0 || body.Type == "" { + c.JSON(400, gin.H{"success": false, "error": "Required parameters missing!"}) + return + } + if body.Type != "input" && body.Type != "output" { + c.JSON(400, gin.H{"success": false, "error": "`Type` must either be `input` or `output`"}) + return + } + + id := uuid.New() + newPort := Port{ + UUID: id.String(), + Name: body.Name, + Properties: PortProperties{ + Backend: body.Backend, + Channels: body.Channels, + }, + State: PortState{ + Mute: false, + Volume: 0, + Balance: 0, + }, + Route: []PortRoute{}, + } + + if body.Type == "input" { + registerPort(&newPort, "input") + portConfig.Input = append(portConfig.Input, newPort) + } else { + registerPort(&newPort, "output") + portConfig.Output = append(portConfig.Output, newPort) + } + saveConfig("ports", &portConfig) + + c.JSON(200, gin.H{"success": true, "UUID": newPort.UUID}) +} + +type DeletePortRequest struct { + UUID string +} + +func handleDeletePort(c *gin.Context) { + var body DeletePortRequest + if err := c.BindJSON(&body); err != nil { + c.JSON(500, gin.H{"success": false, "error": "Parsing request body failed!"}) + return + } + if body.UUID == "" { + c.JSON(400, gin.H{"success": false, "error": "Required parameters missing!"}) + return + } + + port, portIndex, portType := findPort(body.UUID) + if portIndex < 0 { + c.JSON(400, gin.H{"success": false, "error": "Port with provided UUID does not exist!"}) + return + } + + // Remove the port from the configuration + if portType == "output" { + portConfig.Output = append(portConfig.Output[:portIndex], portConfig.Output[portIndex+1:]...) + } else { + portConfig.Input = append(portConfig.Input[:portIndex], portConfig.Input[portIndex+1:]...) + } + + // Unregister port + unregisterPort(port) + + saveConfig("ports", &portConfig) +} + +func handleGetState(c *gin.Context) { + id := c.Query("UUID") + if id == "" { + c.JSON(400, gin.H{"success": false, "error": "`UUID` parameter missing!"}) + return + } + + port, portIndex, _ := findPort(id) + if portIndex < 0 { + c.JSON(400, gin.H{"success": false, "error": "Port with provided UUID does not exist!"}) + return + } + + c.JSON(200, port.State) +} + +type SetPortRequest struct { + Mute *bool + Volume *float32 + Balance *float32 +} + +func handleSetState(c *gin.Context) { + var body SetPortRequest + err := c.BindJSON(&body) + if err != nil { + c.JSON(500, gin.H{"success": false, "error": "Parsing request body failed!"}) + return + } + + id := c.Query("UUID") + if id == "" { + c.JSON(400, gin.H{"success": false, "error": "`UUID` parameter missing!"}) + return + } + + port, portIndex, _ := findPort(id) + if portIndex < 0 { + c.JSON(400, gin.H{"success": false, "error": "Port with provided UUID does not exist!"}) + return + } + + if body.Mute != nil { + port.State.Mute = *body.Mute + } + if body.Volume != nil { + port.State.Volume = *body.Volume + } + if body.Balance != nil { + port.State.Balance = *body.Balance + } + + c.JSON(200, gin.H{"success": true, "state": port.State}) + saveConfig("ports", &portConfig) +} + +type CreateRouteRequest struct { + To string +} + +func handleCreateRoute(c *gin.Context) { + var body CreateRouteRequest + err := c.BindJSON(&body) + if err != nil { + c.JSON(500, gin.H{"success": false, "error": "Parsing request body failed!"}) + return + } + + id := c.Query("UUID") + if id == "" || body.To == "" { + c.JSON(400, gin.H{"success": false, "error": "Required parameters missing!"}) + return + } + + from, fromIndex, fromType := findPort(id) + _, toIndex, toType := findPort(body.To) + if fromIndex < 0 || toIndex < 0 { + c.JSON(400, gin.H{"success": false, "error": "One of `UUID` or `To` does not exist!"}) + return + } + if fromType != "input" || toType != "output" { + c.JSON(400, gin.H{"success": false, "error": "`UUID` needs to be an input and `To` an output port!"}) + return + } + + from.Route = append(from.Route, PortRoute{ToUUID: body.To, Mute: true, Volume: 0, Balance: 0}) + saveConfig("ports", &portConfig) + + c.JSON(200, gin.H{"success": true}) +} + +type SetRouteRequest struct { + Mute *bool + Volume *float32 + Balance *float32 +} + +func handleSetRoute(c *gin.Context) { + var body SetRouteRequest + err := c.BindJSON(&body) + if err != nil { + c.JSON(500, gin.H{"success": false, "error": "Parsing request body failed!"}) + return + } + + fromId := c.Query("UUID") + toId := c.Query("To") + if fromId == "" || toId == "" { + c.JSON(400, gin.H{"success": false, "error": "Required parameters missing!"}) + return + } + + from, fromIndex, _ := findPort(fromId) + if fromIndex < 0 { + c.JSON(400, gin.H{"success": false, "error": "Port with provided UUID does not exist!"}) + return + } + for i, r := range from.Route { + if r.ToUUID != toId { + continue + } + + if body.Mute != nil { + from.Route[i].Mute = *body.Mute + } + if body.Volume != nil { + from.Route[i].Volume = *body.Volume + } + if body.Balance != nil { + from.Route[i].Balance = *body.Balance + } + + saveConfig("ports", &portConfig) + c.JSON(200, gin.H{"success": true}) + return + } + + c.JSON(200, gin.H{"success": false, "error": "No such route exists!"}) +} + +func handleDeleteRoute(c *gin.Context) { + fromId := c.Query("UUID") + toId := c.Query("To") + if fromId == "" || toId == "" { + c.JSON(400, gin.H{"success": false, "error": "Required parameters missing!"}) + return + } + + from, fromIndex, _ := findPort(fromId) + if fromIndex < 0 { + c.JSON(400, gin.H{"success": false, "error": "Port with provided UUID does not exist!"}) + return + } + for i, r := range from.Route { + if r.ToUUID != toId { + continue + } + + from.Route = append(from.Route[:i], from.Route[i+1:]...) + saveConfig("ports", &portConfig) + c.JSON(200, gin.H{"success": true}) + return + } + + c.JSON(200, gin.H{"success": false, "error": "No such route exists!"}) +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..0fd6e47 --- /dev/null +++ b/config.go @@ -0,0 +1,72 @@ +package main + +import ( + "embed" + "errors" + "fmt" + "os" + + "github.com/pelletier/go-toml/v2" +) + +const CONFIG_DIR = "./data" + +//go:embed sample_configs/* +var sampleConfig embed.FS + +type Config struct { + Web struct { + Bind string `toml:"bind"` + Port int `toml:"port"` + } `toml:"web"` + Jack struct { + Enabled bool `toml:"enabled"` + ClientName string `toml:"client_name"` + } `toml:"jack"` +} + +var config Config + +// Copy default config if `{name}.toml` file does not exist +func createDefaultConfig(name string) { + dest_path := fmt.Sprintf("%s/%s.toml", CONFIG_DIR, name) + source_path := fmt.Sprintf("sample_configs/%s.toml", name) + + if _, err := os.Stat(dest_path); errors.Is(err, os.ErrNotExist) { + content, err := sampleConfig.ReadFile(source_path) + if err != nil { + fmt.Printf("Warning! Default config for `%s` does not exist!\n", name) + return + } + os.WriteFile(dest_path, content, 0644) + } +} + +// Load config from `.toml` file into a struct +func loadConfig(name string, dest interface{}) { + createDefaultConfig(name) + + file, err := os.ReadFile(fmt.Sprintf("%s/%s.toml", CONFIG_DIR, name)) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + err = toml.Unmarshal(file, dest) + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +// Save struct as a `.toml` file +func saveConfig(name string, dest interface{}) { + bin, err := toml.Marshal(dest) + if err != nil { + fmt.Println(err) + } + err = os.WriteFile(fmt.Sprintf("%s/%s.toml", CONFIG_DIR, name), bin, 0644) + if err != nil { + fmt.Println(err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6692187 --- /dev/null +++ b/go.mod @@ -0,0 +1,36 @@ +module minie4.de/go-mix + +go 1.21 + +require ( + github.com/gin-gonic/gin v1.9.1 + github.com/google/uuid v1.6.0 + github.com/pelletier/go-toml/v2 v2.1.1 + github.com/xthexder/go-jack v0.0.0-20220805234212-bc8604043aba +) + +require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7e653b1 --- /dev/null +++ b/go.sum @@ -0,0 +1,90 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/xthexder/go-jack v0.0.0-20220805234212-bc8604043aba h1:QighQ8fJJOqipXXurg9WghoImtvl7CHTpe21GDYdIkk= +github.com/xthexder/go-jack v0.0.0-20220805234212-bc8604043aba/go.mod h1:T6DswVPJzBW/Xg64l/gohXVgSW81GwXyMws1fkqxlUg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/jack.go b/jack.go new file mode 100644 index 0000000..97ee06c --- /dev/null +++ b/jack.go @@ -0,0 +1,62 @@ +package main + +import ( + "fmt" + "os" + + "github.com/xthexder/go-jack" +) + +var jackClient *jack.Client + +func initJack(done chan os.Signal) { + jackClient, _ = jack.ClientOpen(config.Jack.ClientName, jack.NoStartServer) + if jackClient == nil { + fmt.Println("Could not connect to jack server.") + return + } + + if code := jackClient.SetProcessCallback(processJackCb); code != 0 { + fmt.Println("Failed to set process callback.") + return + } + + if code := jackClient.Activate(); code != 0 { + fmt.Println("Failed to activate client:", jack.StrError(code)) + return + } +} + +func stopJack() { + jackClient.Close() +} + +func registerJackPort(name string, channels uint8, flags uint64) []*jack.Port { + if channels > 2 { + fmt.Printf("WARN! Parameters of jack port %s changed: more than 2 channels not implemented, using 2 channels", name) + channels = 2 + } + + var i uint8 = 0 + var ports []*jack.Port = []*jack.Port{} + for i < channels { + i++ + portName := fmt.Sprintf("%s_%d", name, i) + port := jackClient.PortRegister(portName, jack.DEFAULT_AUDIO_TYPE, flags, 0) + if port == nil { + fmt.Printf("WARN! Failed registering jack port %s\n", portName) + continue + } + ports = append(ports, port) + } + return ports +} + +func unregisterJackPort(ports []*jack.Port) { + for _, p := range ports { + ret := jackClient.PortUnregister(p) + if ret != 0 { + fmt.Printf("WARN! Failed unregistering jack port!\n") + } + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..6e76c46 --- /dev/null +++ b/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "os" + "os/signal" + "syscall" +) + +func main() { + // Init exit channel + end := make(chan os.Signal, 1) + signal.Notify(end, syscall.SIGINT, syscall.SIGTERM) + + // Load config.toml file + loadConfig("config", &config) + + // Initialize REST API + go initApi() + + // Initialize enabled audio backends + if config.Jack.Enabled { + initJack(end) + } + + // Start audio routing + startRouting() + + // Wait until SIGINT received + <-end + + if config.Jack.Enabled { + stopJack() + } +} diff --git a/routing.go b/routing.go new file mode 100644 index 0000000..183e6d2 --- /dev/null +++ b/routing.go @@ -0,0 +1,198 @@ +package main + +import ( + "fmt" + + "github.com/xthexder/go-jack" +) + +type PortState struct { + Mute bool + Volume float32 + Balance float32 +} + +type PortProperties struct { + Backend string + Channels uint8 +} + +type PortRoute struct { + ToUUID string + Mute bool + Volume float32 + Balance float32 +} + +type Port struct { + UUID string + Name string + Properties PortProperties + State PortState + Route []PortRoute + object []*jack.Port +} + +var portConfig struct { + Input []Port + Output []Port +} + +var portsInited bool = false + +// Find index and pointer of port +func findPort(findUUID string) (*Port, int, string) { + var port *Port + var portIndex int = -1 + var portType string + for i, p := range portConfig.Input { + if p.UUID == findUUID { + port = &portConfig.Input[i] + portIndex = i + portType = "input" + } + } + for i, p := range portConfig.Output { + if p.UUID == findUUID { + port = &portConfig.Output[i] + portIndex = i + portType = "output" + } + } + + return port, portIndex, portType +} + +func registerPort(port *Port, portType string) { + switch backend := port.Properties.Backend; backend { + case "jack": + if !config.Jack.Enabled { + return + } + var jackFlag int + if portType == "input" { + jackFlag = jack.PortIsInput + } else if portType == "output" { + jackFlag = jack.PortIsOutput + } else { + fmt.Printf("WARN! Trying to register port with invalid type `%s`\n", portType) + return + } + port.object = registerJackPort(port.Name, port.Properties.Channels, uint64(jackFlag)) + default: + fmt.Printf("WARN! Error loading port %s: Backend %s is not implemented!", port.UUID, backend) + } +} + +func unregisterPort(port *Port) { + switch backend := port.Properties.Backend; backend { + case "jack": + if !config.Jack.Enabled { + return + } + unregisterJackPort(port.object) + port.object = nil + default: + fmt.Printf("WARN! Error unloading port %s: Backend %s is not implemented!", port.UUID, backend) + } +} + +func startRouting() { + // Load port configuration from config file + loadConfig("ports", &portConfig) + + // Register ports + for i := range portConfig.Input { + registerPort(&portConfig.Input[i], "input") + } + for i := range portConfig.Output { + registerPort(&portConfig.Output[i], "output") + } + + portsInited = true +} + +type JackOutputBuffers struct { + UUID string + Buffers [][]jack.AudioSample +} + +func addToBuffer(src *[]jack.AudioSample, dst *[]jack.AudioSample, volume float32) { + for i, sample := range *src { + (*dst)[i] += sample * jack.AudioSample(volume) + } +} + +func calcBalanceFactor(ch uint8, balance float32) float32 { + if ch == 0 { + return max(0, min(1, 1-balance)) + } else { + return max(0, min(1, 1+balance)) + } +} + +func processJackCb(nframes uint32) int { + if !portsInited { + return 0 + } + + // Get memory addresses to write to for all outputs + var outputBuffers []JackOutputBuffers = []JackOutputBuffers{} + for _, out := range portConfig.Output { + outputBuffer := JackOutputBuffers{out.UUID, [][]jack.AudioSample{}} + for _, port := range out.object { + buffer := port.GetBuffer(nframes) + for i := range buffer { + buffer[i] = 0 + } + outputBuffer.Buffers = append(outputBuffer.Buffers, buffer) + } + outputBuffers = append(outputBuffers, outputBuffer) + } + + // Write audio data from every input to all outputs it routes to + for _, in := range portConfig.Input { + if in.State.Mute { + continue + } + + // For every channel + for ch := uint8(0); ch < in.Properties.Channels; ch++ { + if in.object == nil || uint8(len(in.object)) <= ch { + // Input is not initialized + continue + } + chSamples := in.object[ch].GetBuffer(nframes) + + // For every route that exists for the input + for _, route := range in.Route { + for _, out := range outputBuffers { + if out.UUID != route.ToUUID { + continue + } + + if out.Buffers == nil || uint8(len(out.Buffers)) <= ch { + // Output is not initialized + continue + } + + routeVolume := in.State.Volume * route.Volume * calcBalanceFactor(ch, in.State.Balance) + + if in.Properties.Channels < uint8(len(out.Buffers)) { + // If input is mono and output is stereo, write to both output channels at once + for i, buf := range out.Buffers { + finalVolume := routeVolume * calcBalanceFactor(uint8(i), route.Balance) + addToBuffer(&chSamples, &buf, finalVolume) + } + } else { + // Else only write to current channel (or channel 0 if output is mono) + finalVolume := routeVolume * calcBalanceFactor(ch, route.Balance) + index := min(uint8(len(out.Buffers)-1), ch) + addToBuffer(&chSamples, &out.Buffers[index], finalVolume) + } + } + } + } + } + return 0 +} diff --git a/sample_configs/config.toml b/sample_configs/config.toml new file mode 100644 index 0000000..279f9ae --- /dev/null +++ b/sample_configs/config.toml @@ -0,0 +1,7 @@ +[web] +bind = "0.0.0.0" +port = 8080 + +[jack] +enabled = true +client_name = "go-mix Jack" \ No newline at end of file diff --git a/sample_configs/ports.toml b/sample_configs/ports.toml new file mode 100644 index 0000000..e69de29