DEV Community

loading...

Quick and dirty Audio playing in Golang on Windows

ik5 profile image ik_5 ・4 min read

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 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 and PlaySound 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 (the pain).

Binding ...

Windows usually provides C based ABI for API calls (unless you are using .NET apps, and then you are using CLR based virtual machine's ABI).

There are two ways for me to load it..., and I decided to bind myself on run-time, using Go's syscall.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 (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).

...
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:

...
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.

...
    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), while home/pro etc.. editions supported mostly extended ASCII with some 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.

...
// 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 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, 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

Warning

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

Discussion (0)

pic
Editor guide