March 6, 2015

Go Challenge #1

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!

Alexandre Grison - //grison.me - @algrison