mirror of
https://github.com/vsariola/sointu.git
synced 2025-06-04 01:28:45 -04:00
feat(go4k): Implement .asm exporting.
This commit is contained in:
parent
5ceab766cc
commit
377132321f
@ -131,3 +131,36 @@ func ConstructDelayTimeTable(patch Patch) ([]int, [][]int) {
|
|||||||
}
|
}
|
||||||
return delayTable, unitindices
|
return delayTable, unitindices
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConstructSampleOffsetTable collects the sample offests from
|
||||||
|
// all sample-based oscillators and collects them in a table,
|
||||||
|
// so that they appear in the table only once. Returns the collected
|
||||||
|
// table and [][]int array where element [i][j] is the index in the
|
||||||
|
// table by instrument i / unit j (units other than sample oscillators
|
||||||
|
// have the value 0)
|
||||||
|
func ConstructSampleOffsetTable(patch Patch) ([]SampleOffset, [][]int) {
|
||||||
|
unitindices := make([][]int, len(patch))
|
||||||
|
var offsetTable []SampleOffset
|
||||||
|
offsetMap := map[SampleOffset]int{}
|
||||||
|
for i, instr := range patch {
|
||||||
|
unitindices[i] = make([]int, len(instr.Units))
|
||||||
|
for j, unit := range instr.Units {
|
||||||
|
if unit.Type == "oscillator" && unit.Parameters["type"] == Sample {
|
||||||
|
offset := SampleOffset{
|
||||||
|
Start: unit.Parameters["start"],
|
||||||
|
LoopStart: unit.Parameters["loopstart"],
|
||||||
|
LoopLength: unit.Parameters["looplength"],
|
||||||
|
}
|
||||||
|
if ind, ok := offsetMap[offset]; ok {
|
||||||
|
unitindices[i][j] = ind // the sample has been already added to table, reuse the index
|
||||||
|
} else {
|
||||||
|
ind = len(offsetTable)
|
||||||
|
unitindices[i][j] = ind
|
||||||
|
offsetMap[offset] = ind
|
||||||
|
offsetTable = append(offsetTable, offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return offsetTable, unitindices
|
||||||
|
}
|
||||||
|
@ -4,15 +4,14 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ParseAsm(reader io.Reader) (*Song, error) {
|
func DeserializeAsm(asmcode string) (*Song, error) {
|
||||||
var bpm int
|
var bpm int
|
||||||
scanner := bufio.NewScanner(reader)
|
scanner := bufio.NewScanner(strings.NewReader(asmcode))
|
||||||
patterns := make([][]byte, 0)
|
patterns := make([][]byte, 0)
|
||||||
tracks := make([]Track, 0)
|
tracks := make([]Track, 0)
|
||||||
var patch Patch
|
var patch Patch
|
||||||
@ -186,3 +185,227 @@ func ParseAsm(reader io.Reader) (*Song, error) {
|
|||||||
s := Song{BPM: bpm, Patterns: patterns, Tracks: tracks, Patch: patch, SongLength: -1}
|
s := Song{BPM: bpm, Patterns: patterns, Tracks: tracks, Patch: patch, SongLength: -1}
|
||||||
return &s, nil
|
return &s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SerializeAsm(song *Song) (string, error) {
|
||||||
|
paramorder := map[string][]string{
|
||||||
|
"add": []string{},
|
||||||
|
"addp": []string{},
|
||||||
|
"pop": []string{},
|
||||||
|
"loadnote": []string{},
|
||||||
|
"mul": []string{},
|
||||||
|
"mulp": []string{},
|
||||||
|
"push": []string{},
|
||||||
|
"xch": []string{},
|
||||||
|
"distort": []string{"drive"},
|
||||||
|
"hold": []string{"holdfreq"},
|
||||||
|
"crush": []string{"resolution"},
|
||||||
|
"gain": []string{"gain"},
|
||||||
|
"invgain": []string{"invgain"},
|
||||||
|
"filter": []string{"frequency", "resonance", "lowpass", "bandpass", "highpass", "negbandpass", "neghighpass"},
|
||||||
|
"clip": []string{},
|
||||||
|
"pan": []string{"panning"},
|
||||||
|
"delay": []string{"pregain", "dry", "feedback", "damp", "delay", "count", "notetracking"},
|
||||||
|
"compressor": []string{"attack", "release", "invgain", "threshold", "ratio"},
|
||||||
|
"speed": []string{},
|
||||||
|
"out": []string{"gain"},
|
||||||
|
"outaux": []string{"outgain", "auxgain"},
|
||||||
|
"aux": []string{"gain", "channel"},
|
||||||
|
"send": []string{"amount", "voice", "unit", "port", "sendpop"},
|
||||||
|
"envelope": []string{"attack", "decay", "sustain", "release", "gain"},
|
||||||
|
"noise": []string{"shape", "gain"},
|
||||||
|
"oscillator": []string{"transpose", "detune", "phase", "color", "shape", "gain", "type", "lfo", "unison"},
|
||||||
|
"loadval": []string{"value"},
|
||||||
|
"receive": []string{},
|
||||||
|
"in": []string{"channel"},
|
||||||
|
}
|
||||||
|
indentation := 0
|
||||||
|
indent := func() string {
|
||||||
|
return strings.Repeat(" ", indentation*4)
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
println := func(format string, params ...interface{}) {
|
||||||
|
if len(format) > 0 {
|
||||||
|
fmt.Fprintf(&b, "%v", indent())
|
||||||
|
fmt.Fprintf(&b, format, params...)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&b, "\n")
|
||||||
|
}
|
||||||
|
align := func(table [][]string, format string) [][]string {
|
||||||
|
var maxwidth []int
|
||||||
|
// find the maximum width of each column
|
||||||
|
for _, row := range table {
|
||||||
|
for k, elem := range row {
|
||||||
|
l := len(elem)
|
||||||
|
if len(maxwidth) <= k {
|
||||||
|
maxwidth = append(maxwidth, l)
|
||||||
|
} else {
|
||||||
|
if maxwidth[k] < l {
|
||||||
|
maxwidth[k] = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// align each column, depending on the specified formatting
|
||||||
|
for _, row := range table {
|
||||||
|
for k, elem := range row {
|
||||||
|
l := len(elem)
|
||||||
|
var f byte
|
||||||
|
if k >= len(format) {
|
||||||
|
f = format[len(format)-1] // repeat the last format specifier for all remaining columns
|
||||||
|
} else {
|
||||||
|
f = format[k]
|
||||||
|
}
|
||||||
|
switch f {
|
||||||
|
case 'n': // no alignment
|
||||||
|
row[k] = elem
|
||||||
|
case 'l': // left align
|
||||||
|
row[k] = elem + strings.Repeat(" ", maxwidth[k]-l)
|
||||||
|
case 'r': // right align
|
||||||
|
row[k] = strings.Repeat(" ", maxwidth[k]-l) + elem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
printTable := func(table [][]string) {
|
||||||
|
indentation++
|
||||||
|
for _, row := range table {
|
||||||
|
println("%v %v", row[0], strings.Join(row[1:], ","))
|
||||||
|
}
|
||||||
|
indentation--
|
||||||
|
}
|
||||||
|
delayTable, delayIndices := ConstructDelayTimeTable(song.Patch)
|
||||||
|
sampleTable, sampleIndices := ConstructSampleOffsetTable(song.Patch)
|
||||||
|
// The actual printing starts here
|
||||||
|
println("%%define BPM %d", song.BPM)
|
||||||
|
// delay modulation is pretty much the only %define that the asm preprocessor cannot figure out
|
||||||
|
// as the preprocessor has no clue if a SEND modulates a delay unit. So, unfortunately, for the
|
||||||
|
// time being, we need to figure during export if INCLUDE_DELAY_MODULATION needs to be defined.
|
||||||
|
delaymod := false
|
||||||
|
for i, instrument := range song.Patch {
|
||||||
|
for j, unit := range instrument.Units {
|
||||||
|
if unit.Type == "send" {
|
||||||
|
targetInstrument := i
|
||||||
|
if unit.Parameters["voice"] > 0 {
|
||||||
|
v, err := song.Patch.InstrumentForVoice(unit.Parameters["voice"] - 1)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("INSTRUMENT #%v / SEND #%v targets voice %v, which does not exist", i, j, unit.Parameters["voice"])
|
||||||
|
}
|
||||||
|
targetInstrument = v
|
||||||
|
}
|
||||||
|
if unit.Parameters["unit"] < 0 || unit.Parameters["unit"] >= len(song.Patch[targetInstrument].Units) {
|
||||||
|
return "", fmt.Errorf("INSTRUMENT #%v / SEND #%v target unit %v out of range", i, j, unit.Parameters["unit"])
|
||||||
|
}
|
||||||
|
if song.Patch[targetInstrument].Units[unit.Parameters["unit"]].Type == "delay" && unit.Parameters["port"] == 5 {
|
||||||
|
delaymod = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if delaymod {
|
||||||
|
println("%%define INCLUDE_DELAY_MODULATION")
|
||||||
|
}
|
||||||
|
println("")
|
||||||
|
println("%%include \"sointu/header.inc\"\n")
|
||||||
|
var patternTable [][]string
|
||||||
|
for _, pattern := range song.Patterns {
|
||||||
|
row := []string{"PATTERN"}
|
||||||
|
for _, v := range pattern {
|
||||||
|
if v == 1 {
|
||||||
|
row = append(row, "HLD")
|
||||||
|
} else {
|
||||||
|
row = append(row, strconv.Itoa(int(v)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
patternTable = append(patternTable, row)
|
||||||
|
}
|
||||||
|
println("BEGIN_PATTERNS")
|
||||||
|
printTable(align(patternTable, "lr"))
|
||||||
|
println("END_PATTERNS\n")
|
||||||
|
var trackTable [][]string
|
||||||
|
for _, track := range song.Tracks {
|
||||||
|
row := []string{"TRACK", fmt.Sprintf("VOICES(%d)", track.NumVoices)}
|
||||||
|
for _, v := range track.Sequence {
|
||||||
|
row = append(row, strconv.Itoa(int(v)))
|
||||||
|
}
|
||||||
|
trackTable = append(trackTable, row)
|
||||||
|
}
|
||||||
|
println("BEGIN_TRACKS")
|
||||||
|
printTable(align(trackTable, "lr"))
|
||||||
|
println("END_TRACKS\n")
|
||||||
|
println("BEGIN_PATCH")
|
||||||
|
indentation++
|
||||||
|
for i, instrument := range song.Patch {
|
||||||
|
var instrTable [][]string
|
||||||
|
for j, unit := range instrument.Units {
|
||||||
|
stereomono := "MONO"
|
||||||
|
if unit.Stereo {
|
||||||
|
stereomono = "STEREO"
|
||||||
|
}
|
||||||
|
row := []string{fmt.Sprintf("SU_%v", strings.ToUpper(unit.Type)), stereomono}
|
||||||
|
for _, parname := range paramorder[unit.Type] {
|
||||||
|
if unit.Type == "oscillator" && unit.Parameters["type"] == Sample && parname == "color" {
|
||||||
|
row = append(row, fmt.Sprintf("COLOR(%v)", strconv.Itoa(sampleIndices[i][j])))
|
||||||
|
} else if unit.Type == "delay" && parname == "count" {
|
||||||
|
count := len(unit.DelayTimes)
|
||||||
|
if unit.Stereo {
|
||||||
|
count /= 2
|
||||||
|
}
|
||||||
|
row = append(row, fmt.Sprintf("COUNT(%v)", strconv.Itoa(count)))
|
||||||
|
} else if unit.Type == "delay" && parname == "delay" {
|
||||||
|
row = append(row, fmt.Sprintf("DELAY(%v)", strconv.Itoa(delayIndices[i][j])))
|
||||||
|
} else if unit.Type == "oscillator" && parname == "type" {
|
||||||
|
switch unit.Parameters["type"] {
|
||||||
|
case Sine:
|
||||||
|
row = append(row, "TYPE(SINE)")
|
||||||
|
case Trisaw:
|
||||||
|
row = append(row, "TYPE(TRISAW)")
|
||||||
|
case Pulse:
|
||||||
|
row = append(row, "TYPE(PULSE)")
|
||||||
|
case Gate:
|
||||||
|
row = append(row, "TYPE(GATE)")
|
||||||
|
case Sample:
|
||||||
|
row = append(row, "TYPE(SAMPLE)")
|
||||||
|
}
|
||||||
|
} else if v, ok := unit.Parameters[parname]; ok {
|
||||||
|
row = append(row, fmt.Sprintf("%v(%v)", strings.ToUpper(parname), strconv.Itoa(int(v))))
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("The parameter map for unit %v does not contain %v, even though it should", unit.Type, parname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
instrTable = append(instrTable, row)
|
||||||
|
}
|
||||||
|
println("BEGIN_INSTRUMENT VOICES(%d)", instrument.NumVoices)
|
||||||
|
printTable(align(instrTable, "ln"))
|
||||||
|
println("END_INSTRUMENT")
|
||||||
|
}
|
||||||
|
indentation--
|
||||||
|
println("END_PATCH\n")
|
||||||
|
if len(delayTable) > 0 {
|
||||||
|
var delStrTable [][]string
|
||||||
|
for _, v := range delayTable {
|
||||||
|
row := []string{"DELTIME", strconv.Itoa(int(v))}
|
||||||
|
delStrTable = append(delStrTable, row)
|
||||||
|
}
|
||||||
|
println("BEGIN_DELTIMES")
|
||||||
|
printTable(align(delStrTable, "lr"))
|
||||||
|
println("END_DELTIMES\n")
|
||||||
|
}
|
||||||
|
if len(sampleTable) > 0 {
|
||||||
|
var samStrTable [][]string
|
||||||
|
for _, v := range sampleTable {
|
||||||
|
samStrTable = append(samStrTable, []string{
|
||||||
|
"SAMPLE_OFFSET",
|
||||||
|
fmt.Sprintf("START(%d)", v.Start),
|
||||||
|
fmt.Sprintf("LOOPSTART(%d)", v.LoopStart),
|
||||||
|
fmt.Sprintf("LOOPLENGTH(%d)", v.LoopLength),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
println("BEGIN_SAMPLE_OFFSETS")
|
||||||
|
printTable(align(samStrTable, "r"))
|
||||||
|
println("END_SAMPLE_OFFSETS\n")
|
||||||
|
}
|
||||||
|
println("%%include \"sointu/footer.inc\"")
|
||||||
|
ret := b.String()
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
@ -28,11 +28,11 @@ func TestAllAsmFiles(t *testing.T) {
|
|||||||
basename := filepath.Base(filename)
|
basename := filepath.Base(filename)
|
||||||
testname := strings.TrimSuffix(basename, path.Ext(basename))
|
testname := strings.TrimSuffix(basename, path.Ext(basename))
|
||||||
t.Run(testname, func(t *testing.T) {
|
t.Run(testname, func(t *testing.T) {
|
||||||
file, err := os.Open(filename)
|
asmcode, err := ioutil.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("cannot read the .asm file: %v", filename)
|
t.Fatalf("cannot read the .asm file: %v", filename)
|
||||||
}
|
}
|
||||||
song, err := go4k.ParseAsm(file)
|
song, err := go4k.DeserializeAsm(string(asmcode))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("could not parse the .asm file: %v", err)
|
t.Fatalf("could not parse the .asm file: %v", err)
|
||||||
}
|
}
|
||||||
@ -70,6 +70,67 @@ func TestAllAsmFiles(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSerializingAllAsmFiles(t *testing.T) {
|
||||||
|
bridge.Init()
|
||||||
|
_, myname, _, _ := runtime.Caller(0)
|
||||||
|
files, err := filepath.Glob(path.Join(path.Dir(myname), "..", "tests", "*.asm"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cannot glob files in the test directory: %v", err)
|
||||||
|
}
|
||||||
|
for _, filename := range files {
|
||||||
|
basename := filepath.Base(filename)
|
||||||
|
testname := strings.TrimSuffix(basename, path.Ext(basename))
|
||||||
|
t.Run(testname, func(t *testing.T) {
|
||||||
|
asmcode, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cannot read the .asm file: %v", filename)
|
||||||
|
}
|
||||||
|
song, err := go4k.DeserializeAsm(string(asmcode)) // read the asm
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not parse the .asm file: %v", err)
|
||||||
|
}
|
||||||
|
str, err := go4k.SerializeAsm(song) // serialize again
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not serialize asm file: %v", err)
|
||||||
|
}
|
||||||
|
song, err = go4k.DeserializeAsm(str) // deserialize again. The rendered song should still give same results.
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not parse the serialized asm code: %v", err)
|
||||||
|
}
|
||||||
|
synth, err := bridge.Synth(song.Patch)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Compiling patch failed: %v", err)
|
||||||
|
}
|
||||||
|
buffer, err := go4k.Play(synth, *song)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Play failed: %v", err)
|
||||||
|
}
|
||||||
|
if os.Getenv("GO4K_TEST_SAVE_OUTPUT") == "YES" {
|
||||||
|
outputpath := path.Join(path.Dir(myname), "actual_output")
|
||||||
|
if _, err := os.Stat(outputpath); os.IsNotExist(err) {
|
||||||
|
os.Mkdir(outputpath, 0755)
|
||||||
|
}
|
||||||
|
outFileName := path.Join(path.Dir(myname), "actual_output", testname+".raw")
|
||||||
|
outfile, err := os.OpenFile(outFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
|
||||||
|
defer outfile.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Creating file failed: %v", err)
|
||||||
|
}
|
||||||
|
var createdbuf bytes.Buffer
|
||||||
|
err = binary.Write(&createdbuf, binary.LittleEndian, buffer)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error converting buffer: %v", err)
|
||||||
|
}
|
||||||
|
_, err = outfile.Write(createdbuf.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compareToRaw(t, buffer, testname+".raw")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func compareToRaw(t *testing.T, buffer []float32, rawname string) {
|
func compareToRaw(t *testing.T, buffer []float32, rawname string) {
|
||||||
_, filename, _, _ := runtime.Caller(0)
|
_, filename, _, _ := runtime.Caller(0)
|
||||||
expectedb, err := ioutil.ReadFile(path.Join(path.Dir(filename), "..", "tests", "expected_output", rawname))
|
expectedb, err := ioutil.ReadFile(path.Join(path.Dir(filename), "..", "tests", "expected_output", rawname))
|
||||||
|
20
go4k/go4k.go
20
go4k/go4k.go
@ -38,6 +38,20 @@ func (p Patch) TotalVoices() int {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (patch Patch) InstrumentForVoice(voice int) (int, error) {
|
||||||
|
if voice < 0 {
|
||||||
|
return 0, errors.New("voice cannot be negative")
|
||||||
|
}
|
||||||
|
for i, instr := range patch {
|
||||||
|
if voice < instr.NumVoices {
|
||||||
|
return i, nil
|
||||||
|
} else {
|
||||||
|
voice -= instr.NumVoices
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, errors.New("voice number is beyond the total voices of an instrument")
|
||||||
|
}
|
||||||
|
|
||||||
type Track struct {
|
type Track struct {
|
||||||
NumVoices int
|
NumVoices int
|
||||||
Sequence []byte
|
Sequence []byte
|
||||||
@ -57,6 +71,12 @@ func Render(synth Synth, buffer []float32) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SampleOffset struct {
|
||||||
|
Start int
|
||||||
|
LoopStart int
|
||||||
|
LoopLength int
|
||||||
|
}
|
||||||
|
|
||||||
// UnitParameter documents one parameter that an unit takes
|
// UnitParameter documents one parameter that an unit takes
|
||||||
type UnitParameter struct {
|
type UnitParameter struct {
|
||||||
Name string // thould be found with this name in the Unit.Parameters map
|
Name string // thould be found with this name in the Unit.Parameters map
|
||||||
|
Loading…
x
Reference in New Issue
Block a user