A cross-plattform Soundboard written in Go and Svelte
Tags: go, svelte, javascript, showdev
Published: 2020-05-14 13:16:37

{% github kevinschweikert/go-soundboard no-readme %}

## :bulb: The idea

The "need" for this application arosed, when i wanted to have some fun and use a soundboard for some web conferences with friends and family. I am running on Linux and i couldn't find a software which worked like I wanted it to work. So I decided to write my own and practice my skills in my first real Go project.
It should just output some sound files with the push of a button. Then I could use the **Jack Audio Connection Kit** 

> JACK Audio Connection Kit (or JACK; a recursive acronym) is a professional sound server daemon that provides real-time, low-latency connections for both audio and MIDI data between applications that use its API.
> 
> Wikipedia

to route that into my virtual input. Fortunately this was super easy because, when I was running it for the first time, it showed up as an own Jack client. So I just had to make the connection, like in the following example. The **PulseAudio JACK Sink** and the **PulseAudio JACK Source** are my virtual in- and output. They are set in my system as in- and output device as well. Then I can connect **system** (Microphone) and **alsa-jack.jackP.122733.0** (my soundboard application) into the virtual input. It's also connected to my system output to hear the sounds myself. Then, in the meeting software i just have to select **PulseAudio JACK Source** as the microphone input and the participants will hear me as well as my awesome sounds!

![System In Out](https://raw.githubusercontent.com/kevinschweikert/go-soundboard/master/assets/system_in_out.png)

![JACK Routing](https://raw.githubusercontent.com/kevinschweikert/go-soundboard/master/assets/jack-routing.png)

## :floppy_disk: The Server

The server is written in Go. It's a simple HTTP-Server which serves the Web-UI and creates a websocket endpoint for the control messages. I used the package [Beep](https://github.com/faiface/beep) to play the audio files in a folder and [Gorilla Websocket](https://github.com/gorilla/websocket) for easy websocket handling.

When you start the application it searches for all the files in the specified folder. For that i created a package called `audio` and some structs to hold the necessary information.

```go
package audio

// SoundFile holds a sound struct
type SoundFile struct {
	Path      string `json:"path"`
	Name      string `json:"name"`
	Extension string `json:"extension"`
	ID        int    `json:"id"`
}
```

After i collected all the `SoundFiles` i created a new `SoundDirectory` to keep things more compact and have a reference to the folder file path

```go
// SoundDirectory collects all SoundFiles from a specific path
type SoundDirectory struct {
	SoundFiles []SoundFile `json:"soundfiles"`
	Path       string      `json:"path"`
}
```

Then a new Panel object is created like in [this Beep example](https://github.com/faiface/beep/blob/master/examples/speedy-player/main.go) but slightly modified to also hold the newly created SoundDirectory and instead of a streamer i used the mixer to only resample one stream instead of every file stream. To learn more about the Beep package, look at the [Wiki](https://github.com/faiface/beep/wiki)

```go
// Panel holds all Player structs like mixer, ctrl and Volume
type Panel struct {
	speakerSampleRate beep.SampleRate
	mixer             *beep.Mixer
	ctrl              *beep.Ctrl
	Volume            *effects.Volume
	SoundDir          SoundDirectory
}

// NewPanel returns a pointer to a Panel struct
func NewPanel(speakerSampleRate int, dir SoundDirectory) *Panel {
	mixer := &beep.Mixer{}
	ctrl := &beep.Ctrl{Streamer: mixer}
	volume := &effects.Volume{Streamer: mixer, Base: 2}
	return &Panel{beep.SampleRate(speakerSampleRate), mixer, ctrl, volume, dir}
}
```

In the `main` function I parse some command line flags, get all the audio files from the specified folder (code is not shown in this article), instantiate a new `audio.Pane`l struct and pass this to the handleWebsocket function. After this, I start the server. There is some other code to serve the static files from the web interface, but I decided to keep that out of the scope of this article. 

```go
// Define and parse the command line flags
folderPath := flag.String("path", "./sounds", "path to sound files")
speakerSampleRate := flag.Int("samplerate", 48000, "Output Samplerate in Hz")
buffSize := flag.Int("buff", 256, "Output buffer size in bytes")
port := flag.Int("port", 8000, "Port to listen for the webinterface")
flag.Parse()

// create a new SoundDirectory
dir, err := audio.GetFilesInFolder(*folderPath)
if err != nil {
	log.Println(err)
}

// create a new Panel
ap := audio.NewPanel(*speakerSampleRate, dir)
err = ap.Init(*buffSize)
if err != nil {
	log.Println(err)
}

http.HandleFunc("/websocket", handleWebSocket([OTHER ARGUMENTS], ap))
log.Printf("Server listening on 0.0.0.0:%d", *port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
```

From there I can now send the `SoundFile` slice through the websocket connection with marshalling it into a `Msg` struct with some additional information.

```go
// Switch constants
const (
	Load   = "load"
	Play   = "play"
	Error  = "error"
	Volume = "volume"
	Stop   = "stop"
)

// Msg struct to marshal and unmarshal the websocket json data
type Msg struct {
    Type       string            `json:"type"`
    Msg        string            `json:"msg"`
    SoundFiles []audio.SoundFile `json:"soundfiles"`
    Volume     float64           `json:"volume"`
}
```

As you can see, I defined my own message protocol. Every message has to have a type and with this information i know how to use this message. For example in a switch statement.   
I read the JSON object from the connection with `c.ReadJSON()` and put the `Type` field in the switch statement. After this i can decide, what to do with the message.   
For example, when the `Msg` is of `Type: Play` i use the function `PlaySound()` from my `audio.Panel` and give it the first file from the `SoundFiles` array (That's my solution to reuse the `SoundFiles` field multiple times. As an array of multiple files in the `Load` command or an array with only one item in the `Play` command).   
If there is an error while trying to play `SoundFile` i create a new message with `Type: Error` and the error text itself in the message field. This is sent to my frontend and is handled with a notification for the user. But there are more possibilities like a message box with a kind of log of all the error messages.

```go
//c is the pointer to the websocket client connection with the type *websocket.Conn

payload := new(Msg)
err := c.ReadJSON(payload)

switch Msg.Type {
	case Play:
		err := ap.PlaySound(payload.SoundFiles[0])
			if err != nil {
				c.WriteJSON(Msg{
					Type: Error,
					Msg:  err.Error(),
				})
			}
	case Load:
		....
	.
	.
	.
}
```


---

## :pencil2: The UI

Because, i have no idea how to build a Desktop UI i decided to build a web interface with my favorite JavaScript Framework [Svelte](https://svelte.dev/). The Web-UI is served from my application and connects to the `/websocket` route to receive all the necessary data, which is also processed in a switch statement. In a simpler form it looks like this:


```html
<script>
	// Import the SoundButton component
	import SoundButton from "./SoundButton.svelte";

	// variable to hold the soundfile array
	let sounds = [] 
	
	// create websocket connection with location.host to work when served from localhost or other interface
	const websocket = new WebSocket("ws://" + location.host + "/websocket");
	
	//Define onmessage event handling
    websocket.onmessage = function(event) {
    
    	// data will be the json object representing the go Msg struct
    	const data = JSON.parse(event.data);
      
    	// same switch logic like in Go
    	switch (data.type) {
    		case "load":
    		// put the soundfiles array into the global "sound" variable
        	sounds = data.soundfiles;
        	break;
        case "volume":
        	...
        	break;
        .
        .
        .
      }
    };
    
    const playSound = (msg) => {
    	ws.send(JSON.stringify(msg))
	}
	
</script>

<!-- for each array object in sounds create a SoundButton component and pass in this object -->
{#each sounds as sound}
      <SoundButton on:play={playSound} soundFile={sound}/>
{/each}
```

For each array object in `sounds`, Svelte will create a `<SoundButton/>` component. If the array changes, the buttons will dynamically change as well. Also, you notice that the comomponent has a custom event `on:play`. It will be fired when the button is clicked and send some data with the event. In Svelte you can just create an event dispatcher and name your custom event, so you can listen to it wherever you want to use the component. The SoundButton component looks something like this:

```html
<script>
	import { createEventDispatcher } from "svelte";
	const dispatch = createEventDispatcher()
	
	export let soundFile = {} 
	
	const playSound = () => {
    	const playMsg = {
    		type: "play",
    		soundfiles: [soundFile],
    	};
    	dispatch("play", playMsg);
	 };
</script>

<button on:click={playSound}>
	{soundFile.name}
</button>
```

I know that this is a very basic explanation how everything works but i want to keep it short and basic. If there are any questions I'm happy to help and explain! Have a look at the full code at github:

{% github kevinschweikert/go-soundboard no-readme %}

## :wrench: Usage

```bash
go build -o [EXECUTABLE_NAME]
./[EXECUTABLE_NAME] [FLAGS]

OR

go run .

```

Start the server application with these possible flags:

```bash
   -buff int
        Output buffer size in bytes (default 256)
  -path string
        path to sound files (default "./sounds")
  -port int
        Port to listen for the web interface (default 8000)
  -samplerate int
        Output Samplerate in Hz (default 48000)
 ```
 
 The go to `localhost:8000` and you should see this:
 
 ![Go Soundboard Webinterface](https://raw.githubusercontent.com/kevinschweikert/go-soundboard/master/assets/go-soundboard-banner.png)

## :confetti_ball: The End

This is my first post and public project i am showing to you. There is still so many things i could make better but I'm happy to hear your thoughts! I like to hear your suggestions or constructive critisicism about my code, the idea and the article itself! I'm working on building executables for every system, so it's easier to use for everyone and have fun with it! 

Cheers!