Created
March 20, 2015 17:33
-
-
Save paulsmith/09a7c8523b34a3a3f081 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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