Sound file Plotter in Go using gosndfile / libsndfile

· by joe · Read in about 2 min · (314 Words)

It’s no secret that golang is my new favourite language. I’ve used it to implement the latest folktunefinder┬ásearch engine and really enjoyed it.

On an unrelated note, whilst looking at what libraries are available I came across the gosndfile library written by Matt Kane / @nynexrepublic. It’s a wrapper for libsndfile, a C library for reading and writing sound files.

To get started, I thought I’d write a short program that plots a signal (mono, stereo, and in theory, more channels) from a sound file and makes a PNG out of it. It’s not particularly efficient, as it allocates a very large buffer up-front and doesn’t re-use it. A real implementation would use a smaller buffer and loop over the sound file until the end. Still, it’s a fun proof of concept for the library. And it would have to iterate over the whole file anyway in order to dimension the image in the first place.

Here’s a heavily cropped sample output. Click for a full version. You’ll have to zoom in, it’s very wide and not very high.

 

Here’s the code. You can download it here.

You’ll need to install libsndfile C library and gosndfile Go library.

package main

import (
    "fmt"
    "image"
    "image/color"
    "image/png"
    "log"
    "os"
    "sndfile"
)

// Number of seconds worth of buffer to allocate.
const Seconds = 10

// Height per channel.
const ImageHeight = 200

func main() {
    if len(os.Args) < 3 {
        fmt.Println("Usage: go run plotter.go /path/to/wav /path/to/png")
        return
    }
    var info sndfile.Info   soundFile, err := sndfile.Open(os.Args[1], sndfile.Read, &info)
    if err != nil {
        log.Fatal("Error", err)
    }
    defer soundFile.Close()
    imageFile, err := os.Create(os.Args[2])
    if err != nil {
        log.Fatal(err)
    }
    defer imageFile.Close()
    buffer := make([]float32, Seconds*info.Samplerate*info.Channels)
    numRead, err := soundFile.ReadItems(buffer)
    numSamples := int(numRead/int64(info.Channels))
    numChannels := int(info.Channels)
    outimage := image.NewRGBA(image.Rect(0, 0, numSamples, ImageHeight * numChannels))
    if err != nil {
        return
    }
    // Both math.Abs and math.Max operate on float64. Hm.
    max := float32(0)
    for _, v := range buffer {
        if v > max {
            max = v
        } else if v*-1 > max {
            max = v * -1
        }
    }

    // Work out scaling factor to normalise signaland get best use of space.
    mult := float32(ImageHeight/max) / 2

    // Signed float so add 1 to turn [-1, 1] into [0, 2].
    for i := 0; i < numSamples; i++ {
        for channel := 0; channel < numChannels; channel ++ {
            y := int(buffer[i*numChannels+channel]*mult+ImageHeight/2) + ImageHeight * channel
            outimage.Set(i, y, color.Black)
            outimage.Set(i, y+1, color.Black)

        }

    }

    png.Encode(imageFile, outimage)
}

This looks like a really great library for doing interesting things. I’m looking forward to working out what those interesting things are!

Read more