Quick and dirty Audio playing in Golang on Windows
Tags: audio, go, windows
Published: 2020-02-20 15:50:17

Over 20 years ago I used to write Delphi and VB in the MS Windows world.

A decision by MS to remove something from Windows libraries helped me  switching to Linux, and I never really looked back.

But now I have a client that wish for my service to work on their Windows (instead of Linux). 

I started looking at the Windows API again (hello old MSDN), and understood how much have changed since I last touched the API.

Most documentation are for the C# dev's, but I never even written an `Hello world` with it, and found out how much effort does the working with [`IMMDevice`](https://docs.microsoft.com/en-us/windows/win32/api/mmdeviceapi/nn-mmdeviceapi-immdevice) requires from me, and all my client requires from me here is to play a `wav` file for the default Audio device.

So I started thinking how to do that. Google reminded me by accident the "good" old world of [`waveform`](https://docs.microsoft.com/en-us/windows/win32/multimedia/waveform-audio?redirectedfrom=MSDN) and [`PlaySound`](https://docs.microsoft.com/en-us/previous-versions//dd743680(v=vs.85)?redirectedfrom=MSDN) function.

After several hours, I came up with a working copy of Go over Windows (my development is still on Linux, and cross compiled to Windows).
I do not own a Windows copy, only Linux(es) for so many years now, so my tests are on my Client's Windows machine, while I'm connected using [RDP](https://en.wikipedia.org/wiki/Remote_Desktop_Protocol) (the pain).

## Binding ...

Windows usually provides C based ABI for API calls (unless you are using .NET apps, and then you are using [CLR](https://en.wikipedia.org/wiki/Common_Language_Runtime) based virtual machine's ABI).

[There are two ways for me to load it...](https://dev.to/ik5/golang-and-shared-objects-part-1-the-background-4ecf), and I decided to bind myself on `run-time`, using Go's [`syscall.DLL`](https://golang.org/pkg/syscall/?GOOS=windows#DLL).

### First step

My first step was going over the `mmsystem.h` header file, just to find out that it's a `meta` header for including many headers. 
Quick [`grep`](https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/findstr) (thanks Google for that command btw) I found that all I need is under the `playsoundapi.h` header.

I copied all the integer based constants and just made it Go syntax friendly (but not Linter friendly).

```go
...
const (
	SND_SYNC      uint = 0x0000 /* play synchronously (default) */
	SND_ASYNC     uint = 0x0001 /* play asynchronously */
	SND_NODEFAULT uint = 0x0002 /* silence (!default) if sound not found */
	SND_MEMORY    uint = 0x0004 /* pszSound points to a memory file */
	SND_LOOP      uint = 0x0008 /* loop the sound until next sndPlaySound */
	SND_NOSTOP    uint = 0x0010 /* don't stop any currently playing sound */
...
```

### Initialize `syscall.DLL`

I can implement some nice functions after having the constants.

After some thinking, I decided to create a wrapper using a more Go friendly function that calls the ugly Windows API functions.

I started by defining a call to the `.dll` file:

```go
...
var (
	mmsystem = syscall.MustLoadDLL("winmm.dll")
...
```

`syscall.MustLoadDLL` loads a `.dll` file to memory, and if fails the loading will panic my process.

`mmsystem` is now a pointer of struct `syscall.DLL`.


**Note**:

Using Go, every function with `Must` will `panic` if there is an error.


### Memory address for functions

Now that I have the `.dll` file all loaded an warm up for me (thanks Go), I can get do some stuff with it.

In this case, I need some API's function love.

```go
...
  	playSound     = mmsystem.MustFindProc("PlaySound")
	sndPlaySoundA = mmsystem.MustFindProc("sndPlaySoundA")
	sndPlaySoundW = mmsystem.MustFindProc("sndPlaySoundW")
)
...
```

The old world of Windows (maybe also in the new?) the `NT`/`server` based run-time supported [Unicode (UTF-16BE)](https://en.wikipedia.org/wiki/UTF-16), while home/pro etc.. editions supported mostly [extended ASCII](https://en.wikipedia.org/wiki/Extended_ASCII) with some [`code page`](https://en.wikipedia.org/wiki/Windows_code_page) encoding for human languages.

The suffix of `A` and `W` provides support for the two types.
The `A` suffix is for `ASCII` and the `W` suffix is for `Wide char` encoding - meaning multi-byte encoding.

I decided to support both of them in order to learn how Go works with both of them.

### Wrapping and gifting

I loaded the functions that I wish to use.

Now it's time to wrap the API functions in order to have a nice Go like code rather then repeating my code usage each time.

When I used `MustFindProc`, I actually got a new pointer to a struct named `Proc`.

`Proc` have one interesting function: `Call`. It actually execute the arguments we need (up to 18 arguments), using `syscallXX` for us instead of us writing this ugly code.

`Call` returns three arguments, but I ignored them (bad dev, bad dev) - the last argument is an error that might have happened.

```go
...
// PlaySound play sound in Windows
func PlaySound(sound string, hmod int, flags uint) {
	s16, _ := syscall.UTF16PtrFromString(sound)
	playSound.Call(uintptr(unsafe.Pointer(s16)), uintptr(hmod), uintptr(flags))
}

// SndPlaySoundA play sound file in Windows
func SndPlaySoundA(sound string, flags uint) {
	b := append([]byte(sound), 0)
	sndPlaySoundA.Call(uintptr(unsafe.Pointer(&b[0])), uintptr(flags))
}
...
```

On `PlaySound` I converted the Go string (UTF-8) to `syscall`'s `UTF16BE` (c-)string.

On `SndPlaySoundA` I converted Go string into ASCII, using slice of byte, and added a [null terminated](https://en.wikipedia.org/wiki/Null-terminated_string) at the end of the slice to let C based code when the string needs to end.

On all functions  (`PlaySound` and `SndPlaySoundX`) there is some ugly code, so the wrapper hides it, giving us a nice Go-ish syntax instead.

When using `unsafe`, it is important to note that [CPU's endian](https://en.wikipedia.org/wiki/Endianness), and so does memory addresses are not cross platformed as much as other code.

So it is important to understand that usage of code that touches `unsafe` contain some possible pitfalls that will be discovered on run-time only, and on some machines only. FUN!


## The full Code

{% gist https://gist.github.com/ik5/14bfae814c17a880b3775bb59b855c5a %}

**Warning**

On a real production code, there are more steps to take - **Always check and validate errors, never ignore them as I did**