DEV Community

ik_5
ik_5

Posted on

Quick and dirty Audio playing in Golang on Windows

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

Top comments (1)

Collapse
 
kenjitamura profile image
kenjitamura

I'd also like to point out that the golang.org/x/sys/windows package includes a command mkwinsyscall that can do most of the work for you in creating a function that calls windows API's.

I created my own PlaySound library for this by creating a file with:

const (
    SND_APPLICATION uint = 0x80       //look for application specific association
    SND_ALIAS       uint = 0x10000    //system event alias in win.ini
    SND_ALIAS_ID    uint = 0x110000   //predefined identifier for system event alias
    SND_ASYNC       uint = 0x1        //play asynchronously and return immediately after begin
    SND_FILENAME    uint = 0x20000    //pszSound parameter is filename
    SND_LOOP        uint = 0x8        //play sound repeatedly until called again with pszSound set to null.  Must also set ASYNC
    SND_MEMORY      uint = 0x4        //pszSound points to sound in memory
    SND_NODEFAULT   uint = 0x2        //don't use default sound if target sound not found.  Returns silent
    SND_NOSTOP      uint = 0x10       //don't stop currently playing sounds if device in use.  Function returns false
    SND_RESOURCE    uint = 0x40004    //pszSound points to resource.  Need to set HMOD
    SND_SENTRY      uint = 0x00080000 //triggers a visual cue with sound.  Requires vista or later
    SND_SYNC        uint = 0x0        //sound is played synchronously and playsound returns after completes.  Default
    SND_SYSTEM      uint = 0x00200000 //sets the sound to be played with volume set for system notification sounds.  Requires vista or later
)

//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go syscall_windows.go

//sys PlaySound(pszSound string, hmod *uint32, fdwSound uint) (ret bool) = Winmm.PlaySound
Enter fullscreen mode Exit fullscreen mode

Then after calling the command:

go generate syscall_windows.go
Enter fullscreen mode Exit fullscreen mode

A file called zsyscall_windows.go was generated that handles all the heavy lifting of translating the go values into windows values. An added benefit is that the generated file will follow sys/windows conventions like loading the dll library with windows.NewLazySystemDLL which limits the search path to the windows system path to help prevent dll preloading.