[ANN] goop, a command-line audio synthesizer

1,377 views
Skip to first unread message

Peter Bourgon

unread,
Jan 24, 2012, 7:50:42 AM1/24/12
to golang-nuts
My pet project is asymptotically approaching something like a stable
architecture, so I thought it would be good to solicit some broader
feedback.

goop[1] is a nonsensical name for a command-line synthesizer. It
models a patch-bay of audio modules: generators (aka. synthesizers)
and effects. It's (largely) event-driven, and makes (in my view) good
use of Go's channel idioms to be almost-completely free of
synchronization primitives.

I'm excited to get feedback from anyone who would care to take a look,
but I'm especially excited to hear from any audio/DSP guys, as I'm
entirely self-taught and may be missing common/best-practice stuff. As
far as I know this is the first "public" project in Go doing audio
generation/manipulation. I'm in huge debt to Gordon Klaus' seemingly
flawless portaudio-go bindings[2] and bobappleyard's readline
bindings[3].

Cheers,
Peter.

[1] http://code.google.com/p/goop
[2] http://code.google.com/p/portaudio-go
[3] http://github.com/bobappleyard/readline

Andrew Gerrand

unread,
Jan 24, 2012, 7:43:17 PM1/24/12
to peter....@gmail.com, golang-nuts

This looks really great. I'll have a play with it over the weekend.

However, this isn't the first audio synthesizer written in Go. I wrote
a (very) rudimentary synth last year but got distracted before it went
anywhere:
https://github.com/nf/gosynth
Its design seems strange to me now. The first thing I'd do were I to
continue with it would be to throw the existing code out. :-)

Andrew

Kyle Lemons

unread,
Jan 25, 2012, 12:53:48 AM1/25/12
to Andrew Gerrand, peter....@gmail.com, golang-nuts
This looks really great. I'll have a play with it over the weekend.

I agree, it looks like fun.  Unfortunately, I'm not sure I even know enough to build something cool (other than, say, DTMF or something, lol).
 
However, this isn't the first audio synthesizer written in Go. I wrote
a (very) rudimentary synth last year but got distracted before it went
anywhere:
 https://github.com/nf/gosynth
Its design seems strange to me now. The first thing I'd do were I to
continue with it would be to throw the existing code out. :-)

It's good to know I'm not the only one who thinks that about Go code he wrote a year or so ago :). 

Tony Worm

unread,
Jan 30, 2012, 5:02:13 PM1/30/12
to golan...@googlegroups.com, peter....@gmail.com
very cool, had just started to try and do this myself three days before you posted this

I took the liberty of adding new generator that uses wav files (just a quick hack until I can look into your code more)
now you can do:

add wav w filename




----------------------------------
relevant code changes--- besides turning into a package


ui.go:

func doAdd(args []string) {
if len(args) < 2 {
fmt.Printf("add <what> <name>\n")
return
}
switch args[0] {
    case "wav":
      if len(args) < 3 {
        fmt.Printf("add wav <name> <file>\n")
        return
      }
      add(args[1], NewWavGenerator(args[2]) )
...
}


generator.go:
type WavGenerator struct {
  generatorChannels
  simpleParameters
  file string
  data []float32
  curr int // pos in data for nextValue()
}

func NewWavGenerator(file string ) *WavGenerator {
  g := WavGenerator{makeGeneratorChannels(),makeSimpleParameters(),file,nil,0}
  w := ReadWavData(file) // read wav file
  g.data = btof32(w.data)  // get float version of data
  fmt.Printf( "len(wav.data) = %d\n", len(g.data) )
  go g.generatorLoop(&g.simpleParameters, &g)
  return &g
}

func (g *WavGenerator) nextValue() (f float32) {
  f = g.data[g.curr]
  g.curr++
  if g.curr >= len(g.data) {g.curr = 0}
  return
}


wav.go (new file)
package goop

import (
  bin "encoding/binary"
  "os"
  "bufio"
  "fmt"
)

type WavData struct {
  bChunkID [4]byte         // B
  ChunkSize uint32       // L
  bFormat [4]byte          // B

  bSubchunk1ID [4]byte     // B
  Subchunk1Size uint32   // L
  AudioFormat uint16     // L
  NumChannels uint16     // L
  SampleRate uint32      // L
  ByteRate uint32        // L
  BlockAlign uint16      // L
  BitsPerSample uint16   // L

  bSubchunk2ID [4]byte     // B
  Subchunk2Size uint32   // L
  data []byte             // L
}

func ReadWavData( fn string ) (wav WavData) {
  ftotal, err := os.OpenFile(fn, os.O_RDONLY, 0)
  if err != nil {
    fmt.Printf( "Error opening\n" )
  }
  file := bufio.NewReader(ftotal)

  bin.Read( file, bin.BigEndian, &wav.bChunkID )
  bin.Read( file, bin.LittleEndian, &wav.ChunkSize )
  bin.Read( file, bin.BigEndian, &wav.bFormat )

  bin.Read( file, bin.BigEndian, &wav.bSubchunk1ID )
  bin.Read( file, bin.LittleEndian, &wav.Subchunk1Size )
  bin.Read( file, bin.LittleEndian, &wav.AudioFormat )
  bin.Read( file, bin.LittleEndian, &wav.NumChannels )
  bin.Read( file, bin.LittleEndian, &wav.SampleRate )
  bin.Read( file, bin.LittleEndian, &wav.ByteRate )
  bin.Read( file, bin.LittleEndian, &wav.BlockAlign )
  bin.Read( file, bin.LittleEndian, &wav.BitsPerSample )


  bin.Read( file, bin.BigEndian, &wav.bSubchunk2ID )
  bin.Read( file, bin.LittleEndian, &wav.Subchunk2Size )

  wav.data = make( []byte, wav.Subchunk2Size )
  bin.Read( file, bin.LittleEndian, &wav.data )

  /*
   *   fmt.Printf( "\n" )
   *   fmt.Printf( "ChunkID*: %s\n", ChunkID )
   *   fmt.Printf( "ChunkSize: %d\n", ChunkSize )
   *   fmt.Printf( "Format: %s\n", Format )
   *   fmt.Printf( "\n" )
   *   fmt.Printf( "Subchunk1ID: %s\n", Subchunk1ID )
   *   fmt.Printf( "Subchunk1Size: %d\n", Subchunk1Size )
   *   fmt.Printf( "AudioFormat: %d\n", AudioFormat )
   *   fmt.Printf( "NumChannels: %d\n", NumChannels )
   *   fmt.Printf( "SampleRate: %d\n", SampleRate )
   *   fmt.Printf( "ByteRate: %d\n", ByteRate )
   *   fmt.Printf( "BlockAlign: %d\n", BlockAlign )
   *   fmt.Printf( "BitsPerSample: %d\n", BitsPerSample )
   *   fmt.Printf( "\n" )
   *   fmt.Printf( "Subchunk2ID: %s\n", Subchunk2ID )
   *   fmt.Printf( "Subchunk2Size: %d\n", Subchunk2Size )
   *   fmt.Printf( "NumSamples: %d\n", Subchunk2Size / uint32(NumChannels) / uint32(BitsPerSample/8) )
   *   fmt.Printf( "\ndata: %v\n", len(data) )
   *   fmt.Printf( "\n\n" )
   */
  return
}

const (
  mid16 uint16 = 1>>2
  big16 uint16 = 1>>1
  big32 uint32 = 65535
)

func btou( b []byte ) (u []uint16) {
  u = make( []uint16, len(b)/2 )
  for i,_ := range u {
    val := uint16(b[i*2])
    val += uint16(b[i*2+1])<<8
    u[i] = val
  }
  return
}

func btoi16( b []byte ) (u []int16) {
  u = make( []int16, len(b)/2 )
  for i,_ := range u {
    val := int16(b[i*2])
    val += int16(b[i*2+1])<<8
    u[i] = val
  }
  return
}

func btof32( b []byte ) (f []float32) {
  u := btoi16(b)
  f = make([]float32, len(u))
  for i,v := range u {
    f[i] = float32(v)/float32(32768)
  }
  return
}

func utob( u []uint16 ) (b []byte) {
  b = make( []byte, len(u)*2 )
  for i,val := range u {
    lo := byte(val)
    hi := byte(val>>8)
    b[i*2] = lo
    b[i*2+1] = hi
  }
  return
}







Matt Kane's Brain

unread,
Jan 30, 2012, 5:22:10 PM1/30/12
to golan...@googlegroups.com, peter....@gmail.com
I've got a libsndfile binding you could incorporate:
https://github.com/mkb218/gosndfile

--
matt kane's brain
http://hydrogenproject.com

Peter Bourgon

unread,
Jan 31, 2012, 7:16:23 AM1/31/12
to golan...@googlegroups.com
On Mon, Jan 30, 2012 at 11:02 PM, Tony Worm <verd...@gmail.com> wrote:
> very cool, had just started to try and do this myself three days before you
> posted this
>
> I took the liberty of adding new generator that uses wav files (just a quick
> hack until I can look into your code more)
> now you can do:
>
> add wav w filename

Sweet! I'll hack this in directly. Thanks.

For the record I've moved the codebase over to Github,

http://github.com/peterbourgon/goop

Reply all
Reply to author
Forward
0 new messages