Skip to content

Instantly share code, notes, and snippets.

@paulsmith
Created March 20, 2015 17:33
Show Gist options
  • Save paulsmith/09a7c8523b34a3a3f081 to your computer and use it in GitHub Desktop.
Save paulsmith/09a7c8523b34a3a3f081 to your computer and use it in GitHub Desktop.
package drum
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"os"
"strconv"
"strings"
)
// DecodeFile decodes the drum machine file found at the provided path
// and returns a pointer to a parsed pattern which is the entry point to the
// rest of the data.
func DecodeFile(path string) (*Pattern, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
p := new(Pattern)
if err := p.decode(f); err != nil {
return nil, err
}
return p, nil
}
// Pattern is the high level representation of the
// drum pattern contained in a .splice file.
type Pattern struct {
// Version is the hardware version of the drum machine, ex. 0.808-alpha
Version string
// BPM is the tempo in beats per minute.
BPM float32
// Track is a slice of track objects for the pattern.
Tracks []Track
}
// Track is a single measure of an audio sample.
type Track struct {
// ID is a unique identifier for the track.
ID int
// Name is the name of the instrument.
Name string
// Steps is a 16 step measure in 4/4 time, each note is a 16th note.
Steps [16]Note
}
// Note is whether a 16th note should be played.
type Note bool
// header is the fixed-size beginning of a .splice file.
type header struct {
// first 6 bytes must be "SPLICE"
Initial [13]byte
// length of rest of file after this point
Len byte // TODO: maybe more than a byte?
// arbitrary string
Version [32]byte
BPM float32
}
// trackHeader is the fixed-size start of a track, of which there are a dynamic
// number of in a .splice file.
type trackHeader struct {
ID byte
_ [3]byte
// length of name
Len byte
}
// countingReader keeps track of number of bytes read by underlying io.Reader.
type countingReader struct {
io.Reader
nread int
}
func (cr *countingReader) Read(p []byte) (n int, err error) {
n, err = cr.Reader.Read(p)
if err != nil {
return
}
cr.nread += n
return
}
// errorReader remembers the last error by Read() and short-circuits future
// reads, to simplify repetitive error handling logic.
type errorReader struct {
io.Reader
err error
}
func (er *errorReader) Read(p []byte) (n int, err error) {
if er.err != nil {
return 0, er.err
}
n, err = er.Reader.Read(p)
er.err = err
return
}
func (p *Pattern) String() string {
var buf bytes.Buffer
buf.WriteString("Saved with HW Version: " + p.Version + "\n")
buf.WriteString("Tempo: " + strconv.FormatFloat(float64(p.BPM), 'f', -1, 32) + "\n")
for _, t := range p.Tracks {
buf.WriteString(fmt.Sprintf("(%d) %s\t", t.ID, t.Name))
for i, step := range t.Steps {
if i%4 == 0 {
buf.WriteString("|")
}
buf.WriteString(step.String())
}
buf.WriteString("|\n")
}
return buf.String()
}
func (n Note) String() string {
if bool(n) {
return "x"
}
return "-"
}
func (p *Pattern) decode(r io.Reader) error {
var hdr header
if err := binary.Read(r, binary.LittleEndian, &hdr); err != nil {
return err
}
p.Version = strings.TrimRight(string(hdr.Version[:]), "\x00")
p.BPM = hdr.BPM
// remaining number of bytes past the version and the tempo
remaining := int(hdr.Len) - len(hdr.Version) - binary.Size(p.BPM)
// specialized readers to simplify track-reading logic below
cr := &countingReader{Reader: r}
er := &errorReader{Reader: cr}
// read tracks
var (
th trackHeader
name [256]byte
s [16]byte
)
for cr.nread < remaining {
binary.Read(er, binary.LittleEndian, &th)
er.Read(name[:th.Len])
binary.Read(er, binary.LittleEndian, &s)
if er.err != nil {
return er.err
}
t := Track{
ID: int(th.ID),
Name: strings.TrimRight(string(name[:th.Len]), "\x00"),
}
for i := 0; i < 16; i++ {
t.Steps[i] = Note(s[i] == 1)
}
p.Tracks = append(p.Tracks, t)
}
return nil
}
package drum
import (
"fmt"
"path"
"testing"
)
func TestDecodeFile(t *testing.T) {
tData := []struct {
path string
output string
}{
{"pattern_1.splice",
`Saved with HW Version: 0.808-alpha
Tempo: 120
(0) kick |x---|x---|x---|x---|
(1) snare |----|x---|----|x---|
(2) clap |----|x-x-|----|----|
(3) hh-open |--x-|--x-|x-x-|--x-|
(4) hh-close |x---|x---|----|x--x|
(5) cowbell |----|----|--x-|----|
`,
},
{"pattern_2.splice",
`Saved with HW Version: 0.808-alpha
Tempo: 98.4
(0) kick |x---|----|x---|----|
(1) snare |----|x---|----|x---|
(3) hh-open |--x-|--x-|x-x-|--x-|
(5) cowbell |----|----|x---|----|
`,
},
{"pattern_3.splice",
`Saved with HW Version: 0.808-alpha
Tempo: 118
(40) kick |x---|----|x---|----|
(1) clap |----|x---|----|x---|
(3) hh-open |--x-|--x-|x-x-|--x-|
(5) low-tom |----|---x|----|----|
(12) mid-tom |----|----|x---|----|
(9) hi-tom |----|----|-x--|----|
`,
},
{"pattern_4.splice",
`Saved with HW Version: 0.909
Tempo: 240
(0) SubKick |----|----|----|----|
(1) Kick |x---|----|x---|----|
(99) Maracas |x-x-|x-x-|x-x-|x-x-|
(255) Low Conga |----|x---|----|x---|
`,
},
{"pattern_5.splice",
`Saved with HW Version: 0.708-alpha
Tempo: 999
(1) Kick |x---|----|x---|----|
(2) HiHat |x-x-|x-x-|x-x-|x-x-|
`,
},
}
for _, exp := range tData {
decoded, err := DecodeFile(path.Join("fixtures", exp.path))
if err != nil {
t.Fatalf("something went wrong decoding %s - %v", exp.path, err)
}
if fmt.Sprint(decoded) != exp.output {
t.Logf("decoded:\n%#v\n", fmt.Sprint(decoded))
t.Logf("expected:\n%#v\n", exp.output)
t.Fatalf("%s wasn't decoded as expect.\nGot:\n%s\nExpected:\n%s",
exp.path, decoded, exp.output)
}
}
}
package drum
import (
"encoding/binary"
"io"
"log"
"os"
)
// EncodeFile writes an encoded .splice drum machine file to disk.
func EncodeFile(p Pattern, filename string) error {
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
if err := p.encode(f); err != nil {
return err
}
return nil
}
type errorWriter struct {
io.Writer
err error
}
func (er *errorWriter) Write(p []byte) (n int, err error) {
if er.err != nil {
return 0, er.err
}
n, err = er.Writer.Write(p)
er.err = err
return
}
func (p Pattern) encode(w io.Writer) error {
// get total encoded size of tracks
tenclen := 0
for _, t := range p.Tracks {
tenclen += len(t.Name) + len(t.Steps) + binary.Size(trackHeader{})
}
tenclen += 32 // version string
tenclen += 4 // bpm float
hdr := header{}
copy(hdr.Initial[:], "SPLICE")
hdr.Len = byte(tenclen)
copy(hdr.Version[:], p.Version)
hdr.BPM = p.BPM
var data = []interface{}{
hdr,
}
for _, t := range p.Tracks {
data = append(data, trackHeader{
ID: byte(t.ID),
Len: byte(len(t.Name)),
})
data = append(data, []byte(t.Name))
for _, s := range t.Steps {
if bool(s) {
data = append(data, byte(1))
} else {
data = append(data, byte(0))
}
}
}
log.Printf("%#v", data)
er := &errorWriter{Writer: w}
for _, v := range data {
binary.Write(w, binary.LittleEndian, v)
}
if er.err != nil {
return er.err
}
return nil
}
package drum
import (
"bytes"
"log"
"testing"
)
func TestEncode(t *testing.T) {
p := Pattern{
Version: "0.808-alpha",
BPM: 98.4,
Tracks: []Track{
{
ID: 0,
Name: "kick",
Steps: [16]Note{
true, false, false, false,
true, false, false, false,
true, false, false, false,
true, false, false, false,
},
},
{
ID: 5,
Name: "cowbell",
Steps: [16]Note{
true, false, false, false,
true, false, true, false,
true, false, false, false,
true, false, true, false,
},
},
},
}
var buf bytes.Buffer
if err := p.encode(&buf); err != nil {
t.Fatalf("encoding: %v", err)
}
log.Printf("%#v", buf.Bytes())
log.Printf("%q", buf.Bytes())
p2 := new(Pattern)
if err := p2.decode(&buf); err != nil {
t.Fatalf("decoding: %v", err)
}
log.Printf("%s", p2)
EncodeFile(p, "test.splice")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment