mirror of
https://github.com/vsariola/sointu.git
synced 2025-06-04 01:28:45 -04:00
347 lines
11 KiB
Go
347 lines
11 KiB
Go
package go4k
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func ParseAsm(asmcode string) (*Song, error) {
|
|
paramReg, err := regexp.Compile(`(?:([a-zA-Z]\w*)\s*\(\s*([0-9]+)\s*\)|([0-9]+))`) // matches FOO(42), groups "FOO" and "42" OR just numbers
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error compiling paramReg: %v", err)
|
|
}
|
|
parseParams := func(s string) (map[string]int, []int, error) {
|
|
matches := paramReg.FindAllStringSubmatch(s, 256)
|
|
namedParams := map[string]int{}
|
|
unnamedParams := make([]int, 0)
|
|
for _, match := range matches {
|
|
if match[1] == "" { // the second part of OR fired
|
|
val, err := strconv.Atoi(match[3])
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("error converting %v to integer, which is unexpected as regexp matches only numbers: %v", match[3], err)
|
|
}
|
|
unnamedParams = append(unnamedParams, val)
|
|
} else {
|
|
val, err := strconv.Atoi(match[2])
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("error converting %v to integer, which is unexpected as regexp matches only numbers: %v", match[2], err)
|
|
}
|
|
namedParams[strings.ToLower(match[1])] = val
|
|
}
|
|
}
|
|
return namedParams, unnamedParams, nil
|
|
}
|
|
wordReg, err := regexp.Compile(`\s*([a-zA-Z_][a-zA-Z0-9_]*)([^;\n]*)`) // matches a word and "the rest", until newline or a comment
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
toBytes := func(ints []int) []byte {
|
|
ret := []byte{}
|
|
for _, v := range ints {
|
|
ret = append(ret, byte(v))
|
|
}
|
|
return ret
|
|
}
|
|
var song Song
|
|
scanner := bufio.NewScanner(strings.NewReader(asmcode))
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
macroMatch := wordReg.FindStringSubmatch(line)
|
|
if macroMatch == nil {
|
|
continue
|
|
}
|
|
word, rest := macroMatch[1], macroMatch[2]
|
|
namedParams, unnamedParams, err := parseParams(rest)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing parameters: %v", err)
|
|
}
|
|
switch word {
|
|
case "BEGIN_SONG":
|
|
song = Song{BPM: namedParams["bpm"], Patterns: nil, Tracks: nil, Patch: Patch{}, Output16Bit: namedParams["output_16bit"] == 1, Hold: byte(namedParams["hold"])}
|
|
case "PATTERN":
|
|
song.Patterns = append(song.Patterns, toBytes(unnamedParams))
|
|
case "TRACK":
|
|
song.Tracks = append(song.Tracks, Track{namedParams["voices"], toBytes(unnamedParams)})
|
|
case "BEGIN_INSTRUMENT":
|
|
song.Patch.Instruments = append(song.Patch.Instruments, Instrument{NumVoices: namedParams["voices"], Units: []Unit{}})
|
|
case "DELTIME":
|
|
song.Patch.DelayTimes = append(song.Patch.DelayTimes, unnamedParams...)
|
|
case "SAMPLE_OFFSET":
|
|
song.Patch.SampleOffsets = append(song.Patch.SampleOffsets, SampleOffset{
|
|
Start: namedParams["start"],
|
|
LoopStart: namedParams["loopstart"],
|
|
LoopLength: namedParams["looplength"]})
|
|
}
|
|
if strings.HasPrefix(word, "SU_") {
|
|
unittype := strings.ToLower(word[3:])
|
|
unit := Unit{Type: unittype, Parameters: namedParams}
|
|
lastIndex := len(song.Patch.Instruments) - 1
|
|
if lastIndex < 0 {
|
|
return nil, fmt.Errorf("opcode %v before BEGIN_INSTRUMENT", word)
|
|
}
|
|
song.Patch.Instruments[lastIndex].Units = append(song.Patch.Instruments[lastIndex].Units, unit)
|
|
}
|
|
|
|
}
|
|
return &song, nil
|
|
}
|
|
|
|
func FormatAsm(song *Song) (string, error) {
|
|
paramorder := map[string][]string{
|
|
"add": []string{"stereo"},
|
|
"addp": []string{"stereo"},
|
|
"pop": []string{"stereo"},
|
|
"loadnote": []string{"stereo"},
|
|
"mul": []string{"stereo"},
|
|
"mulp": []string{"stereo"},
|
|
"push": []string{"stereo"},
|
|
"xch": []string{"stereo"},
|
|
"distort": []string{"stereo", "drive"},
|
|
"hold": []string{"stereo", "holdfreq"},
|
|
"crush": []string{"stereo", "resolution"},
|
|
"gain": []string{"stereo", "gain"},
|
|
"invgain": []string{"stereo", "invgain"},
|
|
"filter": []string{"stereo", "frequency", "resonance", "lowpass", "bandpass", "highpass", "negbandpass", "neghighpass"},
|
|
"clip": []string{"stereo"},
|
|
"pan": []string{"stereo", "panning"},
|
|
"delay": []string{"stereo", "pregain", "dry", "feedback", "damp", "delay", "count", "notetracking"},
|
|
"compressor": []string{"stereo", "attack", "release", "invgain", "threshold", "ratio"},
|
|
"speed": []string{},
|
|
"out": []string{"stereo", "gain"},
|
|
"outaux": []string{"stereo", "outgain", "auxgain"},
|
|
"aux": []string{"stereo", "gain", "channel"},
|
|
"send": []string{"stereo", "amount", "voice", "unit", "port", "sendpop"},
|
|
"envelope": []string{"stereo", "attack", "decay", "sustain", "release", "gain"},
|
|
"noise": []string{"stereo", "shape", "gain"},
|
|
"oscillator": []string{"stereo", "transpose", "detune", "phase", "color", "shape", "gain", "type", "lfo", "unison"},
|
|
"loadval": []string{"stereo", "value"},
|
|
"receive": []string{"stereo"},
|
|
"in": []string{"stereo", "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--
|
|
}
|
|
// 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 := 0
|
|
for i, instrument := range song.Patch.Instruments {
|
|
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.Instruments[targetInstrument].Units) {
|
|
return "", fmt.Errorf("INSTRUMENT #%v / SEND #%v target unit %v out of range", i, j, unit.Parameters["unit"])
|
|
}
|
|
if song.Patch.Instruments[targetInstrument].Units[unit.Parameters["unit"]].Type == "delay" && unit.Parameters["port"] == 5 {
|
|
delaymod = 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// The actual printing starts here
|
|
output_16bit := 0
|
|
if song.Output16Bit {
|
|
output_16bit = 1
|
|
}
|
|
println("%%include \"sointu/header.inc\"\n")
|
|
println("BEGIN_SONG BPM(%v),OUTPUT_16BIT(%v),CLIP_OUTPUT(0),DELAY_MODULATION(%v),HOLD(%v)\n", song.BPM, output_16bit, delaymod, song.Hold)
|
|
var patternTable [][]string
|
|
for _, pattern := range song.Patterns {
|
|
row := []string{"PATTERN"}
|
|
for _, v := range pattern {
|
|
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 _, instrument := range song.Patch.Instruments {
|
|
var instrTable [][]string
|
|
for _, unit := range instrument.Units {
|
|
row := []string{fmt.Sprintf("SU_%v", strings.ToUpper(unit.Type))}
|
|
for _, parname := range paramorder[unit.Type] {
|
|
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(song.Patch.DelayTimes) > 0 {
|
|
var delStrTable [][]string
|
|
for _, v := range song.Patch.DelayTimes {
|
|
row := []string{"DELTIME", strconv.Itoa(int(v))}
|
|
delStrTable = append(delStrTable, row)
|
|
}
|
|
println("BEGIN_DELTIMES")
|
|
printTable(align(delStrTable, "lr"))
|
|
println("END_DELTIMES\n")
|
|
}
|
|
if len(song.Patch.SampleOffsets) > 0 {
|
|
var samStrTable [][]string
|
|
for _, v := range song.Patch.SampleOffsets {
|
|
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("END_SONG")
|
|
ret := b.String()
|
|
return ret, nil
|
|
}
|
|
|
|
func CHeader(song *Song, maxSamples int) string {
|
|
template :=
|
|
`// auto-generated by Sointu, editing not recommended
|
|
#ifndef SU_RENDER_H
|
|
#define SU_RENDER_H
|
|
|
|
#define SU_MAX_SAMPLES %v
|
|
#define SU_BUFFER_LENGTH (SU_MAX_SAMPLES*2)
|
|
|
|
#define SU_SAMPLE_RATE 44100
|
|
#define SU_BPM %v
|
|
#define SU_PATTERN_SIZE %v
|
|
#define SU_MAX_PATTERNS %v
|
|
#define SU_TOTAL_ROWS (SU_MAX_PATTERNS*SU_PATTERN_SIZE)
|
|
#define SU_SAMPLES_PER_ROW (SU_SAMPLE_RATE*4*60/(SU_BPM*16))
|
|
|
|
#include <stdint.h>
|
|
#if UINTPTR_MAX == 0xffffffff
|
|
#if defined(__clang__) || defined(__GNUC__)
|
|
#define SU_CALLCONV __attribute__ ((stdcall))
|
|
#elif defined(_WIN32)
|
|
#define SU_CALLCONV __stdcall
|
|
#endif
|
|
#else
|
|
#define SU_CALLCONV
|
|
#endif
|
|
|
|
typedef %v SUsample;
|
|
#define SU_SAMPLE_RANGE %v
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
void SU_CALLCONV su_render_song(SUsample *buffer);
|
|
%v
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
#endif
|
|
`
|
|
maxSamplesText := "SU_TOTAL_ROWS*SU_SAMPLES_PER_ROW"
|
|
if maxSamples > 0 {
|
|
maxSamplesText = fmt.Sprintf("%v", maxSamples)
|
|
}
|
|
sampleType := "float"
|
|
sampleRange := "1.0f"
|
|
if song.Output16Bit {
|
|
sampleType = "short"
|
|
sampleRange = "32768"
|
|
}
|
|
defineGmdls := ""
|
|
for _, instr := range song.Patch.Instruments {
|
|
for _, unit := range instr.Units {
|
|
if unit.Type == "oscillator" && unit.Parameters["type"] == Sample {
|
|
defineGmdls = "\n#define SU_LOAD_GMDLS\nvoid SU_CALLCONV su_load_gmdls(void);"
|
|
break
|
|
}
|
|
}
|
|
}
|
|
header := fmt.Sprintf(template, maxSamplesText, song.BPM, song.PatternRows(), song.SequenceLength(), sampleType, sampleRange, defineGmdls)
|
|
return header
|
|
}
|