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 #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 }