Skip to content

Commit

Permalink
Initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Jason Crawford committed Dec 18, 2020
1 parent 828ae5a commit 43d80d7
Show file tree
Hide file tree
Showing 21 changed files with 1,182 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@

# Dependency directories (remove the comment below to include it)
# vendor/
.vscode/
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,21 @@
# goaudiofile
goaudiofile is a pure Go audio file support library

## What is it?

It's a music and audio file format support library for Go.

## What formats does it support?

| Subfolder | Name | Notes |
|-----------|------|-------|
| `s3m` | Scream Tracker 3 Module | Based on the format described in `TECH.DOC`, originally supplied with the Scream Tracker 3 application, by Sami Tammilehto / FutureCrew |
| `mod` | Protracker / Fast Tracker Module | Based on the format described in `FMODDOC.TXT`, originally supplied with the FireMOD 1.06 source code distribution, by Brett Paterson / FireLight. In order to stay free of copyright concerns (FireLight still operates and maintains FMOD / FireMOD), the associated FireMOD source code was not referenced during the creation of this library. Any similarities of this library to the FireMOD source code is purely accidental and coincidental. |

## Bugs

### Known Bugs

| Tags | Notes |
|------|-------|
| `s3m` | The technical document describing the S3M format has many errors and inconsistencies that have been speculated and argued over by many experts in the field for many decades. This implementation attempts to use the least troublesome representation of each point, where possible. As a result, the data obtained from a format read with this library might not produce a 100% accurate-to-ST3 result. |
| `mod` | If you thought `s3m` was a truly-inconsistent format, then you obviously haven't met its older brother, the Protracker/FastTracker `mod`. |
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/heucuva/goaudiofile

go 1.15

require github.com/pkg/errors v0.9.1
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
1 change: 1 addition & 0 deletions goaudiofile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package goaudiofile
13 changes: 13 additions & 0 deletions internal/util/string.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package util

import "bytes"

// GetString converts a fixed length byte array with embedded nulls into a string
func GetString(data []byte) string {
n := bytes.Index(data, []byte{0})
if n == -1 {
n = len(data)
}
s := string(data[:n])
return s
}
36 changes: 36 additions & 0 deletions music/tracked/mod/fasttracker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package mod

import (
"encoding/binary"
"errors"
"io"
)

type fmtFT struct {
formatIntf
}

var (
fasttracker = &fmtFT{}
)

func (f *fmtFT) readPattern(ffmt *modFormatDetails, r io.Reader) (*Pattern, error) {
if r == nil {
return nil, errors.New("r is nil")
}

p := NewPattern(ffmt.channels)
for _, row := range p {
for c := 0; c < ffmt.channels; c++ {
if err := binary.Read(r, binary.LittleEndian, &row[c]); err != nil {
return nil, err
}
}
}

return &p, nil
}

func (f *fmtFT) rectifyOrderList(ffmt *modFormatDetails, in [128]uint8) ([128]uint8, error) {
return in, nil
}
14 changes: 14 additions & 0 deletions music/tracked/mod/instheader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package mod

// InstrumentHeader is a representation of the MOD file instrument header
type InstrumentHeader struct {
Name [22]byte
Len WordLength
FineTune uint8
Volume uint8
LoopStart WordLength
LoopEnd WordLength
}

// SampleData is the data associated to the instrument
type SampleData []uint8
118 changes: 118 additions & 0 deletions music/tracked/mod/mod.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package mod

import (
"encoding/binary"
"errors"
"io"

"github.com/heucuva/goaudiofile/internal/util"
)

// File is an S3M internal file representation
type File struct {
Head ModuleHeader
Patterns []Pattern
Instruments []SampleData
}

type formatIntf interface {
readPattern(*modFormatDetails, io.Reader) (*Pattern, error)
rectifyOrderList(*modFormatDetails, [128]uint8) ([128]uint8, error)
}

type modFormatDetails struct {
sig string
channels int
format formatIntf
}

var (
sigChannels = [...]modFormatDetails{
// amiga noisetracker / protracker
{"M.K.", 4, protracker}, {"M!K!", 4, protracker},
// startracker (startrekker?)
{"FLT4", 4, startrekker}, {"FLT8", 8, startrekker},
// fasttracker
{"2CHN", 2, fasttracker}, {"4CHN", 4, fasttracker},
{"6CHN", 6, fasttracker}, {"8CHN", 8, fasttracker},
// fasttracker 2
{"10CH", 10, fasttracker}, {"11CH", 11, fasttracker},
{"12CH", 12, fasttracker}, {"13CH", 13, fasttracker},
{"14CH", 14, fasttracker}, {"15CH", 15, fasttracker},
{"16CH", 16, fasttracker}, {"17CH", 17, fasttracker},
{"18CH", 18, fasttracker}, {"19CH", 19, fasttracker},
{"20CH", 20, fasttracker}, {"21CH", 21, fasttracker},
{"22CH", 22, fasttracker}, {"23CH", 23, fasttracker},
{"24CH", 24, fasttracker}, {"25CH", 25, fasttracker},
{"26CH", 26, fasttracker}, {"27CH", 27, fasttracker},
{"28CH", 28, fasttracker}, {"29CH", 29, fasttracker},
{"30CH", 30, fasttracker}, {"31CH", 31, fasttracker},
{"32CH", 32, fasttracker},
}
)

// Read reads a MOD file from the reader `r` and creates an internal MOD File representation
func Read(r io.Reader) (*File, error) {
f := File{}

if err := binary.Read(r, binary.LittleEndian, &f.Head); err != nil {
return nil, err
}

sig := util.GetString(f.Head.Sig[:])
var ffmt *modFormatDetails
for _, s := range sigChannels {
if s.sig == sig {
ffmt = &s
break
}
}

if ffmt == nil || ffmt.channels == 0 {
return nil, errors.New("invalid file format")
}

processor := ffmt.format
if processor == nil {
return nil, errors.New("could not identify format reader")
}

numPatterns := 0
orderList, err := processor.rectifyOrderList(ffmt, f.Head.Order)
if err != nil {
return nil, err
}
for i, o := range orderList {
if i < int(f.Head.SongLen) {
f.Head.Order[i] = o
}
// we count all patterns, even if we're not in the 'song' range
// hidden/'deleted' patterns can exist...
if numPatterns <= int(o) {
numPatterns = int(o) + 1
}
}

f.Patterns = make([]Pattern, numPatterns)
for i := 0; i < numPatterns; i++ {
pattern, err := processor.readPattern(ffmt, r)
if err != nil {
return nil, err
}
if pattern == nil {
continue
}
f.Patterns[i] = *pattern
}

f.Instruments = make([]SampleData, len(f.Head.Samples))
for instNum, inst := range f.Head.Samples {
samp := make([]byte, inst.Len.Value())
if err := binary.Read(r, binary.LittleEndian, &samp); err != nil {
return nil, err
}
f.Instruments[instNum] = samp
}

return &f, nil
}
11 changes: 11 additions & 0 deletions music/tracked/mod/moduleheader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package mod

// ModuleHeader is a representation of the MOD file header
type ModuleHeader struct {
Name [20]byte
Samples [31]InstrumentHeader
SongLen uint8
RestartPos uint8
Order [128]uint8
Sig [4]uint8
}
45 changes: 45 additions & 0 deletions music/tracked/mod/pattern.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package mod

// Channel is a representation of the MOD file pattern channel bitfield
type Channel [4]uint8

// Period is a coefficient used in the calculation of an instrument's variable playback frequency (i.e.: a note)
type Period uint16

// Instrument returns the instrument number for this pattern channel
func (p Channel) Instrument() uint8 {
return (p[0] & 0xF0) | (p[2] >> 4)
}

// Period returns the note period for this pattern channel
func (p Channel) Period() Period {
return (Period(p[0]&0x0F) << 8) | Period(p[1])
}

// Effect returns the effect value for this pattern channel
func (p Channel) Effect() uint8 {
return (p[2] & 0x0F)
}

// EffectParameter returns the effect parameter value for this pattern channel
func (p Channel) EffectParameter() uint8 {
return p[3]
}

// Row is an array of all channels for a particular pattern row
type Row []Channel

// Pattern is a representation of a MOD file's single pattern
type Pattern [64]Row

// NewPattern creates a new pattern with a number of channels equal to the `channels` parameter
func NewPattern(channels int) Pattern {
p := Pattern{}

for r := range p {
row := make(Row, channels)
p[r] = row
}

return p
}
36 changes: 36 additions & 0 deletions music/tracked/mod/protracker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package mod

import (
"encoding/binary"
"errors"
"io"
)

type fmtPT struct {
formatIntf
}

var (
protracker = &fmtPT{}
)

func (f *fmtPT) readPattern(ffmt *modFormatDetails, r io.Reader) (*Pattern, error) {
if r == nil {
return nil, errors.New("r is nil")
}

p := NewPattern(ffmt.channels)
for _, row := range p {
for c := 0; c < ffmt.channels; c++ {
if err := binary.Read(r, binary.LittleEndian, &row[c]); err != nil {
return nil, err
}
}
}

return &p, nil
}

func (f *fmtPT) rectifyOrderList(ffmt *modFormatDetails, in [128]uint8) ([128]uint8, error) {
return in, nil
}
54 changes: 54 additions & 0 deletions music/tracked/mod/startrekker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package mod

import (
"encoding/binary"
"errors"
"io"
)

type fmtST struct {
formatIntf
}

var (
startrekker = &fmtST{}
)

func (f *fmtST) readPattern(ffmt *modFormatDetails, r io.Reader) (*Pattern, error) {
if r == nil {
return nil, errors.New("r is nil")
}

p := NewPattern(ffmt.channels)
for _, row := range p {
for c := 0; c < 4; c++ {
if err := binary.Read(r, binary.LittleEndian, &row[c]); err != nil {
return nil, err
}
}
}
// weird format...
if ffmt.channels == 8 {
for _, row := range p {
for c := 4; c < 8; c++ {
if err := binary.Read(r, binary.LittleEndian, &row[c]); err != nil {
return nil, err
}
}
}
}

return &p, nil
}

func (f *fmtST) rectifyOrderList(ffmt *modFormatDetails, in [128]uint8) ([128]uint8, error) {
// really weird format...
if ffmt.channels == 8 {
out := [128]uint8{}
for i, o := range in {
out[i] = o / 2
}
return out, nil
}
return in, nil
}
12 changes: 12 additions & 0 deletions music/tracked/mod/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package mod

import "syscall"

// WordLength is a count of WORD (uint16) sized values, stored in BigEndian format
type WordLength uint16

// Value returns the actual length described by this WordLength
func (m WordLength) Value() int {
v := syscall.Ntohs(uint16(m))
return int(v) << 1
}
Loading

0 comments on commit 43d80d7

Please sign in to comment.