- add documentation

- add more info to README.MD
- splited into multiple files
This commit is contained in:
Sören Oesterwind 2022-05-16 20:43:33 +02:00
parent d8155d86e0
commit 951f61fbd4
7 changed files with 688 additions and 190 deletions

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
*.exe
sounds/**
sounds/**
conf.json

View File

@ -1,2 +1,28 @@
![Soundr Logo](/resources/logo.svg "Soundr Logo")
# Soundr
An opensource audio server meant for professional applications
Soundr is a simple, open-source, cross-platform audio playing server written in go.
It aims to be simple to setup and work in many envoriments. It is also designed to be
easy to use and maintain.
Soundr is able to play multiple audio files at the same time. It is able to intigrate well as it uses a REST endpoint.
Swagger Documentation for that endpoint is available in `apiDocs.yml`.
The software it self is written in go and uses the BEEP library. It is made to be shipped as a single executable.
Another target was a minimal dependency tree.
Initally it was written to be used with [Bitfocus Companion](https://bitfocus.io/companion) in a more professional envoriment. (A client for Companion is currently WiP)
# Installation
Installation is as simple as it gets as it is a single executable.
Download one of the releases, drop your sounds into the /sounds folder and run the executable.
# Configuration
TODO, no config yet. Soon ports and other settings will be added.
# Usage
Drop your sounds into the /sounds. You can play them by sending a GET request to the /v1/play endpoint.
You need to know the base64 encoded file name of the sound you want to play. You can get started by querying /v1/list. It will return a list of all sounds with their respective base64 encoded file name.
Use that base64 as the `file` parameter in the request.
**Note**: The sounds must be in the format `*.mp3` (more will be supported soon :tm:).
# ToDo
- [ ] Add support for other audio formats

144
apiDocs.yml Normal file
View File

@ -0,0 +1,144 @@
openapi: '3.0.2'
info:
title: Soundr
version: '1.0'
servers:
- url: http://localhost:8082/v1/
paths:
/play:
get:
summary: Plays a sound by it's base64'd name. Will load it to buffer first if not already loaded.
parameters:
- in: query
name: file
description: A base64 encoded version of the file name
schema:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
id:
type: number
description: The ID of the playing sound
'400':
description: Bad Request
content:
application/json:
schema:
type: object
properties:
reason:
type: string
description: The error message, in this case probably "file not found"
/buffer:
get:
summary: Loads a sound into the buffer.
parameters:
- in: query
name: file
description: A base64 encoded version of the file name
schema:
type: string
responses:
'200':
description: OK
'400':
description: Bad Request
content:
application/json:
schema:
type: object
properties:
reason:
type: string
description: The error message, in this case probably "file not found"
/bufferAll:
get:
summary: Loads all sounds into the buffer.
responses:
'200':
description: OK
/stop:
get:
summary: Stops a given sound by it's ID.
parameters:
- in: query
name: id
description: The ID of the sound to stop
schema:
type: number
responses:
'200':
description: OK
'400':
description: Bad Request
content:
application/json:
schema:
type: object
properties:
reason:
type: string
description: The error message
/stopAll:
get:
summary: Stops all sounds.
responses:
'200':
description: OK
/current:
get:
summary: Gets the current playing sound(s).
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
sounds:
type: array
items:
type: object
properties:
id:
type: number
description: The ID of the sound
name:
type: string
description: The name of the sound
loaded:
type: boolean
description: Whether the sound is loaded into the buffer
/list:
get: # TODO REWORK!!!!!
summary: Lists all sounds in the buffer.
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
sounds:
type: array
items:
type: object
properties:
name:
type: string
description: The name of the sound
base64:
type: string
description: The base64 version of the name
url:
type: string
description: The URL to the sound

78
handlerFunctions.go Normal file
View File

@ -0,0 +1,78 @@
package main
import (
"fmt"
"log"
"os"
"time"
"github.com/faiface/beep"
"github.com/faiface/beep/mp3"
"github.com/faiface/beep/speaker"
)
func BufferSound(file string) bool {
_, ok := streamMap[file]
if !ok {
fmt.Println("Not in memory, loading")
f, err := os.Open("./sounds/" + file)
if err != nil {
log.Fatal(err)
}
fmt.Println("Opened file")
streamer, format, _ := mp3.Decode(f)
speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))
fmt.Println("Decoded file")
buffer := beep.NewBuffer(format)
buffer.Append(streamer)
streamer.Close()
fmt.Println("Bufferd file")
// Save to streamMap
streamMap[file] = streamBuf{
Streamer: streamer,
Format: format,
Buffer: buffer,
}
return (true)
} else {
return (false)
}
}
func PlaySound(file string, index int) int {
playbacks[index] = playback{
File: file,
IsLoaded: false,
Streamer: nil,
Control: nil,
}
fmt.Println("Playing sound: " + file)
var buffer *beep.Buffer
BufferSound(file)
buffer = streamMap[file].Buffer
streamer := streamMap[file].Streamer
fmt.Println("Trying to play sound")
shot := buffer.Streamer(0, buffer.Len())
done := make(chan bool)
ctrl := &beep.Ctrl{Streamer: beep.Seq(shot, beep.Callback(func() {
done <- true
})), Paused: false}
playbacks[index] = playback{
File: file,
IsLoaded: true,
Streamer: streamer,
Control: ctrl,
}
speaker.Play(ctrl)
<-done
fmt.Println("Finished playing sound: " + file)
delete(playbacks, index)
return 1
}

181
resources/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

236
soundr.go
View File

@ -1,22 +1,14 @@
package main
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"html"
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"sync"
"time"
"github.com/faiface/beep"
"github.com/faiface/beep/mp3"
"github.com/faiface/beep/speaker"
)
type playback struct {
@ -38,198 +30,66 @@ type streamBuf struct {
Buffer *beep.Buffer
}
type Configuration struct {
Port int
}
var playbacks map[int]playback
var mapMutex = sync.Mutex{}
var streamMap map[string]streamBuf
func BufferSound(file string) bool {
_, ok := streamMap[file]
if !ok {
fmt.Println("Not in memory, loading")
f, err := os.Open("./sounds/" + file)
if err != nil {
log.Fatal(err)
}
fmt.Println("Opened file")
streamer, format, err := mp3.Decode(f)
speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))
fmt.Println("Decoded file")
buffer := beep.NewBuffer(format)
buffer.Append(streamer)
streamer.Close()
fmt.Println("Bufferd file")
// Save to streamMap
streamMap[file] = streamBuf{
Streamer: streamer,
Format: format,
Buffer: buffer,
}
return (true)
} else {
return (false)
}
}
func PlaySound(file string, index int) int {
playbacks[index] = playback{
File: file,
IsLoaded: false,
Streamer: nil,
Control: nil,
}
fmt.Println("Playing sound: " + file)
var buffer *beep.Buffer
BufferSound(file)
buffer = streamMap[file].Buffer
streamer := streamMap[file].Streamer
fmt.Println("Trying to play sound")
shot := buffer.Streamer(0, buffer.Len())
done := make(chan bool)
ctrl := &beep.Ctrl{Streamer: beep.Seq(shot, beep.Callback(func() {
done <- true
})), Paused: false}
playbacks[index] = playback{
File: file,
IsLoaded: true,
Streamer: streamer,
Control: ctrl,
}
speaker.Play(ctrl)
<-done
fmt.Println("Finished playing sound: " + file)
delete(playbacks, index)
return 1
}
func main() {
fmt.Println("Welcome to Soundr!")
playbacks = make(map[int]playback)
streamMap = make(map[string]streamBuf)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Query().Get("name")))
})
// Create /sounds if not exists
if _, err := os.Stat("./sounds"); os.IsNotExist(err) {
fmt.Println("Created /sounds folder")
os.Mkdir("./sounds", 0777)
}
http.HandleFunc("/v1/play", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var cnt = r.URL.Query().Get("file")
bytArr, err := base64.StdEncoding.DecodeString(cnt)
if err != nil {
log.Fatal(err)
// Handle config
fmt.Println("Opening conf.json")
file, fOpenError := os.Open("conf.json") // Try to open the file
if errors.Is(fOpenError, os.ErrNotExist) { // If it does not exist, create it
fmt.Println("Creating conf.json")
file, fOpenError = os.Create("conf.json")
if fOpenError != nil {
log.Fatal(fOpenError)
}
fmt.Println(string(bytArr[:]))
t, err := os.Stat("./sounds/" + string(bytArr[:]))
t = t
if !errors.Is(err, os.ErrNotExist) {
var currIndex = len(playbacks)
fmt.Fprintf(w, "{\"status\":\"ok\", \"id\":%d}", currIndex)
defer file.Close()
fmt.Println("Writing to conf.json")
// Write the default config to the file
json.NewEncoder(file).Encode(Configuration{
Port: 8080,
})
fmt.Println("Wrote to conf.json")
}
defer file.Close()
// Decode the config
decoder := json.NewDecoder(file)
configuration := Configuration{}
err := decoder.Decode(&configuration)
go PlaySound(string(bytArr[:]), currIndex)
if err != nil {
fmt.Println("error:", err)
}
} else {
fmt.Fprintf(w, "{\"status\":\"fail\", \"reason\":\"file not found\"}")
}
})
http.HandleFunc("/v1/buffer", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var cnt = r.URL.Query().Get("file")
bytArr, err := base64.StdEncoding.DecodeString(cnt)
if err != nil {
log.Fatal(err)
}
t, err := os.Stat("./sounds/" + string(bytArr[:]))
t = t
if !errors.Is(err, os.ErrNotExist) {
fmt.Fprintf(w, "{\"status\":\"ok\"}")
go BufferSound(string(bytArr[:]))
} else {
fmt.Fprintf(w, "{\"status\":\"fail\", \"reason\":\"file not found\"}")
}
})
http.HandleFunc("/v1/stopAll", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
playbacks = make(map[int]playback)
fmt.Fprintf(w, "{\"status\":\"ok\"}")
speaker.Clear()
//fmt.Fprintf(w, "{\"status\":\"ok\", \"id\":%d}", currIndex)
})
http.HandleFunc("/v1/stop", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var cnt, err = strconv.Atoi(r.URL.Query().Get("id"))
if err != nil {
fmt.Fprintf(w, "{\"status\":\"fail\", \"reason\":\"invalid id\"}")
}
value, ok := playbacks[cnt]
if !ok {
fmt.Fprintf(w, "{\"status\":\"fail\", \"reason\":\"audio not playing\"}")
} else {
fmt.Fprintf(w, "{\"status\":\"ok\", \"id\":%d}", value)
value.Control.Paused = true
value.Control.Streamer = nil
delete(playbacks, cnt)
}
//fmt.Fprintf(w, "{\"status\":\"ok\", \"id\":%d}", currIndex)
})
http.HandleFunc("/v1/current", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var tempResultSet map[int]playbackWebReturn
tempResultSet = make(map[int]playbackWebReturn)
for index, element := range playbacks {
tempResultSet[index] = playbackWebReturn{File: element.File, IsLoaded: element.IsLoaded, Id: index}
}
j, err := json.Marshal(tempResultSet)
if err != nil {
fmt.Printf("Error: %s", err.Error())
} else {
fmt.Println(string(j))
}
fmt.Fprintf(w, string(j))
})
http.HandleFunc("/v1/list", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var temp [][3]string
files, err := ioutil.ReadDir("./sounds/")
if err != nil {
log.Fatal(err)
}
for _, f := range files {
var soundObj [3]string
soundObj[0] = f.Name()
soundObj[1] = base64.StdEncoding.EncodeToString([]byte(f.Name()))
soundObj[2] = r.URL.Host + "/v1/play?file=" + soundObj[1]
temp = append(temp, soundObj)
}
j, err := json.Marshal(temp)
if err != nil {
fmt.Printf("Error: %s", err.Error())
} else {
fmt.Println(string(j))
}
fmt.Fprintf(w, string(j))
})
log.Fatal(http.ListenAndServe(":8081", nil))
// Web server stuff
// Play route, takes file as parameter, file is base64 encoded
http.HandleFunc("/v1/play", handlePlay)
// Buffer route, buffers file
http.HandleFunc("/v1/buffer", handleBuffer)
http.HandleFunc("/v1/bufferAll", handleBufferAll)
http.HandleFunc("/v1/stop", handleStop)
http.HandleFunc("/v1/stopAll", handleStopAll)
http.HandleFunc("/v1/current", handleCurrent)
http.HandleFunc("/v1/list", handleListing)
fmt.Println("Listening on port " + fmt.Sprint(configuration.Port))
log.Fatal(http.ListenAndServe(":"+fmt.Sprint(configuration.Port), nil))
}

208
webRoutes.go Normal file
View File

@ -0,0 +1,208 @@
package main
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"github.com/faiface/beep/speaker"
)
func handlePlay(w http.ResponseWriter, r *http.Request) {
// Rejct everything else then GET requests
if r.Method != "GET" {
http.Error(w, "Method is not supported.", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json") // Set the content type to json
var cnt = r.URL.Query().Get("file") // Retrieve the file name from the query string
bytArr, err := base64.StdEncoding.DecodeString(cnt) // Decode the base64 string
if err != nil {
log.Fatal(err)
}
t, err := os.Stat("./sounds/" + string(bytArr[:])) // Check if the file exists
if errors.Is(err, os.ErrNotExist) {
w.WriteHeader(400)
fmt.Fprintf(w, "{\"status\":\"fail\", \"reason\":\"file not found\"}")
return
}
if t.IsDir() { // Make sure it is not a folder we are trying to play
w.WriteHeader(400)
fmt.Fprintf(w, "{\"status\":\"fail\", \"reason\":\"target is folder\"}")
return
}
var currIndex = len(playbacks) // Create a new index for the playback
fmt.Fprintf(w, "{\"status\":\"ok\", \"id\":%d}", currIndex) // Return a JSON object to the user
go PlaySound(string(bytArr[:]), currIndex) // Play the sound
}
// Handle Buffering
func handleBufferAll(w http.ResponseWriter, r *http.Request) {
// Rejct everything else then GET requests
if r.Method != "GET" {
http.Error(w, "Method is not supported.", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json") // Set the content type to json
var temp []string
files, err := ioutil.ReadDir("./sounds/") // Read the directory
if err != nil {
log.Fatal(err)
}
// Loop through the files and add the file name to the temp array
// Also triggers the buffer process for the file
for _, f := range files {
temp = append(temp, f.Name())
go BufferSound(f.Name())
}
// Return the amount of files buffered
fmt.Fprintf(w, "{\"status\":\"ok\", \"amount\":%d}", len(temp))
}
func handleBuffer(w http.ResponseWriter, r *http.Request) {
// Rejct everything else then GET requests
if r.Method != "GET" {
http.Error(w, "Method is not supported.", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json") // Set the content type to json
var cnt = r.URL.Query().Get("file") // Retrieve the file name from the query string
bytArr, err := base64.StdEncoding.DecodeString(cnt) // Decode the base64 string
if err != nil {
log.Fatal(err)
}
t, err := os.Stat("./sounds/" + string(bytArr[:])) // Check if the file exists
if errors.Is(err, os.ErrNotExist) {
w.WriteHeader(400)
fmt.Fprintf(w, "{\"status\":\"fail\", \"reason\":\"file not found\"}")
return
}
if t.IsDir() { // Make sure it is not a folder we are trying to play
w.WriteHeader(400)
fmt.Fprintf(w, "{\"status\":\"fail\", \"reason\":\"target is folder\"}")
return
}
fmt.Fprintf(w, "{\"status\":\"ok\"}")
go BufferSound(string(bytArr[:]))
}
// Handeling Stop
func handleStop(w http.ResponseWriter, r *http.Request) {
// Rejct everything else then GET requests
if r.Method != "GET" {
http.Error(w, "Method is not supported.", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json") // Set the content type to json
var cnt, err = strconv.Atoi(r.URL.Query().Get("id")) // Retrieve the id, first convert it to an int
if err != nil {
fmt.Fprintf(w, "{\"status\":\"fail\", \"reason\":\"invalid id\"}")
}
value, ok := playbacks[cnt] // Get value from playbacks map
if !ok {
w.WriteHeader(400)
fmt.Fprintf(w, "{\"status\":\"fail\", \"reason\":\"audio not playing\"}")
} else {
fmt.Fprintf(w, "{\"status\":\"ok\"}")
// Stop by pausing first then, set the streamer to nil. Finally delete it from the map
value.Control.Paused = true
value.Control.Streamer = nil
delete(playbacks, cnt)
}
}
func handleStopAll(w http.ResponseWriter, r *http.Request) {
// Rejct everything else then GET requests
if r.Method != "GET" {
http.Error(w, "Method is not supported.", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json") // Set the content type to json
// Pause and stop all playbacks
for _, v := range playbacks {
v.Control.Paused = true
v.Control.Streamer = nil
}
speaker.Clear() // Clear the speaker and make it shut up
// Reset the map
playbacks = make(map[int]playback)
fmt.Fprintf(w, "{\"status\":\"ok\"}")
}
func handleCurrent(w http.ResponseWriter, r *http.Request) {
// Rejct everything else then GET requests
if r.Method != "GET" {
http.Error(w, "Method is not supported.", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json") // Set the content type to json
var tempResultSet map[int]playbackWebReturn = make(map[int]playbackWebReturn) // Create a new map to store the results
// Iterate through the playbacks map and add important information to the tempResultSet map
for index, element := range playbacks {
tempResultSet[index] = playbackWebReturn{File: element.File, IsLoaded: element.IsLoaded, Id: index}
}
// Convert the map to a JSON object and return it to the user
j, err := json.Marshal(tempResultSet)
if err != nil {
fmt.Printf("Error: %s", err.Error())
} else {
fmt.Println(string(j))
fmt.Fprintf(w, string(j))
}
}
func handleListing(w http.ResponseWriter, r *http.Request) {
// Rejct everything else then GET requests
if r.Method != "GET" {
http.Error(w, "Method is not supported.", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json") // Set the content type to json
var temp [][3]string
files, err := ioutil.ReadDir("./sounds/") // Find all files in the sounds directory
if err != nil {
log.Fatal(err)
}
// Add the file data to the temp array
for _, f := range files {
var soundObj [3]string
soundObj[0] = f.Name()
soundObj[1] = base64.StdEncoding.EncodeToString([]byte(f.Name()))
soundObj[2] = r.URL.Host + "/v1/play?file=" + soundObj[1]
temp = append(temp, soundObj)
}
// Convert the array to a JSON object and return it to the user
j, err := json.Marshal(temp)
if err != nil {
fmt.Printf("Error: %s", err.Error())
} else {
fmt.Println(string(j))
fmt.Fprintf(w, string(j))
}
}