I took part of the Go Challenge #1. It was a fun little problem to solve, took me a few hours. Waiting to see the results of the other players to learn some proper Go
This is my submission:
decoder.go
package drum
import (
"bytes"
"encoding/binary"
"encoding/hex"
"fmt"
"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) {
// Open the file
f, ferr := os.Open(path)
defer f.Close()
if ferr != nil {
return decodeError("Error reading file: "+path, ferr)
}
// skip start of file
f.Seek(13, 0)
// check how much of the file we should read to avoid invalid file (see sample 5)
h, e := readNBytesAsHex(1, f)
if e != nil {
return decodeError("Error reading file size: ", e)
}
bytesToRead, _ := strconv.ParseInt(h, 16, 32)
// Read the version
r, e := readNBytes(1, f)
if e != nil {
return decodeError("Error reading version: ", e)
}
bytesToRead--
version := make([]string, 0)
for r[0] != 0 {
version = append(version, string(r))
r, e = readNBytes(1, f)
if e != nil {
return decodeError("Error reading version: ", e)
}
bytesToRead--
}
// Keep track of position to correctly update bytesToRead after
// we'll read the tempo from the .splice
lastPosition, _ := f.Seek(0, 1)
// Go to the tempo location in .splice file
f.Seek(46, 0)
var tempo float32
tempoBytes, e := readNBytes(4, f)
if e != nil {
return decodeError("Error reading tempo: ", e)
}
buf := bytes.NewReader(tempoBytes)
binary.Read(buf, binary.LittleEndian, &tempo)
// Compute how much bytes we skipped between last known position
// and byte ending the tempo data
currentPosition, _ := f.Seek(0, 1)
bytesToRead -= 4 + (currentPosition - lastPosition + 2)
// Now that we've got the version & tempo, init our Pattern
pattern := NewPattern(strings.Join(version, ""), tempo)
// iterates on tracks until we reach our limit to read
for bytesToRead > 0 {
// Retrieve the track id
b, e := readNBytesAsHex(1, f)
if e != nil {
return decodeError("Error reading track id: ", e)
}
id, _ := strconv.ParseInt(b, 16, 32)
bytesToRead -= 3
f.Seek(3, 1) // skip 3 bytes
// Read the track name and its length
s, e := readNBytes(1, f)
if e != nil {
return decodeError("Error reading track name (length): ", e)
}
trackNameLength := readIntFromHex(s)
name, e := readNBytes(trackNameLength, f)
if e != nil {
return decodeError("Error reading track name: ", e)
}
bytesToRead -= int64(trackNameLength + 1)
// Read the sequence
sequence, e := readNBytes(16, f)
if e != nil {
return decodeError("Error reading track sequence: ", e)
}
bytesToRead -= 16
// add the current track to our Pattern
track := NewTrack(id, string(name), sequence)
pattern.AddTrack(track)
}
// ensure Close
f.Close()
return pattern, nil
}
// Struct representing a Track inside a Splice
type Track struct {
number int64
name string
sequence string
}
// Pattern is the high level representation of the
// drum pattern contained in a .splice file.
type Pattern struct {
version string
tempo float32
tracks []Track
}
// decodeError returns a nil Pattern and a custom error
func decodeError(label string, e error) (*Pattern, error) {
return nil, fmt.Errorf(label+"\n\t%#v", e)
}
// readIntFromHex reads an integer from hexadecimal representation of bytes
func readIntFromHex(data []byte) int {
x := hex.EncodeToString(data)
i, _ := strconv.Atoi(x)
return i
}
// readNBytes reads `n` bytes from the file `f`
func readNBytes(n int, f *os.File) ([]byte, error) {
bytesToRead := make([]byte, n)
_, e := f.Read(bytesToRead)
return bytesToRead, e
}
// readNBytesAsHex reads `n` byte from the file `f` as hexadecimal
func readNBytesAsHex(n int, f *os.File) (string, error) {
bytes, e := readNBytes(n, f)
if e != nil {
return "", e
} else {
return hex.EncodeToString(bytes), nil
}
}
drum.go
package drum
import (
"encoding/hex"
"fmt"
"regexp"
"strings"
)
// NewTrack creates a new track instance from its id, name and sequence
func NewTrack(id int64, name string, seq []byte) *Track {
sequence := strings.Replace(strings.Replace(hex.EncodeToString(seq), "01", "x", 100), "00", "-", 100)
return &Track{id, name, string(sequence)}
}
// String formats a Track as a String where steps are grouped by 4 and separated with a pipe character
func (t *Track) String() string {
reg, _ := regexp.Compile("(....)")
return fmt.Sprintf("(%d) %s\t%s", t.number, t.name, reg.ReplaceAllString(t.sequence, "|$1")+"|")
}
// NewPattern creates a new Pattern instance from its Splice version and tempo
func NewPattern(version string, tempo float32) *Pattern {
return &Pattern{version, tempo, make([]Track, 0)}
}
// AddTrack adds a Track to a Pattern
func (p *Pattern) AddTrack(track *Track) {
p.tracks = append(p.tracks, *track)
}
// String formats a Pattern as a String using the challenge specified format
func (p *Pattern) String() string {
str := fmt.Sprintf("Saved with HW Version: %s\nTempo: %g\n", p.version, p.tempo)
for _, track := range p.tracks {
str += track.String() + "\n"
}
return str
}
Or at this URL: https://gist.github.com/agrison/e3662bbdbed349d776af
Cheers!