-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Jason Crawford
committed
Dec 18, 2020
1 parent
828ae5a
commit 43d80d7
Showing
21 changed files
with
1,182 additions
and
1 deletion.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,3 +13,4 @@ | |
|
||
# Dependency directories (remove the comment below to include it) | ||
# vendor/ | ||
.vscode/ |
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
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`. | |
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
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 |
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
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= |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
package goaudiofile |
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
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 | ||
} |
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
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 | ||
} |
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
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 |
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
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 | ||
} |
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
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 | ||
} |
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
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 | ||
} |
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
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 | ||
} |
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
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 | ||
} |
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
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 | ||
} |
Oops, something went wrong.