feat(asm&go4k): Rewrote both library & player to use text/template compiler

There is no more plain .asms, both library & player are created from the templates using go text/template package.
This commit is contained in:
Veikko Sariola 2020-12-14 15:46:12 +02:00
parent 2ad61ff6b2
commit d0bd877b3f
141 changed files with 1195 additions and 5542 deletions

View File

@ -34,17 +34,38 @@ enable_language(ASM_NASM)
# By putting them there, we can pass the same compile definitions to C and ASM
set(CMAKE_ASM_NASM_COMPILE_OBJECT "<CMAKE_ASM_NASM_COMPILER> <INCLUDES> <DEFINES> <FLAGS> -f ${CMAKE_ASM_NASM_OBJECT_FORMAT} -o <OBJECT> <SOURCE>")
# Sointu as a header-only library
set(HEADERLIB sointuinterface)
add_library(${HEADERLIB} INTERFACE)
target_include_directories(${HEADERLIB} INTERFACE ${PROJECT_SOURCE_DIR}/include)
if(WIN32)
set(sointuexe ${CMAKE_CURRENT_BINARY_DIR}/sointu-cli.exe)
else()
set(sointuexe ${CMAKE_CURRENT_BINARY_DIR}/sointu-cli)
endif()
# the tests include the entire ASM but we still want to rebuild when they change
file(GLOB templates ${PROJECT_SOURCE_DIR}/templates/*.asm)
file(GLOB go4k "${PROJECT_SOURCE_DIR}/go4k/*.go" "${PROJECT_SOURCE_DIR}/go4k/cmd/sointu-cli/*.go")
file(GLOB compilersrc "${PROJECT_SOURCE_DIR}/go4k/compiler/*.go")
if(DEFINED CMAKE_C_SIZEOF_DATA_PTR AND CMAKE_C_SIZEOF_DATA_PTR EQUAL 8)
set(arch "amd64")
elseif(DEFINED CMAKE_CXX_SIZEOF_DATA_PTR AND CMAKE_CXX_SIZEOF_DATA_PTR EQUAL 8)
set(arch "amd64")
else()
set(arch "386")
endif()
# Sointu as static library
set(STATICLIB sointu)
add_library(${STATICLIB} render.asm)
set(sointuasm sointu.asm)
add_custom_command(
OUTPUT ${sointuasm}
COMMAND go run ${PROJECT_SOURCE_DIR}/go4k/compiler/generate.go -arch=${arch} ${CMAKE_CURRENT_BINARY_DIR}
DEPENDS "${templates}" "${go4k}" "${compilersrc}"
)
add_library(${STATICLIB} ${sointuasm})
set_target_properties(${STATICLIB} PROPERTIES LINKER_LANGUAGE C)
target_link_libraries(${STATICLIB} ${HEADERLIB})
target_compile_definitions(${STATICLIB} PUBLIC SU_USE_INTROSPECTION RUNTIME_TABLES)
target_include_directories(${STATICLIB} INTERFACE ${CMAKE_CURRENT_BINARY_DIR})
# We should put examples here
# add_subdirectory(examples)

1
go.mod
View File

@ -12,5 +12,6 @@ require (
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.11 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
gopkg.in/yaml.v2 v2.3.0
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
)

1
go.sum
View File

@ -48,6 +48,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,170 +0,0 @@
package go4k
// FindSuperIntArray finds a small super array containing all
// the subarrays passed to it. Returns the super array and indices where
// the subarrays can be found. For example:
// FindSuperIntArray([][]int{{4,5,6},{1,2,3},{3,4}})
// returns {1,2,3,4,5,6},{3,0,2}
// Implemented using a greedy search, so does not necessarily find
// the true optimal (the problem is NP-hard and analogous to traveling
// salesman problem).
//
// Used to construct a small delay time table without unnecessary repetition
// of delay times.
func FindSuperIntArray(arrays [][]int) ([]int, []int) {
// If we go past MAX_MERGES, the algorithm could get slow and hang the computer
// So this is a safety limit: after this problem size, just merge any arrays
// until we get into more manageable range
const maxMerges = 1000
min := func(a int, b int) int {
if a < b {
return a
}
return b
}
overlap := func(a []int, b []int) (int, int) {
minShift := len(a)
for shift := len(a) - 1; shift >= 0; shift-- {
overlapping := true
for k := shift; k < min(len(a), len(b)+shift); k++ {
if a[k] != b[k-shift] {
overlapping = false
break
}
}
if overlapping {
minShift = shift
}
}
overlap := min(len(a)-minShift, len(b))
return overlap, minShift
}
sliceNumbers := make([]int, len(arrays))
startIndices := make([]int, len(arrays))
var processedArrays [][]int
for i := range arrays {
if len(arrays[i]) == 0 {
// Zero length arrays do not need to be processed at all
// They will 'start' at index 0 always as they have no length.
sliceNumbers[i] = -1
} else {
sliceNumbers[i] = len(processedArrays)
processedArrays = append(processedArrays, arrays[i])
}
}
if len(processedArrays) == 0 {
return []int{}, startIndices // no arrays with len>0 to process, just return empty array and all indices as 0
}
for len(processedArrays) > 1 { // there's at least two candidates that could be be merged
maxO, maxI, maxJ, maxS := -1, -1, -1, -1
if len(processedArrays) < maxMerges {
// find the pair i,j that results in the largest overlap with array i coming first, followed by potentially overlapping array j
for i := range processedArrays {
for j := range processedArrays {
if i == j {
continue
}
overlap, shift := overlap(processedArrays[i], processedArrays[j])
if overlap > maxO {
maxI, maxJ, maxO, maxS = i, j, overlap, shift
}
}
}
} else {
// The task is daunting, we have over MAX_MERGES overlaps to test. Just merge two first ones until the task is more manageable size
overlap, shift := overlap(processedArrays[0], processedArrays[1])
maxI, maxJ, maxO, maxS = 0, 1, overlap, shift
}
for k := range sliceNumbers {
if sliceNumbers[k] == maxJ {
// update slice pointers to point maxI instead of maxJ (maxJ will be appended to maxI, taking overlap into account)
sliceNumbers[k] = maxI
startIndices[k] += maxS // the array j starts at index maxS in array i
}
if sliceNumbers[k] > maxJ {
// pointers maxJ reduced by 1 as maxJ will be deleted
sliceNumbers[k]--
}
}
// if array j was not entirely included within array j
if maxO < len(processedArrays[maxJ]) {
// append array maxJ to array maxI, without duplicating the overlapping part
processedArrays[maxI] = append(processedArrays[maxI], processedArrays[maxJ][maxO:]...)
}
// finally, remove element maxJ from processedArrays
processedArrays = append(processedArrays[:maxJ], processedArrays[maxJ+1:]...)
}
return processedArrays[0], startIndices // there should be only one slice left in the arrays after the loop
}
// ConstructDelayTimeTable tries to construct the delay times table
// abusing overlapping between different delay times tables as much
// as possible. Especially: if two delay units use exactly the same
// delay times, they appear in the table only once.
func ConstructDelayTimeTable(patch Patch) ([]int, [][]int) {
ind := make([][]int, len(patch.Instruments))
var subarrays [][]int
// flatten the delay times into one array of arrays
// saving the indices where they were placed
for i, instr := range patch.Instruments {
ind[i] = make([]int, len(instr.Units))
for j, unit := range instr.Units {
// only include delay times for delays. Only delays
// should use delay times
if unit.Type == "delay" {
ind[i][j] = len(subarrays)
end := unit.Parameters["count"]
if unit.Parameters["stereo"] > 0 {
end *= 2
}
subarrays = append(subarrays, patch.DelayTimes[unit.Parameters["delay"]:end])
}
}
}
delayTable, indices := FindSuperIntArray(subarrays)
// cancel the flattening, so unitindices can be used to
// to find the index of each delay in the delay table
unitindices := make([][]int, len(patch.Instruments))
for i, instr := range patch.Instruments {
unitindices[i] = make([]int, len(instr.Units))
for j, unit := range instr.Units {
if unit.Type == "delay" {
unitindices[i][j] = indices[ind[i][j]]
}
}
}
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.Instruments))
var offsetTable []SampleOffset
offsetMap := map[SampleOffset]int{}
for i, instr := range patch.Instruments {
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
}

View File

@ -1,36 +0,0 @@
package go4k_test
import (
"fmt"
"reflect"
"testing"
"github.com/vsariola/sointu/go4k"
)
func TestFindSuperIntArray(t *testing.T) {
var tests = []struct {
input [][]int
wantSuper []int
wantIndices []int
}{
{[][]int{}, []int{}, []int{}},
{[][]int{nil, nil}, []int{}, []int{0, 0}},
{[][]int{{3, 4, 5}, {1, 2, 3}}, []int{1, 2, 3, 4, 5}, []int{2, 0}},
{[][]int{{3, 4, 5}, {1, 2, 3}, nil}, []int{1, 2, 3, 4, 5}, []int{2, 0, 0}},
{[][]int{{3, 4, 5}, {1, 2, 3}, {}}, []int{1, 2, 3, 4, 5}, []int{2, 0, 0}},
{[][]int{{3, 4, 5}, {1, 2, 3}, {2, 3}}, []int{1, 2, 3, 4, 5}, []int{2, 0, 1}},
{[][]int{{1, 2, 3, 4, 5}, {1, 2, 3}}, []int{1, 2, 3, 4, 5}, []int{0, 0}},
{[][]int{{1, 2, 3, 4, 5}, {2, 3}}, []int{1, 2, 3, 4, 5}, []int{0, 1}},
{[][]int{{1, 2, 3, 4, 5}, {2, 3}, {5, 6, 7}}, []int{1, 2, 3, 4, 5, 6, 7}, []int{0, 1, 4}},
{[][]int{{1, 2, 3, 4}, {3, 4, 1}, {2, 3, 4, 5}}, []int{3, 4, 1, 2, 3, 4, 5}, []int{2, 0, 3}},
}
for i, tt := range tests {
t.Run(fmt.Sprintf("TestFindSuperIntArray %d", i), func(t *testing.T) {
super, indices := go4k.FindSuperIntArray(tt.input)
if !reflect.DeepEqual(super, tt.wantSuper) || !reflect.DeepEqual(indices, tt.wantIndices) {
t.Errorf("FindSuperIntArray(%v) got (%v,%v), want (%v,%v)", tt.input, super, indices, tt.wantSuper, tt.wantIndices)
}
})
}
}

View File

@ -1,346 +0,0 @@
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
}

View File

@ -1,4 +1,4 @@
package go4k_test
package bridge_test
import (
"bytes"
@ -9,19 +9,18 @@ import (
"os"
"path"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"github.com/vsariola/sointu/go4k"
"github.com/vsariola/sointu/go4k/bridge"
"gopkg.in/yaml.v2"
)
func TestAllAsmFiles(t *testing.T) {
bridge.Init()
_, myname, _, _ := runtime.Caller(0)
files, err := filepath.Glob(path.Join(path.Dir(myname), "..", "tests", "*.asm"))
files, err := filepath.Glob(path.Join(path.Dir(myname), "..", "..", "tests", "*.yml"))
if err != nil {
t.Fatalf("cannot glob files in the test directory: %v", err)
}
@ -37,89 +36,16 @@ func TestAllAsmFiles(t *testing.T) {
if err != nil {
t.Fatalf("cannot read the .asm file: %v", filename)
}
song, err := go4k.ParseAsm(string(asmcode))
var song go4k.Song
err = yaml.Unmarshal(asmcode, &song)
if err != nil {
t.Fatalf("could not parse the .asm file: %v", err)
t.Fatalf("could not parse the .yml file: %v", err)
}
synth, err := bridge.Synth(song.Patch)
if err != nil {
t.Fatalf("Compiling patch failed: %v", err)
}
buffer, err := go4k.Play(synth, *song)
buffer = buffer[:song.TotalRows()*song.SamplesPerRow()*2] // extend to the nominal length always.
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)
}
}
if song.Output16Bit {
int16Buffer := convertToInt16Buffer(buffer)
compareToRawInt16(t, int16Buffer, testname+".raw")
} else {
compareToRawFloat32(t, buffer, testname+".raw")
}
})
}
}
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) {
if runtime.GOOS != "windows" && strings.Contains(testname, "sample") {
t.Skip("Samples (gm.dls) available only on Windows")
return
}
asmcode, err := ioutil.ReadFile(filename)
if err != nil {
t.Fatalf("cannot read the .asm file: %v", filename)
}
song, err := go4k.ParseAsm(string(asmcode)) // read the asm
if err != nil {
t.Fatalf("could not parse the .asm file: %v", err)
}
str, err := go4k.FormatAsm(song) // serialize again
if err != nil {
t.Fatalf("Could not serialize asm file: %v", err)
}
song2, err := go4k.ParseAsm(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)
}
if !reflect.DeepEqual(song, song2) {
t.Fatalf("serialize/deserialize does not result equal songs, before: %v, after %v", song, song2)
}
synth, err := bridge.Synth(song2.Patch)
if err != nil {
t.Fatalf("Compiling patch failed: %v", err)
}
buffer, err := go4k.Play(synth, *song2)
buffer, err := go4k.Play(synth, song)
buffer = buffer[:song.TotalRows()*song.SamplesPerRow()*2] // extend to the nominal length always.
if err != nil {
t.Fatalf("Play failed: %v", err)
@ -157,7 +83,7 @@ func TestSerializingAllAsmFiles(t *testing.T) {
func compareToRawFloat32(t *testing.T, buffer []float32, rawname string) {
_, 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))
if err != nil {
t.Fatalf("cannot read expected: %v", err)
}
@ -179,7 +105,7 @@ func compareToRawFloat32(t *testing.T, buffer []float32, rawname string) {
func compareToRawInt16(t *testing.T, buffer []int16, rawname string) {
_, 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))
if err != nil {
t.Fatalf("cannot read expected: %v", err)
}

View File

@ -1,74 +1,48 @@
package bridge
// #cgo CFLAGS: -I"${SRCDIR}/../../build/"
// #cgo LDFLAGS: "${SRCDIR}/../../build/libsointu.a"
// #include <sointu.h>
import "C"
import (
"errors"
"fmt"
"strings"
"github.com/vsariola/sointu/go4k"
"github.com/vsariola/sointu/go4k/compiler"
)
// #cgo CFLAGS: -I"${SRCDIR}/../../include/sointu"
// #cgo LDFLAGS: "${SRCDIR}/../../build/libsointu.a"
// #include <sointu.h>
import "C"
type opTableEntry struct {
opcode C.int
parameterList []string
}
var opcodeTable = map[string]opTableEntry{
"add": opTableEntry{C.su_add_id, []string{}},
"addp": opTableEntry{C.su_addp_id, []string{}},
"pop": opTableEntry{C.su_pop_id, []string{}},
"loadnote": opTableEntry{C.su_loadnote_id, []string{}},
"mul": opTableEntry{C.su_mul_id, []string{}},
"mulp": opTableEntry{C.su_mulp_id, []string{}},
"push": opTableEntry{C.su_push_id, []string{}},
"xch": opTableEntry{C.su_xch_id, []string{}},
"distort": opTableEntry{C.su_distort_id, []string{"drive"}},
"hold": opTableEntry{C.su_hold_id, []string{"holdfreq"}},
"crush": opTableEntry{C.su_crush_id, []string{"resolution"}},
"gain": opTableEntry{C.su_gain_id, []string{"gain"}},
"invgain": opTableEntry{C.su_invgain_id, []string{"invgain"}},
"filter": opTableEntry{C.su_filter_id, []string{"frequency", "resonance"}},
"clip": opTableEntry{C.su_clip_id, []string{}},
"pan": opTableEntry{C.su_pan_id, []string{"panning"}},
"delay": opTableEntry{C.su_delay_id, []string{"pregain", "dry", "feedback", "damp", "delay", "count"}},
"compressor": opTableEntry{C.su_compres_id, []string{"attack", "release", "invgain", "threshold", "ratio"}},
"speed": opTableEntry{C.su_speed_id, []string{}},
"out": opTableEntry{C.su_out_id, []string{"gain"}},
"outaux": opTableEntry{C.su_outaux_id, []string{"outgain", "auxgain"}},
"aux": opTableEntry{C.su_aux_id, []string{"gain", "channel"}},
"send": opTableEntry{C.su_send_id, []string{"amount"}},
"envelope": opTableEntry{C.su_envelope_id, []string{"attack", "decay", "sustain", "release", "gain"}},
"noise": opTableEntry{C.su_noise_id, []string{"shape", "gain"}},
"oscillator": opTableEntry{C.su_oscillat_id, []string{"transpose", "detune", "phase", "color", "shape", "gain"}},
"loadval": opTableEntry{C.su_loadval_id, []string{"value"}},
"receive": opTableEntry{C.su_receive_id, []string{}},
"in": opTableEntry{C.su_in_id, []string{"channel"}},
}
type RenderError struct {
errcode int
}
func (e *RenderError) Error() string {
var reasons []string
if e.errcode&0x40 != 0 {
reasons = append(reasons, "FPU stack over/underflow")
func Synth(patch go4k.Patch) (*C.Synth, error) {
s := new(C.Synth)
comPatch, err := compiler.Encode(&patch, compiler.AllFeatures{})
if err != nil {
return nil, fmt.Errorf("error compiling patch: %v", err)
}
if e.errcode&0x04 != 0 {
reasons = append(reasons, "FPU divide by zero")
if len(comPatch.Commands) > 2048 { // TODO: 2048 could probably be pulled automatically from cgo
return nil, errors.New("bridge supports at most 2048 commands; the compiled patch has more")
}
if e.errcode&0x01 != 0 {
reasons = append(reasons, "FPU invalid operation")
if len(comPatch.Values) > 16384 { // TODO: 16384 could probably be pulled automatically from cgo
return nil, errors.New("bridge supports at most 16384 values; the compiled patch has more")
}
if e.errcode&0x3800 != 0 {
reasons = append(reasons, "FPU stack push/pops are not balanced")
for i, v := range comPatch.Commands {
s.Commands[i] = (C.uchar)(v)
}
return "RenderError: " + strings.Join(reasons, ", ")
for i, v := range comPatch.Values {
s.Values[i] = (C.uchar)(v)
}
for i, v := range comPatch.DelayTimes {
s.DelayTimes[i] = (C.ushort)(v)
}
for i, v := range comPatch.SampleOffsets {
s.SampleOffsets[i].Start = (C.uint)(v.Start)
s.SampleOffsets[i].LoopStart = (C.ushort)(v.LoopStart)
s.SampleOffsets[i].LoopLength = (C.ushort)(v.LoopLength)
}
s.NumVoices = C.uint(comPatch.NumVoices)
s.Polyphony = C.uint(comPatch.PolyphonyBitmask)
s.RandSeed = 1
return s, nil
}
// Render renders until the buffer is full or the modulated time is reached, whichever
@ -101,131 +75,37 @@ func (synth *C.Synth) Render(buffer []float32, maxtime int) (int, int, error) {
return int(samples), int(time), nil
}
func Synth(patch go4k.Patch) (*C.Synth, error) {
s := new(C.Synth)
totalVoices := 0
commands := make([]byte, 0)
values := make([]byte, 0)
polyphonyBitmask := 0
for insid, instr := range patch.Instruments {
if len(instr.Units) > 63 {
return nil, errors.New("An instrument can have a maximum of 63 units")
}
if instr.NumVoices < 1 {
return nil, errors.New("Each instrument must have at least 1 voice")
}
for unitid, unit := range instr.Units {
if val, ok := opcodeTable[unit.Type]; ok {
opCode := val.opcode
if unit.Parameters["stereo"] == 1 {
opCode++
}
commands = append(commands, byte(opCode))
for _, paramname := range val.parameterList {
if unit.Type == "delay" && paramname == "count" {
count := unit.Parameters["count"]*2 - 1
if unit.Parameters["notetracking"] == 1 {
count++
}
values = append(values, byte(count))
} else if pval, ok := unit.Parameters[paramname]; ok {
values = append(values, byte(pval))
} else {
return nil, fmt.Errorf("Unit parameter undefined: %v (at instrument %v, unit %v)", paramname, insid, unitid)
}
}
if unit.Type == "oscillator" {
flags := 0
switch unit.Parameters["type"] {
case go4k.Sine:
flags = 0x40
case go4k.Trisaw:
flags = 0x20
case go4k.Pulse:
flags = 0x10
case go4k.Gate:
flags = 0x04
case go4k.Sample:
flags = 0x80
}
if unit.Parameters["lfo"] == 1 {
flags += 0x08
}
flags += unit.Parameters["unison"]
values = append(values, byte(flags))
} else if unit.Type == "filter" {
flags := 0
if unit.Parameters["lowpass"] == 1 {
flags += 0x40
}
if unit.Parameters["bandpass"] == 1 {
flags += 0x20
}
if unit.Parameters["highpass"] == 1 {
flags += 0x10
}
if unit.Parameters["negbandpass"] == 1 {
flags += 0x08
}
if unit.Parameters["neghighpass"] == 1 {
flags += 0x04
}
values = append(values, byte(flags))
} else if unit.Type == "send" {
address := unit.Parameters["unit"]*16 + 24 + unit.Parameters["port"]
if unit.Parameters["voice"] > 0 {
address += 0x4000 + 16 + (unit.Parameters["voice"]-1)*1024 // global send, address is computed relative to synthworkspace
}
if unit.Parameters["sendpop"] == 1 {
address += 0x8000
}
values = append(values, byte(address&255), byte(address>>8))
}
} else {
return nil, fmt.Errorf("Unknown unit type: %v (at instrument %v, unit %v)", unit.Type, insid, unitid)
}
}
commands = append(commands, byte(C.su_advance_id))
totalVoices += instr.NumVoices
for k := 0; k < instr.NumVoices-1; k++ {
polyphonyBitmask = (polyphonyBitmask << 1) + 1
}
polyphonyBitmask <<= 1
}
if totalVoices > 32 {
return nil, errors.New("Sointu does not support more than 32 concurrent voices")
}
if len(commands) > 2048 { // TODO: 2048 could probably be pulled automatically from cgo
return nil, errors.New("The patch would result in more than 2048 commands")
}
if len(values) > 16384 { // TODO: 16384 could probably be pulled automatically from cgo
return nil, errors.New("The patch would result in more than 16384 values")
}
for i := range commands {
s.Commands[i] = (C.uchar)(commands[i])
}
for i := range values {
s.Values[i] = (C.uchar)(values[i])
}
for i, deltime := range patch.DelayTimes {
s.DelayTimes[i] = (C.ushort)(deltime)
}
for i, samoff := range patch.SampleOffsets {
s.SampleOffsets[i].Start = (C.uint)(samoff.Start)
s.SampleOffsets[i].LoopStart = (C.ushort)(samoff.LoopStart)
s.SampleOffsets[i].LoopLength = (C.ushort)(samoff.LoopLength)
}
s.NumVoices = C.uint(totalVoices)
s.Polyphony = C.uint(polyphonyBitmask)
s.RandSeed = 1
return s, nil
}
// Trigger is part of C.Synths' implementation of go4k.Synth interface
func (s *C.Synth) Trigger(voice int, note byte) {
s.SynthWrk.Voices[voice] = C.Voice{}
s.SynthWrk.Voices[voice].Note = C.int(note)
}
// Release is part of C.Synths' implementation of go4k.Synth interface
func (s *C.Synth) Release(voice int) {
s.SynthWrk.Voices[voice].Release = 1
}
// Render error stores the exact errorcode, which is actually just the x87 FPU flags,
// with only the critical failure flags masked. Useful if you are interested exactly
// what went wrong with the patch.
type RenderError struct {
errcode int
}
func (e *RenderError) Error() string {
var reasons []string
if e.errcode&0x40 != 0 {
reasons = append(reasons, "FPU stack over/underflow")
}
if e.errcode&0x04 != 0 {
reasons = append(reasons, "FPU divide by zero")
}
if e.errcode&0x01 != 0 {
reasons = append(reasons, "FPU invalid operation")
}
if e.errcode&0x3800 != 0 {
reasons = append(reasons, "FPU stack push/pops are not balanced")
}
return "RenderError: " + strings.Join(reasons, ", ")
}

View File

@ -12,25 +12,14 @@ import (
"github.com/vsariola/sointu/go4k/bridge"
)
const BPM = 100
const SAMPLE_RATE = 44100
const TOTAL_ROWS = 16
const SAMPLES_PER_ROW = SAMPLE_RATE * 4 * 60 / (BPM * 16)
const su_max_samples = SAMPLES_PER_ROW * TOTAL_ROWS
// const bufsize = su_max_samples * 2
func TestBridge(t *testing.T) {
patch := go4k.Patch{
Instruments: []go4k.Instrument{
go4k.Instrument{1, []go4k.Unit{
go4k.Unit{"envelope", map[string]int{"stereo": 0, "attack": 64, "decay": 64, "sustain": 64, "release": 80, "gain": 128}},
go4k.Unit{"envelope", map[string]int{"stereo": 0, "attack": 95, "decay": 64, "sustain": 64, "release": 80, "gain": 128}},
go4k.Unit{"out", map[string]int{"stereo": 1, "gain": 128}},
}}},
SampleOffsets: []go4k.SampleOffset{},
DelayTimes: []int{}}
go4k.Unit{Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 64, "decay": 64, "sustain": 64, "release": 80, "gain": 128}},
go4k.Unit{Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 95, "decay": 64, "sustain": 64, "release": 80, "gain": 128}},
go4k.Unit{Type: "out", Parameters: map[string]int{"stereo": 1, "gain": 128}},
}}}}
synth, err := bridge.Synth(patch)
if err != nil {
@ -64,6 +53,7 @@ func TestBridge(t *testing.T) {
for i, v := range createdb {
if expectedb[i] != v {
t.Errorf("byte mismatch @ %v, got %v, expected %v", i, v, expectedb[i])
break
}
}
}
@ -72,10 +62,8 @@ func TestStackUnderflow(t *testing.T) {
patch := go4k.Patch{
Instruments: []go4k.Instrument{
go4k.Instrument{1, []go4k.Unit{
go4k.Unit{"pop", map[string]int{}},
}}},
SampleOffsets: []go4k.SampleOffset{},
DelayTimes: []int{}}
go4k.Unit{Type: "pop", Parameters: map[string]int{}},
}}}}
synth, err := bridge.Synth(patch)
if err != nil {
t.Fatalf("bridge compile error: %v", err)
@ -91,10 +79,8 @@ func TestStackBalancing(t *testing.T) {
patch := go4k.Patch{
Instruments: []go4k.Instrument{
go4k.Instrument{1, []go4k.Unit{
go4k.Unit{"push", map[string]int{}},
}}},
SampleOffsets: []go4k.SampleOffset{},
DelayTimes: []int{}}
go4k.Unit{Type: "push", Parameters: map[string]int{}},
}}}}
synth, err := bridge.Synth(patch)
if err != nil {
t.Fatalf("bridge compile error: %v", err)
@ -110,27 +96,25 @@ func TestStackOverflow(t *testing.T) {
patch := go4k.Patch{
Instruments: []go4k.Instrument{
go4k.Instrument{1, []go4k.Unit{
go4k.Unit{"loadval", map[string]int{"value": 128}},
go4k.Unit{"loadval", map[string]int{"value": 128}},
go4k.Unit{"loadval", map[string]int{"value": 128}},
go4k.Unit{"loadval", map[string]int{"value": 128}},
go4k.Unit{"loadval", map[string]int{"value": 128}},
go4k.Unit{"loadval", map[string]int{"value": 128}},
go4k.Unit{"loadval", map[string]int{"value": 128}},
go4k.Unit{"loadval", map[string]int{"value": 128}},
go4k.Unit{"loadval", map[string]int{"value": 128}},
go4k.Unit{"pop", map[string]int{}},
go4k.Unit{"pop", map[string]int{}},
go4k.Unit{"pop", map[string]int{}},
go4k.Unit{"pop", map[string]int{}},
go4k.Unit{"pop", map[string]int{}},
go4k.Unit{"pop", map[string]int{}},
go4k.Unit{"pop", map[string]int{}},
go4k.Unit{"pop", map[string]int{}},
go4k.Unit{"pop", map[string]int{}},
}}},
SampleOffsets: []go4k.SampleOffset{},
DelayTimes: []int{}}
go4k.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
go4k.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
go4k.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
go4k.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
go4k.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
go4k.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
go4k.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
go4k.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
go4k.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
go4k.Unit{Type: "pop", Parameters: map[string]int{}},
go4k.Unit{Type: "pop", Parameters: map[string]int{}},
go4k.Unit{Type: "pop", Parameters: map[string]int{}},
go4k.Unit{Type: "pop", Parameters: map[string]int{}},
go4k.Unit{Type: "pop", Parameters: map[string]int{}},
go4k.Unit{Type: "pop", Parameters: map[string]int{}},
go4k.Unit{Type: "pop", Parameters: map[string]int{}},
go4k.Unit{Type: "pop", Parameters: map[string]int{}},
go4k.Unit{Type: "pop", Parameters: map[string]int{}},
}}}}
synth, err := bridge.Synth(patch)
if err != nil {
t.Fatalf("bridge compile error: %v", err)
@ -146,12 +130,10 @@ func TestDivideByZero(t *testing.T) {
patch := go4k.Patch{
Instruments: []go4k.Instrument{
go4k.Instrument{1, []go4k.Unit{
go4k.Unit{"loadval", map[string]int{"value": 128}},
go4k.Unit{"invgain", map[string]int{"invgain": 0}},
go4k.Unit{"pop", map[string]int{}},
}}},
SampleOffsets: []go4k.SampleOffset{},
DelayTimes: []int{}}
go4k.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
go4k.Unit{Type: "invgain", Parameters: map[string]int{"invgain": 0}},
go4k.Unit{Type: "pop", Parameters: map[string]int{}},
}}}}
synth, err := bridge.Synth(patch)
if err != nil {
t.Fatalf("bridge compile error: %v", err)

View File

@ -1,6 +0,0 @@
// +build !windows
package bridge
func Init() {
}

View File

@ -1,12 +0,0 @@
// +build windows
package bridge
// #cgo CFLAGS: -I"${SRCDIR}/../../include/sointu"
// #cgo LDFLAGS: "${SRCDIR}/../../build/libsointu.a"
// #include <sointu.h>
import "C"
func Init() {
C.su_load_gmdls() // GM.DLS is an windows specific sound bank so samples work currently only on windows
}

View File

@ -0,0 +1,8 @@
package bridge
// #include "sointu.h"
import "C"
func init() {
C.su_load_gmdls() // GM.DLS is an windows specific sound bank so samples work currently only on windows
}

View File

@ -1,4 +1,4 @@
package go4k_test
package bridge_test
import (
"bytes"
@ -25,16 +25,14 @@ const su_max_samples = SAMPLES_PER_ROW * TOTAL_ROWS
func TestPlayer(t *testing.T) {
patch := go4k.Patch{
Instruments: []go4k.Instrument{go4k.Instrument{1, []go4k.Unit{
go4k.Unit{"envelope", map[string]int{"stereo": 0, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
go4k.Unit{"oscillator", map[string]int{"stereo": 0, "transpose": 64, "detune": 64, "phase": 0, "color": 96, "shape": 64, "gain": 128, "type": go4k.Sine, "lfo": 0, "unison": 0}},
go4k.Unit{"mulp", map[string]int{"stereo": 0}},
go4k.Unit{"envelope", map[string]int{"stereo": 0, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
go4k.Unit{"oscillator", map[string]int{"stereo": 0, "transpose": 72, "detune": 64, "phase": 64, "color": 64, "shape": 96, "gain": 128, "type": go4k.Sine, "lfo": 0, "unison": 0}},
go4k.Unit{"mulp", map[string]int{"stereo": 0}},
go4k.Unit{"out", map[string]int{"stereo": 1, "gain": 128}},
}}},
DelayTimes: []int{},
SampleOffsets: []go4k.SampleOffset{}}
go4k.Unit{Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
go4k.Unit{Type: "oscillator", Parameters: map[string]int{"stereo": 0, "transpose": 64, "detune": 64, "phase": 0, "color": 96, "shape": 64, "gain": 128, "type": go4k.Sine, "lfo": 0, "unison": 0}},
go4k.Unit{Type: "mulp", Parameters: map[string]int{"stereo": 0}},
go4k.Unit{Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
go4k.Unit{Type: "oscillator", Parameters: map[string]int{"stereo": 0, "transpose": 72, "detune": 64, "phase": 64, "color": 64, "shape": 96, "gain": 128, "type": go4k.Sine, "lfo": 0, "unison": 0}},
go4k.Unit{Type: "mulp", Parameters: map[string]int{"stereo": 0}},
go4k.Unit{Type: "out", Parameters: map[string]int{"stereo": 1, "gain": 128}},
}}}}
patterns := [][]byte{{64, 0, 68, 0, 32, 0, 0, 0, 75, 0, 78, 0, 0, 0, 0, 0}}
tracks := []go4k.Track{go4k.Track{1, []byte{0}}}
song := go4k.Song{BPM: 100, Patterns: patterns, Tracks: tracks, Patch: patch, Output16Bit: false, Hold: 1}
@ -47,7 +45,7 @@ func TestPlayer(t *testing.T) {
t.Fatalf("Render failed: %v", err)
}
_, filename, _, _ := runtime.Caller(0)
expectedb, err := ioutil.ReadFile(path.Join(path.Dir(filename), "..", "tests", "expected_output", "test_oscillat_sine.raw"))
expectedb, err := ioutil.ReadFile(path.Join(path.Dir(filename), "..", "..", "tests", "expected_output", "test_oscillat_sine.raw"))
if err != nil {
t.Fatalf("cannot read expected: %v", err)
}

View File

@ -17,6 +17,7 @@ import (
"github.com/vsariola/sointu/go4k"
"github.com/vsariola/sointu/go4k/audio/oto"
"github.com/vsariola/sointu/go4k/bridge"
"github.com/vsariola/sointu/go4k/compiler"
)
func main() {
@ -41,12 +42,21 @@ func main() {
flag.Usage()
os.Exit(0)
}
var comp *compiler.Compiler
if !*asmOut && !*jsonOut && !*rawOut && !*headerOut && !*play && !*yamlOut && *tmplDir == "" {
*play = true // if the user gives nothing to output, then the default behaviour is just to play the file
}
needsRendering := *play || *exactLength || *rawOut
if needsRendering {
bridge.Init()
needsCompile := *headerOut || *asmOut
if needsCompile {
var err error
comp, err = compiler.New()
if err != nil {
fmt.Fprintf(os.Stderr, `error creating compiler: %v`, err)
os.Exit(1)
}
comp.Amd64 = *targetArch == "amd64"
comp.OS = *targetOs
}
process := func(filename string) error {
output := func(extension string, contents []byte) error {
@ -87,11 +97,7 @@ func main() {
var song go4k.Song
if errJSON := json.Unmarshal(inputBytes, &song); errJSON != nil {
if errYaml := yaml.Unmarshal(inputBytes, &song); errYaml != nil {
song2, errAsm := go4k.ParseAsm(string(inputBytes))
if errAsm != nil {
return fmt.Errorf("The song could not be parsed as .json (%v), .yml (%v) nor .asm (%v)", errJSON, errYaml, errAsm)
}
song = *song2
return fmt.Errorf("The song could not be parsed as .json (%v) or .yml (%v)", errJSON, errYaml)
}
}
var buffer []float32
@ -121,23 +127,26 @@ func main() {
return fmt.Errorf("error updating the hold value of the song: %v", err)
}
}
if *headerOut {
var compiledPlayer map[string]string
if needsCompile {
maxSamples := 0 // 0 means it is calculated automatically
if *exactLength {
maxSamples = len(buffer) / 2
}
header := go4k.CHeader(&song, maxSamples)
if err := output(".h", []byte(header)); err != nil {
var err error
compiledPlayer, err = comp.Player(&song, maxSamples)
if err != nil {
return fmt.Errorf("compiling player failed: %v", err)
}
}
if *headerOut {
if err := output(".h", []byte(compiledPlayer["h"])); err != nil {
return fmt.Errorf("Error outputting header file: %v", err)
}
}
if *asmOut {
asmCode, err := go4k.Compile(&song, *targetArch, *targetOs)
if err != nil {
return fmt.Errorf("Could not format the song as asm file: %v", err)
}
if err := output(".asm", []byte(asmCode)); err != nil {
if err := output(".asm", []byte(compiledPlayer["asm"])); err != nil {
return fmt.Errorf("Error outputting asm file: %v", err)
}
}

81
go4k/compiler/compiler.go Normal file
View File

@ -0,0 +1,81 @@
package compiler
import (
"bytes"
"fmt"
"path"
"path/filepath"
"runtime"
"text/template"
"github.com/Masterminds/sprig"
"github.com/vsariola/sointu/go4k"
)
//go:generate go run generate.go
type Compiler struct {
Template *template.Template
Amd64 bool
OS string
DisableSections bool
}
// New returns a new compiler using the default .asm templates
func New() (*Compiler, error) {
_, myname, _, _ := runtime.Caller(0)
templateDir := filepath.Join(path.Dir(myname), "..", "..", "templates")
compiler, err := NewFromTemplates(templateDir)
return compiler, err
}
func NewFromTemplates(directory string) (*Compiler, error) {
globPtrn := filepath.Join(directory, "*.*")
tmpl, err := template.New("base").Funcs(sprig.TxtFuncMap()).ParseGlob(globPtrn)
if err != nil {
return nil, fmt.Errorf(`could not create template based on directory "%v": %v`, directory, err)
}
return &Compiler{Template: tmpl, Amd64: runtime.GOARCH == "amd64", OS: runtime.GOOS}, nil
}
func (com *Compiler) compile(templateName string, data interface{}) (string, error) {
result := bytes.NewBufferString("")
err := com.Template.ExecuteTemplate(result, templateName, data)
return result.String(), err
}
func (com *Compiler) Library() (map[string]string, error) {
features := AllFeatures{}
m := NewMacros(*com, features)
m.Library = true
asmCode, err := com.compile("library.asm", m)
if err != nil {
return nil, fmt.Errorf(`could not execute template "library.asm": %v`, err)
}
m = NewMacros(*com, features)
m.Library = true
header, err := com.compile("library.h", &m)
if err != nil {
return nil, fmt.Errorf(`could not execute template "library.h": %v`, err)
}
return map[string]string{"asm": asmCode, "h": header}, nil
}
func (com *Compiler) Player(song *go4k.Song, maxSamples int) (map[string]string, error) {
features := NecessaryFeaturesFor(song.Patch)
encodedPatch, err := Encode(&song.Patch, features)
if err != nil {
return nil, fmt.Errorf(`could not encode patch: %v`, err)
}
asmCode, err := com.compile("player.asm", NewPlayerMacros(*com, features, song, encodedPatch, maxSamples))
if err != nil {
return nil, fmt.Errorf(`could not execute template "player.asm": %v`, err)
}
header, err := com.compile("player.h", NewPlayerMacros(*com, features, song, encodedPatch, maxSamples))
if err != nil {
return nil, fmt.Errorf(`could not execute template "player.h": %v`, err)
}
return map[string]string{"asm": asmCode, "h": header}, nil
}

View File

@ -0,0 +1,144 @@
package compiler
import (
"errors"
"fmt"
"github.com/vsariola/sointu/go4k"
)
type EncodedPatch struct {
Commands []byte
Values []byte
DelayTimes []uint16
SampleOffsets []SampleOffset
PolyphonyBitmask uint32
NumVoices uint32
}
type SampleOffset struct {
Start uint32
LoopStart uint16
LoopLength uint16
}
func Encode(patch *go4k.Patch, featureSet FeatureSet) (*EncodedPatch, error) {
var c EncodedPatch
sampleOffsetMap := map[SampleOffset]int{}
for _, instr := range patch.Instruments {
if len(instr.Units) > 63 {
return nil, errors.New("An instrument can have a maximum of 63 units")
}
if instr.NumVoices < 1 {
return nil, errors.New("Each instrument must have at least 1 voice")
}
for _, unit := range instr.Units {
if unit.Type == "oscillator" && unit.Parameters["type"] == 4 {
s := SampleOffset{Start: uint32(unit.Parameters["start"]), LoopStart: uint16(unit.Parameters["loopstart"]), LoopLength: uint16(unit.Parameters["looplength"])}
index, ok := sampleOffsetMap[s]
if !ok {
index = len(c.SampleOffsets)
sampleOffsetMap[s] = index
c.SampleOffsets = append(c.SampleOffsets, s)
}
unit.Parameters["color"] = index
}
if unit.Type == "delay" {
unit.Parameters["delay"] = len(c.DelayTimes)
if unit.Parameters["stereo"] == 1 {
unit.Parameters["count"] = len(unit.VarArgs) / 2
} else {
unit.Parameters["count"] = len(unit.VarArgs)
}
for _, v := range unit.VarArgs {
c.DelayTimes = append(c.DelayTimes, uint16(v))
}
}
command, values, err := EncodeUnit(unit, featureSet)
if err != nil {
return nil, fmt.Errorf(`encoding unit failed: %v`, err)
}
c.Commands = append(c.Commands, command)
c.Values = append(c.Values, values...)
}
c.Commands = append(c.Commands, byte(0)) // advance
c.NumVoices += uint32(instr.NumVoices)
for k := 0; k < instr.NumVoices-1; k++ {
c.PolyphonyBitmask = (c.PolyphonyBitmask << 1) + 1
}
c.PolyphonyBitmask <<= 1
}
if c.NumVoices > 32 {
return nil, fmt.Errorf("Sointu does not support more than 32 concurrent voices; patch uses %v", c.NumVoices)
}
return &c, nil
}
func EncodeUnit(unit go4k.Unit, featureSet FeatureSet) (byte, []byte, error) {
opcode, ok := featureSet.Opcode(unit.Type)
if !ok {
return 0, nil, fmt.Errorf(`the targeted virtual machine is not configured to support unit type "%v"`, unit.Type)
}
var values []byte
for _, v := range go4k.UnitTypes[unit.Type] {
if v.CanModulate && v.CanSet {
values = append(values, byte(unit.Parameters[v.Name]))
}
}
if unit.Type == "aux" {
values = append(values, byte(unit.Parameters["channel"]))
} else if unit.Type == "in" {
values = append(values, byte(unit.Parameters["channel"]))
} else if unit.Type == "oscillator" {
flags := 0
switch unit.Parameters["type"] {
case go4k.Sine:
flags = 0x40
case go4k.Trisaw:
flags = 0x20
case go4k.Pulse:
flags = 0x10
case go4k.Gate:
flags = 0x04
case go4k.Sample:
flags = 0x80
}
if unit.Parameters["lfo"] == 1 {
flags += 0x08
}
flags += unit.Parameters["unison"]
values = append(values, byte(flags))
} else if unit.Type == "filter" {
flags := 0
if unit.Parameters["lowpass"] == 1 {
flags += 0x40
}
if unit.Parameters["bandpass"] == 1 {
flags += 0x20
}
if unit.Parameters["highpass"] == 1 {
flags += 0x10
}
if unit.Parameters["negbandpass"] == 1 {
flags += 0x08
}
if unit.Parameters["neghighpass"] == 1 {
flags += 0x04
}
values = append(values, byte(flags))
} else if unit.Type == "send" {
address := ((unit.Parameters["unit"] + 1) << 4) + unit.Parameters["port"] // each unit is 16 dwords, 8 workspace followed by 8 ports. +1 is for skipping the note/release/inputs
if unit.Parameters["voice"] > 0 {
address += 0x8000 + 16 + (unit.Parameters["voice"]-1)*1024 // global send, +16 is for skipping the out/aux ports
}
if unit.Parameters["sendpop"] == 1 {
address += 0x8
}
values = append(values, byte(address&255), byte(address>>8))
} else if unit.Type == "delay" {
countTrack := (unit.Parameters["count"] << 1) - 1 + unit.Parameters["notetracking"] // 1 means no note tracking and 1 delay, 2 means notetracking with 1 delay, 3 means no note tracking and 2 delays etc.
values = append(values, byte(unit.Parameters["delay"]), byte(countTrack))
}
return byte(opcode + unit.Parameters["stereo"]), values, nil
}

206
go4k/compiler/featureset.go Normal file
View File

@ -0,0 +1,206 @@
package compiler
import (
"sort"
"github.com/vsariola/sointu/go4k"
)
// FeatureSet defines what opcodes / parameters are included in the compiled virtual machine
// It is used by the compiler to decide how to encode opcodes
type FeatureSet interface {
Opcode(unitType string) (int, bool)
TransformCount(unitType string) int
Instructions() []string
InputNumber(unitType string, paramName string) int
SupportsParamValue(unitType string, paramName string, value int) bool
SupportsParamValueOtherThan(unitType string, paramName string, value int) bool
SupportsModulation(unitType string, paramName string) bool
SupportsPolyphony() bool
}
type Instruction struct {
Name string
TransformCount int
}
type paramKey struct {
Unit string
Param string
}
type paramValueKey struct {
Unit string
Param string
Value int
}
// AllFeatures is used by the library compilation / bridging to configure a virtual machine
// that supports every conceivable parameter, so it needs no members and just returns "true" to all
// queries about what it supports. Contrast this NecessaryFeatures that only returns true if the patch
// needs support for that feature
type AllFeatures struct {
}
func (_ AllFeatures) SupportsParamValue(unit string, paramName string, value int) bool {
return true
}
func (_ AllFeatures) SupportsParamValueOtherThan(unit string, paramName string, value int) bool {
return true
}
func (_ AllFeatures) SupportsModulation(unit string, port string) bool {
return true
}
func (_ AllFeatures) SupportsPolyphony() bool {
return true
}
func (_ AllFeatures) Opcode(unitType string) (int, bool) {
code, ok := allOpcodes[unitType]
return code, ok
}
func (_ AllFeatures) TransformCount(unitType string) int {
return allTransformCounts[unitType]
}
func (_ AllFeatures) Instructions() []string {
return allInstructions
}
func (_ AllFeatures) InputNumber(unitType string, paramName string) int {
return allInputs[paramKey{unitType, paramName}]
}
var allOpcodes map[string]int
var allInstructions []string
var allInputs map[paramKey]int
var allTransformCounts map[string]int
func init() {
allInstructions = make([]string, len(go4k.UnitTypes))
allOpcodes = map[string]int{}
allTransformCounts = map[string]int{}
allInputs = map[paramKey]int{}
i := 0
for k, v := range go4k.UnitTypes {
inputCount := 0
transformCount := 0
for _, t := range v {
if t.CanModulate {
allInputs[paramKey{k, t.Name}] = inputCount
inputCount++
}
if t.CanModulate && t.CanSet {
transformCount++
}
}
allInstructions[i] = k // Opcode 0 is reserved for instrument advance, so opcodes start from 1
allTransformCounts[k] = transformCount
i++
}
sort.Strings(allInstructions) // sort the opcodes to have predictable ordering, as maps don't guarantee the order the items
for i, instruction := range allInstructions {
allOpcodes[instruction] = (i + 1) * 2 // make a map to find out the opcode number based on the type
}
}
// NecessaryFeatures returns true only if the patch actually needs the support for the feature
type NecessaryFeatures struct {
opcodes map[string]int
instructions []string
supportsParamValue map[paramKey](map[int]bool)
supportsModulation map[paramKey]bool
polyphony bool
}
func NecessaryFeaturesFor(patch go4k.Patch) NecessaryFeatures {
features := NecessaryFeatures{opcodes: map[string]int{}, supportsParamValue: map[paramKey](map[int]bool){}, supportsModulation: map[paramKey]bool{}}
for instrNo, instrument := range patch.Instruments {
for _, unit := range instrument.Units {
if _, ok := features.opcodes[unit.Type]; !ok {
features.instructions = append(features.instructions, unit.Type)
features.opcodes[unit.Type] = len(features.instructions) * 2 // note that the first opcode gets value 1, as 0 is always reserved for advance
}
for k, v := range unit.Parameters {
key := paramKey{unit.Type, k}
if features.supportsParamValue[key] == nil {
features.supportsParamValue[key] = map[int]bool{}
}
features.supportsParamValue[key][v] = true
}
if unit.Type == "send" {
targetInstrument := instrNo
if unit.Parameters["voice"] > 0 {
v, err := patch.InstrumentForVoice(unit.Parameters["voice"] - 1)
if err != nil {
continue
}
targetInstrument = v
}
if unit.Parameters["unit"] < 0 || unit.Parameters["unit"] >= len(patch.Instruments[targetInstrument].Units) {
continue // send is modulating outside the range of the target instrument; probably a bug in patch, but at least it's not modulating the uniType we're after
}
targetUnit := patch.Instruments[targetInstrument].Units[unit.Parameters["unit"]]
modulatedPortNo := 0
for _, v := range go4k.UnitTypes[targetUnit.Type] {
if v.CanModulate {
if modulatedPortNo == unit.Parameters["port"] {
features.supportsModulation[paramKey{targetUnit.Type, v.Name}] = true
}
modulatedPortNo++
}
}
}
}
if instrument.NumVoices > 1 {
features.polyphony = true
}
}
return features
}
func (n NecessaryFeatures) SupportsParamValue(unit string, paramName string, value int) bool {
m, ok := n.supportsParamValue[paramKey{unit, paramName}]
if !ok {
return false
}
return m[value]
}
func (n NecessaryFeatures) SupportsParamValueOtherThan(unit string, paramName string, value int) bool {
for paramValue := range n.supportsParamValue[paramKey{unit, paramName}] {
if paramValue != value {
return true
}
}
return false
}
func (n NecessaryFeatures) SupportsModulation(unit string, param string) bool {
return n.supportsModulation[paramKey{unit, param}]
}
func (n NecessaryFeatures) SupportsPolyphony() bool {
return n.polyphony
}
func (n NecessaryFeatures) Opcode(unitType string) (int, bool) {
code, ok := n.opcodes[unitType]
return code, ok
}
func (n NecessaryFeatures) Instructions() []string {
return n.instructions
}
func (n NecessaryFeatures) InputNumber(unitType string, paramName string) int {
return allInputs[paramKey{unitType, paramName}]
}
func (_ NecessaryFeatures) TransformCount(unitType string) int {
return allTransformCounts[unitType]
}

61
go4k/compiler/generate.go Normal file
View File

@ -0,0 +1,61 @@
// The following directive is necessary to make the package coherent:
// +build ignore
// This program generates the library headers and assembly files for the library
package main
import (
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"github.com/vsariola/sointu/go4k/compiler"
)
func main() {
targetArch := flag.String("arch", runtime.GOARCH, "Target architecture. Defaults to Go architecture. Possible values: amd64, 386 (anything else is assumed 386)")
targetOs := flag.String("os", runtime.GOOS, "Target OS. Defaults to current Go OS. Possible values: windows, darwin, linux (anything else is assumed linux)")
flag.Usage = printUsage
flag.Parse()
if flag.NArg() != 1 {
flag.Usage()
os.Exit(0)
}
comp, err := compiler.New()
if err != nil {
fmt.Fprintf(os.Stderr, `error creating compiler: %v`, err)
os.Exit(1)
}
comp.Amd64 = *targetArch == "amd64"
comp.OS = *targetOs
library, err := comp.Library()
if err != nil {
fmt.Fprintf(os.Stderr, `error compiling library: %v`, err)
os.Exit(1)
}
filenames := map[string]string{"h": "sointu.h", "asm": "sointu.asm"}
for t, contents := range library {
filename := filenames[t]
err := ioutil.WriteFile(filepath.Join(flag.Args()[0], filename), []byte(contents), os.ModePerm)
if err != nil {
fmt.Fprintf(os.Stderr, `could not write to file "%v": %v`, filename, err)
os.Exit(1)
}
}
os.Exit(0)
}
func printUsage() {
fmt.Fprintf(os.Stderr, "Sointu command line utility for generating the library .asm and .h files.\nUsage: %s [flags] outputDirectory\n", os.Args[0])
flag.PrintDefaults()
}

View File

@ -1,16 +1,11 @@
package go4k
package compiler
import (
"bytes"
"fmt"
"math"
"path"
"path/filepath"
"runtime"
"strings"
"text/template"
"github.com/Masterminds/sprig"
"github.com/vsariola/sointu/go4k"
)
type OplistEntry struct {
@ -19,141 +14,56 @@ type OplistEntry struct {
}
type Macros struct {
Opcodes []OplistEntry
Polyphony bool
MultivoiceTracks bool
PolyphonyBitmask int
Stacklocs []string
Output16Bit bool
Clip bool
Amd64 bool
OS string
DisableSections bool
Sine int // TODO: how can we elegantly access global constants in template, without wrapping each one by one
Trisaw int
Pulse int
Gate int
Sample int
usesFloatConst map[float32]bool
usesIntConst map[int]bool
floatConsts []float32
intConsts []int
calls map[string]bool
stereo map[string]bool
mono map[string]bool
ops map[string]bool
stackframes map[string][]string
unitInputMap map[string](map[string]int)
Stacklocs []string
Output16Bit bool
Clip bool
Library bool
Sine int // TODO: how can we elegantly access global constants in template, without wrapping each one by one
Trisaw int
Pulse int
Gate int
Sample int
usesFloatConst map[float32]bool
usesIntConst map[int]bool
floatConsts []float32
intConsts []int
calls map[string]bool
stackframes map[string][]string
FeatureSet
Compiler
}
type PlayerMacros struct {
Song *Song
VoiceTrackBitmask int
JumpTable []string
Code []byte
Values []byte
Macros
}
func NewPlayerMacros(song *Song, targetArch string, targetOS string) *PlayerMacros {
unitInputMap := map[string](map[string]int){}
for k, v := range UnitTypes {
inputMap := map[string]int{}
inputCount := 0
for _, t := range v {
if t.CanModulate {
inputMap[t.Name] = inputCount
inputCount++
}
}
unitInputMap[k] = inputMap
func NewMacros(c Compiler, f FeatureSet) *Macros {
return &Macros{
calls: map[string]bool{},
usesFloatConst: map[float32]bool{},
usesIntConst: map[int]bool{},
stackframes: map[string][]string{},
Sine: go4k.Sine,
Trisaw: go4k.Trisaw,
Pulse: go4k.Pulse,
Gate: go4k.Gate,
Sample: go4k.Sample,
Compiler: c,
FeatureSet: f,
}
jumpTable, code, values := song.Patch.Encode()
amd64 := targetArch == "amd64"
p := &PlayerMacros{
Song: song,
JumpTable: jumpTable,
Code: code,
Values: values,
Macros: Macros{
mono: map[string]bool{},
stereo: map[string]bool{},
calls: map[string]bool{},
ops: map[string]bool{},
usesFloatConst: map[float32]bool{},
usesIntConst: map[int]bool{},
stackframes: map[string][]string{},
unitInputMap: unitInputMap,
Amd64: amd64,
OS: targetOS,
Sine: Sine,
Trisaw: Trisaw,
Pulse: Pulse,
Gate: Gate,
Sample: Sample,
}}
for _, track := range song.Tracks {
if track.NumVoices > 1 {
p.MultivoiceTracks = true
}
}
trackVoiceNumber := 0
for _, t := range song.Tracks {
for b := 0; b < t.NumVoices-1; b++ {
p.VoiceTrackBitmask += 1 << trackVoiceNumber
trackVoiceNumber++
}
trackVoiceNumber++ // set all bits except last one
}
totalVoices := 0
for _, instr := range song.Patch.Instruments {
if instr.NumVoices > 1 {
p.Polyphony = true
}
for _, unit := range instr.Units {
if !p.ops[unit.Type] {
p.ops[unit.Type] = true
numParams := 0
for _, v := range UnitTypes[unit.Type] {
if v.CanSet && v.CanModulate {
numParams++
}
}
p.Opcodes = append(p.Opcodes, OplistEntry{
Type: unit.Type,
NumParams: numParams,
})
}
if unit.Parameters["stereo"] == 1 {
p.stereo[unit.Type] = true
} else {
p.mono[unit.Type] = true
}
}
totalVoices += instr.NumVoices
for k := 0; k < instr.NumVoices-1; k++ {
p.PolyphonyBitmask = (p.PolyphonyBitmask << 1) + 1
}
p.PolyphonyBitmask <<= 1
}
p.Output16Bit = song.Output16Bit
return p
}
func (p *Macros) Opcode(t string) bool {
return p.ops[t]
func (p *Macros) HasOp(instruction string) bool {
_, ok := p.Opcode(instruction)
return ok
}
func (p *Macros) Stereo(t string) bool {
return p.stereo[t]
func (p *Macros) Stereo(unitType string) bool {
return p.SupportsParamValue(unitType, "stereo", 1)
}
func (p *Macros) Mono(t string) bool {
return p.mono[t]
func (p *Macros) Mono(unitType string) bool {
return p.SupportsParamValue(unitType, "stereo", 0)
}
func (p *Macros) StereoAndMono(t string) bool {
return p.stereo[t] && p.mono[t]
func (p *Macros) StereoAndMono(unitType string) bool {
return p.Stereo(unitType) && p.Mono(unitType)
}
// Macros and functions to accumulate constants automagically
@ -431,6 +341,22 @@ func (p *Macros) Pop(register string) string {
return fmt.Sprintf("pop %v ; %v = %v, Stack: %v ", register, register, last, p.FmtStack())
}
func (p *Macros) SaveFPUState() string {
i := 0
for ; i < 108; i += p.PTRSIZE() {
p.Stacklocs = append(p.Stacklocs, fmt.Sprintf("F%v", i))
}
return fmt.Sprintf("sub %[1]v, %[2]v\nfsave [%[1]v]", p.SP(), i)
}
func (p *Macros) LoadFPUState() string {
i := 0
for ; i < 108; i += p.PTRSIZE() {
p.Stacklocs = p.Stacklocs[:len(p.Stacklocs)-1]
}
return fmt.Sprintf("frstor [%[1]v]\nadd %[1]v, %[2]v", p.SP(), i)
}
func (p *Macros) Stack(name string) (string, error) {
for i, k := range p.Stacklocs {
if k == name {
@ -463,7 +389,11 @@ func (p *Macros) FmtStack() string {
func (p *Macros) ExportFunc(name string, params ...string) string {
if !p.Amd64 {
p.Stacklocs = append(params, "retaddr_"+name) // in 32-bit, we use stdcall and parameters are in the stack
reverseParams := make([]string, len(params))
for i, param := range params {
reverseParams[len(params)-1-i] = param
}
p.Stacklocs = append(reverseParams, "retaddr_"+name) // in 32-bit, we use stdcall and parameters are in the stack
if p.OS == "windows" {
return fmt.Sprintf("%[1]v\nglobal _%[2]v@%[3]v\n_%[2]v@%[3]v:", p.SectText(name), name, len(params)*4)
}
@ -474,54 +404,16 @@ func (p *Macros) ExportFunc(name string, params ...string) string {
return fmt.Sprintf("%[1]v\nglobal %[2]v\n%[2]v:", p.SectText(name), name)
}
func (p *Macros) Count(count int) []int {
s := make([]int, count)
for i := range s {
s[i] = i
}
return s
}
func (p *Macros) Sub(a int, b int) int {
return a - b
}
func (p *Macros) Input(unit string, port string) (string, error) {
umap, ok := p.unitInputMap[unit]
if !ok {
return "", fmt.Errorf(`trying to find input for unknown unit "%v"`, unit)
}
i, ok := umap[port]
if !ok {
return "", fmt.Errorf(`trying to find input for unknown input "%v" for unit "%v"`, port, unit)
}
i := p.InputNumber(unit, port)
if i != 0 {
return fmt.Sprintf("%v + %v", p.INP(), i*4), nil
}
return p.INP(), nil
}
func (p *Macros) InputNumber(unit string, port string) (string, error) {
umap, ok := p.unitInputMap[unit]
if !ok {
return "", fmt.Errorf(`trying to find InputNumber for unknown unit "%v"`, unit)
}
i, ok := umap[port]
if !ok {
return "", fmt.Errorf(`trying to find InputNumber for unknown input "%v" for unit "%v"`, port, unit)
}
return fmt.Sprintf("%v", i), nil
}
func (p *Macros) Modulation(unit string, port string) (string, error) {
umap, ok := p.unitInputMap[unit]
if !ok {
return "", fmt.Errorf(`trying to find input for unknown unit "%v"`, unit)
}
i, ok := umap[port]
if !ok {
return "", fmt.Errorf(`trying to find input for unknown input "%v" for unit "%v"`, port, unit)
}
i := p.InputNumber(unit, port)
return fmt.Sprintf("%v + %v", p.WRK(), i*4+32), nil
}
@ -549,6 +441,32 @@ func (p *Macros) Use(value string, regs ...string) (string, error) {
return value, nil
}
type PlayerMacros struct {
Song *go4k.Song
VoiceTrackBitmask int
MaxSamples int
Macros
EncodedPatch
}
func NewPlayerMacros(c Compiler, f FeatureSet, s *go4k.Song, e *EncodedPatch, maxSamples int) *PlayerMacros {
if maxSamples == 0 {
maxSamples = s.SamplesPerRow() * s.TotalRows()
}
macros := *NewMacros(c, f)
macros.Output16Bit = s.Output16Bit // TODO: should we actually store output16bit in Songs or not?
p := PlayerMacros{Song: s, Macros: macros, MaxSamples: maxSamples, EncodedPatch: *e}
trackVoiceNumber := 0
for _, t := range s.Tracks {
for b := 0; b < t.NumVoices-1; b++ {
p.VoiceTrackBitmask += 1 << trackVoiceNumber
trackVoiceNumber++
}
trackVoiceNumber++ // set all bits except last one
}
return &p
}
func (p *PlayerMacros) NumDelayLines() string {
total := 0
for _, instr := range p.Song.Patch.Instruments {
@ -560,68 +478,3 @@ func (p *PlayerMacros) NumDelayLines() string {
}
return fmt.Sprintf("%v", total)
}
func (p *PlayerMacros) UsesDelayModulation() (bool, error) {
for i, instrument := range p.Song.Patch.Instruments {
for j, unit := range instrument.Units {
if unit.Type == "send" {
targetInstrument := i
if unit.Parameters["voice"] > 0 {
v, err := p.Song.Patch.InstrumentForVoice(unit.Parameters["voice"] - 1)
if err != nil {
return false, 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(p.Song.Patch.Instruments[targetInstrument].Units) {
return false, fmt.Errorf("INSTRUMENT #%v / SEND #%v target unit %v out of range", i, j, unit.Parameters["unit"])
}
if p.Song.Patch.Instruments[targetInstrument].Units[unit.Parameters["unit"]].Type == "delay" && unit.Parameters["port"] == 4 {
return true, nil
}
}
}
}
return false, nil
}
func (p *PlayerMacros) HasParamValue(unitType string, paramName string, value int) bool {
for _, instr := range p.Song.Patch.Instruments {
for _, unit := range instr.Units {
if unit.Type == unitType {
if unit.Parameters[paramName] == value {
return true
}
}
}
}
return false
}
func (p *PlayerMacros) HasParamValueOtherThan(unitType string, paramName string, value int) bool {
for _, instr := range p.Song.Patch.Instruments {
for _, unit := range instr.Units {
if unit.Type == unitType {
if unit.Parameters[paramName] != value {
return true
}
}
}
}
return false
}
func Compile(song *Song, targetArch string, targetOs string) (string, error) {
_, myname, _, _ := runtime.Caller(0)
templateDir := filepath.Join(path.Dir(myname), "..", "templates", "*.asm")
tmpl, err := template.New("base").Funcs(sprig.TxtFuncMap()).ParseGlob(templateDir)
if err != nil {
return "", fmt.Errorf(`could not create template based on dir "%v": %v`, templateDir, err)
}
b := bytes.NewBufferString("")
err = tmpl.ExecuteTemplate(b, "player.asm", NewPlayerMacros(song, targetArch, targetOs))
if err != nil {
return "", fmt.Errorf(`could not execute template "player.asm": %v`, err)
}
return b.String(), nil
}

View File

@ -2,6 +2,7 @@ package go4k
import (
"errors"
"fmt"
"math"
)
@ -9,6 +10,7 @@ import (
type Unit struct {
Type string
Parameters map[string]int `yaml:",flow"`
VarArgs []int
}
const (
@ -25,17 +27,9 @@ type Instrument struct {
Units []Unit
}
type SampleOffset struct {
Start int
LoopStart int
LoopLength int
}
// Patch is simply a list of instruments used in a song
type Patch struct {
Instruments []Instrument
DelayTimes []int `yaml:",flow"`
SampleOffsets []SampleOffset
Instruments []Instrument
}
func (p Patch) TotalVoices() int {
@ -73,30 +67,13 @@ type Synth interface {
func Render(synth Synth, buffer []float32) error {
s, _, err := synth.Render(buffer, math.MaxInt32)
if err != nil {
return fmt.Errorf("go4k.Render failed: %v", err)
}
if s != len(buffer)/2 {
return errors.New("synth.Render should have filled the whole buffer")
return errors.New("in go4k.Render, synth.Render should have filled the whole buffer but did not")
}
return err
}
func (p *Patch) Encode() ([]string, []byte, []byte) {
var code []byte
var values []byte
var jumpTable []string
assignedIds := map[string]byte{}
for _, instr := range p.Instruments {
for _, unit := range instr.Units {
if _, ok := assignedIds[unit.Type]; !ok {
jumpTable = append(jumpTable, unit.Type)
assignedIds[unit.Type] = byte(len(jumpTable) * 2)
}
stereo, unitValues := Encode(unit)
code = append(code, stereo+assignedIds[unit.Type])
values = append(values, unitValues...)
}
code = append(code, 0)
}
return jumpTable, code, values
return nil
}
// UnitParameter documents one parameter that an unit takes
@ -108,70 +85,6 @@ type UnitParameter struct {
CanModulate bool // if this parameter can be modulated i.e. has a port number in "send" unit
}
func Encode(unit Unit) (byte, []byte) {
var values []byte
for _, v := range UnitTypes[unit.Type] {
if v.CanSet && v.CanModulate {
values = append(values, byte(unit.Parameters[v.Name]))
}
}
if unit.Type == "aux" {
values = append(values, byte(unit.Parameters["channel"]))
} else if unit.Type == "in" {
values = append(values, byte(unit.Parameters["channel"]))
} else if unit.Type == "oscillator" {
flags := 0
switch unit.Parameters["type"] {
case Sine:
flags = 0x40
case Trisaw:
flags = 0x20
case Pulse:
flags = 0x10
case Gate:
flags = 0x04
case Sample:
flags = 0x80
}
if unit.Parameters["lfo"] == 1 {
flags += 0x08
}
flags += unit.Parameters["unison"]
values = append(values, byte(flags))
} else if unit.Type == "filter" {
flags := 0
if unit.Parameters["lowpass"] == 1 {
flags += 0x40
}
if unit.Parameters["bandpass"] == 1 {
flags += 0x20
}
if unit.Parameters["highpass"] == 1 {
flags += 0x10
}
if unit.Parameters["negbandpass"] == 1 {
flags += 0x08
}
if unit.Parameters["neghighpass"] == 1 {
flags += 0x04
}
values = append(values, byte(flags))
} else if unit.Type == "send" {
address := ((unit.Parameters["unit"] + 1) << 4) + unit.Parameters["port"] // each unit is 16 dwords, 8 workspace followed by 8 ports. +1 is for skipping the note/release/inputs
if unit.Parameters["voice"] > 0 {
address += 0x8000 + 16 + (unit.Parameters["voice"]-1)*1024 // global send, +16 is for skipping the out/aux ports
}
if unit.Parameters["sendpop"] == 1 {
address += 0x8
}
values = append(values, byte(address&255), byte(address>>8))
} else if unit.Type == "delay" {
countTrack := (unit.Parameters["count"] << 1) - 1 + unit.Parameters["notetracking"] // 1 means no note tracking and 1 delay, 2 means notetracking with 1 delay, 3 means no note tracking and 2 delays etc.
values = append(values, byte(unit.Parameters["delay"]), byte(countTrack))
}
return byte(unit.Parameters["stereo"]), values
}
// UnitTypes documents all the available unit types and if they support stereo variant
// and what parameters they take.
var UnitTypes = map[string]([]UnitParameter){
@ -218,8 +131,6 @@ var UnitTypes = map[string]([]UnitParameter){
{Name: "feedback", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "damp", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "notetracking", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "delay", MinValue: 0, MaxValue: 255, CanSet: true, CanModulate: false},
{Name: "count", MinValue: 0, MaxValue: 255, CanSet: true, CanModulate: false},
{Name: "delaytime", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true}},
"compressor": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
@ -268,7 +179,10 @@ var UnitTypes = map[string]([]UnitParameter){
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "type", MinValue: int(Sine), MaxValue: int(Sample), CanSet: true, CanModulate: false},
{Name: "lfo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "unison", MinValue: 0, MaxValue: 3, CanSet: true, CanModulate: false}},
{Name: "unison", MinValue: 0, MaxValue: 3, CanSet: true, CanModulate: false},
{Name: "samplestart", MinValue: 0, MaxValue: 3440659, CanSet: true, CanModulate: false},
{Name: "loopstart", MinValue: 0, MaxValue: 65535, CanSet: true, CanModulate: false},
{Name: "looplength", MinValue: 0, MaxValue: 65535, CanSet: true, CanModulate: false}},
"loadval": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "value", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
@ -280,3 +194,133 @@ var UnitTypes = map[string]([]UnitParameter){
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false}},
}
type Song struct {
BPM int
Output16Bit bool
Hold byte
Patterns [][]byte `yaml:",flow"`
Tracks []Track
Patch Patch
}
func (s *Song) PatternRows() int {
return len(s.Patterns[0])
}
func (s *Song) SequenceLength() int {
return len(s.Tracks[0].Sequence)
}
func (s *Song) TotalRows() int {
return s.PatternRows() * s.SequenceLength()
}
func (s *Song) SamplesPerRow() int {
return 44100 * 60 / (s.BPM * 4)
}
func (s *Song) FirstTrackVoice(track int) int {
ret := 0
for _, t := range s.Tracks[:track] {
ret += t.NumVoices
}
return ret
}
// TBD: Where shall we put methods that work on pure domain types and have no dependencies
// e.g. Validate here
func (s *Song) Validate() error {
if s.BPM < 1 {
return errors.New("BPM should be > 0")
}
for i := range s.Patterns[:len(s.Patterns)-1] {
if len(s.Patterns[i]) != len(s.Patterns[i+1]) {
return errors.New("Every pattern should have the same length")
}
}
for i := range s.Tracks[:len(s.Tracks)-1] {
if len(s.Tracks[i].Sequence) != len(s.Tracks[i+1].Sequence) {
return errors.New("Every track should have the same sequence length")
}
}
totalTrackVoices := 0
for _, track := range s.Tracks {
totalTrackVoices += track.NumVoices
for _, p := range track.Sequence {
if p < 0 || int(p) >= len(s.Patterns) {
return errors.New("Tracks use a non-existing pattern")
}
}
}
if totalTrackVoices > s.Patch.TotalVoices() {
return errors.New("Tracks use too many voices")
}
return nil
}
func Play(synth Synth, song Song) ([]float32, error) {
err := song.Validate()
if err != nil {
return nil, err
}
curVoices := make([]int, len(song.Tracks))
for i := range curVoices {
curVoices[i] = song.FirstTrackVoice(i)
}
initialCapacity := song.TotalRows() * song.SamplesPerRow() * 2
buffer := make([]float32, 0, initialCapacity)
rowbuffer := make([]float32, song.SamplesPerRow()*2)
for row := 0; row < song.TotalRows(); row++ {
patternRow := row % song.PatternRows()
pattern := row / song.PatternRows()
for t := range song.Tracks {
patternIndex := song.Tracks[t].Sequence[pattern]
note := song.Patterns[patternIndex][patternRow]
if note > 0 && note <= song.Hold { // anything but hold causes an action.
continue
}
synth.Release(curVoices[t])
if note > song.Hold {
curVoices[t]++
first := song.FirstTrackVoice(t)
if curVoices[t] >= first+song.Tracks[t].NumVoices {
curVoices[t] = first
}
synth.Trigger(curVoices[t], note)
}
}
tries := 0
for rowtime := 0; rowtime < song.SamplesPerRow(); {
samples, time, _ := synth.Render(rowbuffer, song.SamplesPerRow()-rowtime)
rowtime += time
buffer = append(buffer, rowbuffer[:samples*2]...)
if tries > 100 {
return nil, fmt.Errorf("Song speed modulation likely so slow that row never advances; error at pattern %v, row %v", pattern, patternRow)
}
}
}
return buffer, nil
}
func (s *Song) UpdateHold(newHold byte) error {
if newHold == 0 {
return errors.New("hold value cannot be 0, 0 is reserved for release")
}
for _, pat := range s.Patterns {
for _, v := range pat {
if v > s.Hold && v <= newHold {
return errors.New("song uses note values greater or equal to the new hold value")
}
}
}
for _, pat := range s.Patterns {
for i, v := range pat {
if v > 0 && v <= s.Hold {
pat[i] = newHold
}
}
}
s.Hold = newHold
return nil
}

View File

@ -1,136 +0,0 @@
package go4k
import (
"errors"
"fmt"
)
type Song struct {
BPM int
Output16Bit bool
Hold byte
Patterns [][]byte `yaml:",flow"`
Tracks []Track
Patch Patch
}
func (s *Song) PatternRows() int {
return len(s.Patterns[0])
}
func (s *Song) SequenceLength() int {
return len(s.Tracks[0].Sequence)
}
func (s *Song) TotalRows() int {
return s.PatternRows() * s.SequenceLength()
}
func (s *Song) SamplesPerRow() int {
return 44100 * 60 / (s.BPM * 4)
}
func (s *Song) FirstTrackVoice(track int) int {
ret := 0
for _, t := range s.Tracks[:track] {
ret += t.NumVoices
}
return ret
}
// TBD: Where shall we put methods that work on pure domain types and have no dependencies
// e.g. Validate here
func (s *Song) Validate() error {
if s.BPM < 1 {
return errors.New("BPM should be > 0")
}
for i := range s.Patterns[:len(s.Patterns)-1] {
if len(s.Patterns[i]) != len(s.Patterns[i+1]) {
return errors.New("Every pattern should have the same length")
}
}
for i := range s.Tracks[:len(s.Tracks)-1] {
if len(s.Tracks[i].Sequence) != len(s.Tracks[i+1].Sequence) {
return errors.New("Every track should have the same sequence length")
}
}
totalTrackVoices := 0
for _, track := range s.Tracks {
totalTrackVoices += track.NumVoices
for _, p := range track.Sequence {
if p < 0 || int(p) >= len(s.Patterns) {
return errors.New("Tracks use a non-existing pattern")
}
}
}
if totalTrackVoices > s.Patch.TotalVoices() {
return errors.New("Tracks use too many voices")
}
return nil
}
func Play(synth Synth, song Song) ([]float32, error) {
err := song.Validate()
if err != nil {
return nil, err
}
curVoices := make([]int, len(song.Tracks))
for i := range curVoices {
curVoices[i] = song.FirstTrackVoice(i)
}
initialCapacity := song.TotalRows() * song.SamplesPerRow() * 2
buffer := make([]float32, 0, initialCapacity)
rowbuffer := make([]float32, song.SamplesPerRow()*2)
for row := 0; row < song.TotalRows(); row++ {
patternRow := row % song.PatternRows()
pattern := row / song.PatternRows()
for t := range song.Tracks {
patternIndex := song.Tracks[t].Sequence[pattern]
note := song.Patterns[patternIndex][patternRow]
if note > 0 && note <= song.Hold { // anything but hold causes an action.
continue
}
synth.Release(curVoices[t])
if note > song.Hold {
curVoices[t]++
first := song.FirstTrackVoice(t)
if curVoices[t] >= first+song.Tracks[t].NumVoices {
curVoices[t] = first
}
synth.Trigger(curVoices[t], note)
}
}
tries := 0
for rowtime := 0; rowtime < song.SamplesPerRow(); {
samples, time, _ := synth.Render(rowbuffer, song.SamplesPerRow()-rowtime)
rowtime += time
buffer = append(buffer, rowbuffer[:samples*2]...)
if tries > 100 {
return nil, fmt.Errorf("Song speed modulation likely so slow that row never advances; error at pattern %v, row %v", pattern, patternRow)
}
}
}
return buffer, nil
}
func (s *Song) UpdateHold(newHold byte) error {
if newHold == 0 {
return errors.New("hold value cannot be 0, 0 is reserved for release")
}
for _, pat := range s.Patterns {
for _, v := range pat {
if v > s.Hold && v <= newHold {
return errors.New("song uses note values greater or equal to the new hold value")
}
}
}
for _, pat := range s.Patterns {
for i, v := range pat {
if v > 0 && v <= s.Hold {
pat[i] = newHold
}
}
}
s.Hold = newHold
return nil
}

View File

@ -1,54 +0,0 @@
package go4k_test
import (
"encoding/json"
"reflect"
"testing"
"github.com/vsariola/sointu/go4k"
)
const expectedMarshaled = `{"BPM":100,"Output16Bit":false,"Hold":1,"Patterns":["QABEACAAAABLAE4AAAAAAA=="],"Tracks":[{"NumVoices":1,"Sequence":"AA=="}],"Patch":{"Instruments":[{"NumVoices":1,"Units":[{"Type":"envelope","Parameters":{"attack":32,"decay":32,"gain":128,"release":64,"stereo":0,"sustain":64}},{"Type":"oscillator","Parameters":{"color":96,"detune":64,"flags":64,"gain":128,"phase":0,"shape":64,"stereo":0,"transpose":64}},{"Type":"mulp","Parameters":{"stereo":0}},{"Type":"envelope","Parameters":{"attack":32,"decay":32,"gain":128,"release":64,"stereo":0,"sustain":64}},{"Type":"oscillator","Parameters":{"color":64,"detune":64,"flags":64,"gain":128,"phase":64,"shape":96,"stereo":0,"transpose":72}},{"Type":"mulp","Parameters":{"stereo":0}},{"Type":"out","Parameters":{"gain":128,"stereo":1}}]}],"DelayTimes":[],"SampleOffsets":[]}}`
var testSong = go4k.Song{
BPM: 100,
Patterns: [][]byte{{64, 0, 68, 0, 32, 0, 0, 0, 75, 0, 78, 0, 0, 0, 0, 0}},
Tracks: []go4k.Track{
{NumVoices: 1, Sequence: []byte{0}},
},
Patch: go4k.Patch{
Instruments: []go4k.Instrument{{NumVoices: 1, Units: []go4k.Unit{
{"envelope", map[string]int{"stereo": 0, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
{"oscillator", map[string]int{"stereo": 0, "transpose": 64, "detune": 64, "phase": 0, "color": 96, "shape": 64, "gain": 128, "flags": 0x40}},
{"mulp", map[string]int{"stereo": 0}},
{"envelope", map[string]int{"stereo": 0, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
{"oscillator", map[string]int{"stereo": 0, "transpose": 72, "detune": 64, "phase": 64, "color": 64, "shape": 96, "gain": 128, "flags": 0x40}},
{"mulp", map[string]int{"stereo": 0}},
{"out", map[string]int{"stereo": 1, "gain": 128}},
}}},
DelayTimes: []int{},
SampleOffsets: []go4k.SampleOffset{},
},
Hold: 1,
}
func TestSongMarshalJSON(t *testing.T) {
songbytes, err := json.Marshal(testSong)
if err != nil {
t.Fatalf("cannot marshal song: %v", err)
}
if string(songbytes) != expectedMarshaled {
t.Fatalf("expectedMarshaled song to unexpected result, got %v, expected %v", string(songbytes), expectedMarshaled)
}
}
func TestSongUnmarshalJSON(t *testing.T) {
var song go4k.Song
err := json.Unmarshal([]byte(expectedMarshaled), &song)
if err != nil {
t.Fatalf("cannot unmarshal song: %v", err)
}
if !reflect.DeepEqual(song, testSong) {
t.Fatalf("unmarshaled song to unexpected result, got %#v, expected %#v", song, testSong)
}
}

View File

@ -14,15 +14,12 @@ var defaultSong = go4k.Song{
},
Patch: go4k.Patch{
Instruments: []go4k.Instrument{{NumVoices: 2, Units: []go4k.Unit{
{"envelope", map[string]int{"stereo": 0, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
{"oscillator", map[string]int{"stereo": 0, "transpose": 64, "detune": 64, "phase": 0, "color": 96, "shape": 64, "gain": 128, "type": go4k.Sine}},
{"mulp", map[string]int{"stereo": 0}},
{"envelope", map[string]int{"stereo": 0, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
{"oscillator", map[string]int{"stereo": 0, "transpose": 72, "detune": 64, "phase": 64, "color": 64, "shape": 96, "gain": 128, "type": go4k.Sine}},
{"mulp", map[string]int{"stereo": 0}},
{"out", map[string]int{"stereo": 1, "gain": 128}},
}}},
DelayTimes: []int{},
SampleOffsets: []go4k.SampleOffset{},
},
{Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
{Type: "oscillator", Parameters: map[string]int{"stereo": 0, "transpose": 64, "detune": 64, "phase": 0, "color": 96, "shape": 64, "gain": 128, "type": go4k.Sine}},
{Type: "mulp", Parameters: map[string]int{"stereo": 0}},
{Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
{Type: "oscillator", Parameters: map[string]int{"stereo": 0, "transpose": 72, "detune": 64, "phase": 64, "color": 64, "shape": 96, "gain": 128, "type": go4k.Sine}},
{Type: "mulp", Parameters: map[string]int{"stereo": 0}},
{Type: "out", Parameters: map[string]int{"stereo": 1, "gain": 128}},
}}}},
}

View File

@ -1,173 +0,0 @@
SECT_TEXT(suarithm)
;-------------------------------------------------------------------------------
; POP opcode: remove (discard) the topmost signal from the stack
;-------------------------------------------------------------------------------
; Mono: a -> (empty)
; Stereo: a b -> (empty)
;-------------------------------------------------------------------------------
%if POP_ID > -1
EXPORT MANGLE_FUNC(su_op_pop,0)
%ifdef INCLUDE_STEREO_POP
jnc su_op_pop_mono
fstp st0
su_op_pop_mono:
%endif
fstp st0
ret
%endif
;-------------------------------------------------------------------------------
; ADD opcode: add the two top most signals on the stack
;-------------------------------------------------------------------------------
; Mono: a b -> a+b b
; Stereo: a b c d -> a+c b+d c d
;-------------------------------------------------------------------------------
%if ADD_ID > -1
EXPORT MANGLE_FUNC(su_op_add,0)
%ifdef INCLUDE_STEREO_ADD
jnc su_op_add_mono
fadd st0, st2
fxch
fadd st0, st3
fxch
ret
su_op_add_mono:
%endif
fadd st1
ret
%endif
;-------------------------------------------------------------------------------
; ADDP opcode: add the two top most signals on the stack and pop
;-------------------------------------------------------------------------------
; Mono: a b -> a+b
; Stereo: a b c d -> a+c b+d
;-------------------------------------------------------------------------------
%if ADDP_ID > -1
EXPORT MANGLE_FUNC(su_op_addp,0)
%ifdef INCLUDE_STEREO_ADDP
jnc su_op_addp_mono
faddp st2, st0
faddp st2, st0
ret
su_op_addp_mono:
%endif
faddp st1, st0
ret
%endif
;-------------------------------------------------------------------------------
; LOADNOTE opcode: load the current note, scaled to [-1,1]
;-------------------------------------------------------------------------------
; Mono: (empty) -> n, where n is the note
; Stereo: (empty) -> n n
;-------------------------------------------------------------------------------
%if LOADNOTE_ID > -1
EXPORT MANGLE_FUNC(su_op_loadnote,0)
%ifdef INCLUDE_STEREO_LOADNOTE
jnc su_op_loadnote_mono
call su_op_loadnote_mono
su_op_loadnote_mono:
%endif
fild dword [INP-su_voice.inputs+su_voice.note]
do fmul dword [,c_i128,]
do fsub dword [,c_0_5,] ; s-.5
fadd st0, st0 ; 2*s-1
ret
%endif
;-------------------------------------------------------------------------------
; MUL opcode: multiply the two top most signals on the stack
;-------------------------------------------------------------------------------
; Mono: a b -> a*b a
; Stereo: a b c d -> a*c b*d c d
;-------------------------------------------------------------------------------
%if MUL_ID > -1
EXPORT MANGLE_FUNC(su_op_mul,0)
%ifdef INCLUDE_STEREO_MUL
jnc su_op_mul_mono
fmul st0, st2
fxch
fadd st0, st3
fxch
ret
su_op_mul_mono:
%endif
fmul st1
ret
%endif
;-------------------------------------------------------------------------------
; MULP opcode: multiply the two top most signals on the stack and pop
;-------------------------------------------------------------------------------
; Mono: a b -> a*b
; Stereo: a b c d -> a*c b*d
;-------------------------------------------------------------------------------
%if MULP_ID > -1
EXPORT MANGLE_FUNC(su_op_mulp,0)
%ifdef INCLUDE_STEREO_MULP
jnc su_op_mulp_mono
fmulp st2, st0
fmulp st2, st0
ret
su_op_mulp_mono:
%endif
fmulp st1
ret
%endif
;-------------------------------------------------------------------------------
; PUSH opcode: push the topmost signal on the stack
;-------------------------------------------------------------------------------
; Mono: a -> a a
; Stereo: a b -> a b a b
;-------------------------------------------------------------------------------
%if PUSH_ID > -1
EXPORT MANGLE_FUNC(su_op_push,0)
%ifdef INCLUDE_STEREO_PUSH
jnc su_op_push_mono
fld st1
fld st1
ret
su_op_push_mono:
%endif
fld st0
ret
%endif
;-------------------------------------------------------------------------------
; XCH opcode: exchange the signals on the stack
;-------------------------------------------------------------------------------
; Mono: a b -> b a
; stereo: a b c d -> c d a b
;-------------------------------------------------------------------------------
%if XCH_ID > -1
EXPORT MANGLE_FUNC(su_op_xch,0)
%ifdef INCLUDE_STEREO_XCH
jnc su_op_xch_mono
fxch st0, st2 ; c b a d
fxch st0, st1 ; b c a d
fxch st0, st3 ; d c a b
su_op_xch_mono:
%endif
fxch st0, st1
ret
%endif

View File

@ -1,168 +0,0 @@
;-------------------------------------------------------------------------------
; ADDP related defines
;-------------------------------------------------------------------------------
%assign ADDP_ID -1
%macro USE_ADDP 0
%if ADDP_ID == -1
%assign ADDP_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_addp,0),
%xdefine NUMPARAMS NUMPARAMS 0,
%endif
%endmacro
%macro SU_ADDP 1
USE_ADDP
%xdefine CMDS CMDS ADDP_ID + %1,
%if %1 == 1
%define INCLUDE_STEREO_ADDP
%endif
%endmacro
;-------------------------------------------------------------------------------
; ADD related defines
;-------------------------------------------------------------------------------
%assign ADD_ID -1
%macro USE_ADD 0
%if ADD_ID == -1
%assign ADD_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_add,0),
%xdefine NUMPARAMS NUMPARAMS 0,
%endif
%endmacro
%assign ADD_ID -1
%macro SU_ADD 1
USE_ADD
%xdefine CMDS CMDS ADD_ID + %1,
%if %1 == 1
%define INCLUDE_STEREO_ADD
%endif
%endmacro
;-------------------------------------------------------------------------------
; POP related defines
;-------------------------------------------------------------------------------
%assign POP_ID -1
%macro USE_POP 0
%if POP_ID == -1
%assign POP_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_pop,0),
%xdefine NUMPARAMS NUMPARAMS 0,
%endif
%endmacro
%macro SU_POP 1
USE_POP
%xdefine CMDS CMDS POP_ID + %1,
%if %1 == 1
%define INCLUDE_STEREO_POP
%endif
%endmacro
;-------------------------------------------------------------------------------
; LOADNOTE related defines
;-------------------------------------------------------------------------------
%assign LOADNOTE_ID -1
%macro USE_LOADNOTE 0
%if LOADNOTE_ID == -1
%assign LOADNOTE_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_loadnote,0),
%xdefine NUMPARAMS NUMPARAMS 0,
%endif
%endmacro
%macro SU_LOADNOTE 1
USE_LOADNOTE
%xdefine CMDS CMDS LOADNOTE_ID + %1,
%if %1 == 1
%define INCLUDE_STEREO_LOADNOTE
%endif
%endmacro
;-------------------------------------------------------------------------------
; MUL related defines
;-------------------------------------------------------------------------------
%assign MUL_ID -1
%macro USE_MUL 0
%if MUL_ID == -1
%assign MUL_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_mul,0),
%xdefine NUMPARAMS NUMPARAMS 0,
%endif
%endmacro
%macro SU_MUL 1
USE_MUL
%xdefine CMDS CMDS MUL_ID + %1,
%if %1 == 1
%define INCLUDE_STEREO_MUL
%endif
%endmacro
;-------------------------------------------------------------------------------
; MULP related defines
;-------------------------------------------------------------------------------
%assign MULP_ID -1
%macro USE_MULP 0
%if MULP_ID == -1
%assign MULP_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_mulp,0),
%xdefine NUMPARAMS NUMPARAMS 0,
%endif
%endmacro
%macro SU_MULP 1
USE_MULP
%xdefine CMDS CMDS MULP_ID + %1,
%if %1 == 1
%define INCLUDE_STEREO_MULP
%endif
%endmacro
;-------------------------------------------------------------------------------
; PUSH related defines
;-------------------------------------------------------------------------------
%assign PUSH_ID -1
%macro USE_PUSH 0
%if PUSH_ID == -1
%assign PUSH_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_push,0),
%xdefine NUMPARAMS NUMPARAMS 0,
%endif
%endmacro
%macro SU_PUSH 1
USE_PUSH
%xdefine CMDS CMDS PUSH_ID + %1,
%if %1 == 1
%define INCLUDE_STEREO_PUSH
%endif
%endmacro
;-------------------------------------------------------------------------------
; XCH related defines
;-------------------------------------------------------------------------------
%assign XCH_ID -1
%macro USE_XCH 0
%if XCH_ID == -1
%assign XCH_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_xch,0),
%xdefine NUMPARAMS NUMPARAMS 0,
%endif
%endmacro
%macro SU_XCH 1
USE_XCH
%xdefine CMDS CMDS XCH_ID + %1,
%if %1 == 1
%define INCLUDE_STEREO_XCH
%endif
%endmacro

View File

@ -1,478 +0,0 @@
;-------------------------------------------------------------------------------
; DISTORT opcode: apply distortion on the signal
;-------------------------------------------------------------------------------
; Mono: x -> x*a/(1-a+(2*a-1)*abs(x)) where x is clamped first
; Stereo: l r -> l*a/(1-a+(2*a-1)*abs(l)) r*a/(1-a+(2*a-1)*abs(r))
;-------------------------------------------------------------------------------
%if DISTORT_ID > -1
SECT_TEXT(sudistrt)
EXPORT MANGLE_FUNC(su_op_distort,0)
%ifdef INCLUDE_STEREO_DISTORT
call su_effects_stereohelper
%define INCLUDE_EFFECTS_STEREOHELPER
%endif
fld dword [INP+su_distort_ports.drive]
%define SU_INCLUDE_WAVESHAPER
; flow into waveshaper
%endif
%ifdef SU_INCLUDE_WAVESHAPER
su_waveshaper:
fxch ; x a
call su_clip
fxch ; a x' (from now on just called x)
fld st0 ; a a x
do fsub dword [,c_0_5,] ; a-.5 a x
fadd st0 ; 2*a-1 a x
fld st2 ; x 2*a-1 a x
fabs ; abs(x) 2*a-1 a x
fmulp st1 ; (2*a-1)*abs(x) a x
fld1 ; 1 (2*a-1)*abs(x) a x
faddp st1 ; 1+(2*a-1)*abs(x) a x
fsub st1 ; 1-a+(2*a-1)*abs(x) a x
fdivp st1, st0 ; a/(1-a+(2*a-1)*abs(x)) x
fmulp st1 ; x*a/(1-a+(2*a-1)*abs(x))
ret
%define SU_INCLUDE_CLIP
%endif ; SU_USE_DST
;-------------------------------------------------------------------------------
; HOLD opcode: sample and hold the signal, reducing sample rate
;-------------------------------------------------------------------------------
; Mono version: holds the signal at a rate defined by the freq parameter
; Stereo version: holds both channels
;-------------------------------------------------------------------------------
%if HOLD_ID > -1
SECT_TEXT(suhold)
EXPORT MANGLE_FUNC(su_op_hold,0)
%ifdef INCLUDE_STEREO_HOLD
call su_effects_stereohelper
%define INCLUDE_EFFECTS_STEREOHELPER
%endif
fld dword [INP+su_hold_ports.freq] ; f x
fmul st0, st0 ; f^2 x
fchs ; -f^2 x
fadd dword [WRK+su_hold_wrk.phase] ; p-f^2 x
fst dword [WRK+su_hold_wrk.phase] ; p <- p-f^2
fldz ; 0 p x
fucomip st1 ; p x
fstp dword [_SP-4] ; t=p, x
jc short su_op_hold_holding ; if (0 < p) goto holding
fld1 ; 1 x
fadd dword [_SP-4] ; 1+t x
fstp dword [WRK+su_hold_wrk.phase] ; x
fst dword [WRK+su_hold_wrk.holdval] ; save holded value
ret ; x
su_op_hold_holding:
fstp st0 ;
fld dword [WRK+su_hold_wrk.holdval] ; x
ret
%endif ; HOLD_ID > -1
;-------------------------------------------------------------------------------
; CRUSH opcode: quantize the signal to finite number of levels
;-------------------------------------------------------------------------------
; Mono: x -> e*int(x/e)
; Stereo: l r -> e*int(l/e) e*int(r/e)
;-------------------------------------------------------------------------------
%if CRUSH_ID > -1
SECT_TEXT(sucrush)
EXPORT MANGLE_FUNC(su_op_crush,0)
%ifdef INCLUDE_STEREO_CRUSH
call su_effects_stereohelper
%define INCLUDE_EFFECTS_STEREOHELPER
%endif
fdiv dword [INP+su_crush_ports.resolution]
frndint
fmul dword [INP+su_crush_ports.resolution]
ret
%endif ; CRUSH_ID > -1
;-------------------------------------------------------------------------------
; GAIN opcode: apply gain on the signal
;-------------------------------------------------------------------------------
; Mono: x -> x*g
; Stereo: l r -> l*g r*g
;-------------------------------------------------------------------------------
%if GAIN_ID > -1
SECT_TEXT(sugain)
%ifdef INCLUDE_STEREO_GAIN
EXPORT MANGLE_FUNC(su_op_gain,0)
fld dword [INP+su_gain_ports.gain] ; g l (r)
jnc su_op_gain_mono
fmul st2, st0 ; g l r/g
su_op_gain_mono:
fmulp st1, st0 ; l/g (r/)
ret
%else
EXPORT MANGLE_FUNC(su_op_gain,0)
fmul dword [INP+su_gain_ports.gain]
ret
%endif
%endif ; GAIN_ID > -1
;-------------------------------------------------------------------------------
; INVGAIN opcode: apply inverse gain on the signal
;-------------------------------------------------------------------------------
; Mono: x -> x/g
; Stereo: l r -> l/g r/g
;-------------------------------------------------------------------------------
%if INVGAIN_ID > -1
SECT_TEXT(suingain)
%ifdef INCLUDE_STEREO_INVGAIN
EXPORT MANGLE_FUNC(su_op_invgain,0)
fld dword [INP+su_invgain_ports.invgain] ; g l (r)
jnc su_op_invgain_mono
fdiv st2, st0 ; g l r/g
su_op_invgain_mono:
fdivp st1, st0 ; l/g (r/)
ret
%else
EXPORT MANGLE_FUNC(su_op_invgain,0)
fdiv dword [INP+su_invgain_ports.invgain]
ret
%endif
%endif ; INVGAIN_ID > -1
;-------------------------------------------------------------------------------
; FILTER opcode: perform low/high/band-pass/notch etc. filtering on the signal
;-------------------------------------------------------------------------------
; Mono: x -> filtered(x)
; Stereo: l r -> filtered(l) filtered(r)
;-------------------------------------------------------------------------------
%if FILTER_ID > -1
SECT_TEXT(sufilter)
EXPORT MANGLE_FUNC(su_op_filter,0)
lodsb ; load the flags to al
%ifdef INCLUDE_STEREO_FILTER
call su_effects_stereohelper
%define INCLUDE_EFFECTS_STEREOHELPER
%endif
fld dword [INP+su_filter_ports.res] ; r x
fld dword [INP+su_filter_ports.freq]; f r x
fmul st0, st0 ; f2 x (square the input so we never get negative and also have a smoother behaviour in the lower frequencies)
fst dword [_SP-4] ; f2 r x
fmul dword [WRK+su_filter_wrk.band] ; f2*b r x
fadd dword [WRK+su_filter_wrk.low] ; f2*b+l r x
fst dword [WRK+su_filter_wrk.low] ; l'=f2*b+l r x
fsubp st2, st0 ; r x-l'
fmul dword [WRK+su_filter_wrk.band] ; r*b x-l'
fsubp st1, st0 ; x-l'-r*b
fst dword [WRK+su_filter_wrk.high] ; h'=x-l'-r*b
fmul dword [_SP-4] ; f2*h'
fadd dword [WRK+su_filter_wrk.band] ; f2*h'+b
fstp dword [WRK+su_filter_wrk.band] ; b'=f2*h'+b
fldz ; 0
%ifdef INCLUDE_LOWPASS
test al, byte LOWPASSFLAG
jz short su_op_filter_skiplowpass
fadd dword [WRK+su_filter_wrk.low]
su_op_filter_skiplowpass:
%endif
%ifdef INCLUDE_BANDPASS
test al, byte BANDPASSFLAG
jz short su_op_filter_skipbandpass
fadd dword [WRK+su_filter_wrk.band]
su_op_filter_skipbandpass:
%endif
%ifdef INCLUDE_HIGHPASS
test al, byte HIGHPASSFLAG
jz short su_op_filter_skiphighpass
fadd dword [WRK+su_filter_wrk.high]
su_op_filter_skiphighpass:
%endif
%ifdef INCLUDE_NEGBANDPASS
test al, byte NEGBANDPASSFLAG
jz short su_op_filter_skipnegbandpass
fsub dword [WRK+su_filter_wrk.band]
su_op_filter_skipnegbandpass:
%endif
%ifdef INCLUDE_NEGHIGHPASS
test al, byte NEGHIGHPASSFLAG
jz short su_op_filter_skipneghighpass
fsub dword [WRK+su_filter_wrk.high]
su_op_filter_skipneghighpass:
%endif
ret
%endif ; SU_INCLUDE_FILTER
;-------------------------------------------------------------------------------
; CLIP opcode: clips the signal into [-1,1] range
;-------------------------------------------------------------------------------
; Mono: x -> min(max(x,-1),1)
; Stereo: l r -> min(max(l,-1),1) min(max(r,-1),1)
;-------------------------------------------------------------------------------
SECT_TEXT(suclip)
%if CLIP_ID > -1
EXPORT MANGLE_FUNC(su_op_clip,0)
%ifdef INCLUDE_STEREO_CLIP
call su_effects_stereohelper
%define INCLUDE_EFFECTS_STEREOHELPER
%endif
%define SU_INCLUDE_CLIP
; flow into su_doclip
%endif ; CLIP_ID > -1
%ifdef SU_INCLUDE_CLIP
su_clip:
fld1 ; 1 x a
fucomi st1 ; if (1 <= x)
jbe short su_clip_do ; goto Clip_Do
fchs ; -1 x a
fucomi st1 ; if (-1 < x)
fcmovb st0, st1 ; x x a
su_clip_do:
fstp st1 ; x' a, where x' = clamp(x)
ret
%endif ; SU_INCLUDE_CLIP
;-------------------------------------------------------------------------------
; PAN opcode: pan the signal
;-------------------------------------------------------------------------------
; Mono: s -> s*(1-p) s*p
; Stereo: l r -> l*(1-p) r*p
;
; where p is the panning in [0,1] range
;-------------------------------------------------------------------------------
%if PAN_ID > -1
SECT_TEXT(supan)
%ifdef INCLUDE_STEREO_PAN
EXPORT MANGLE_FUNC(su_op_pan,0)
jc su_op_pan_do ; this time, if this is mono op...
fld st0 ; ...we duplicate the mono into stereo first
su_op_pan_do:
fld dword [INP+su_pan_ports.panning] ; p l r
fld1 ; 1 p l r
fsub st1 ; 1-p p l r
fmulp st2 ; p (1-p)*l r
fmulp st2 ; (1-p)*l p*r
ret
%else ; ifndef INCLUDE_STEREO_PAN
EXPORT MANGLE_FUNC(su_op_pan,0)
fld dword [INP+su_pan_ports.panning] ; p s
fmul st1 ; p*s s
fsub st1, st0 ; p*s s-p*s
; Equal to
; s*p s*(1-p)
fxch ; s*(1-p) s*p SHOULD PROBABLY DELETE, WHY BOTHER
ret
%endif ; INCLUDE_STEREO_PAN
%endif ; SU_USE_PAN
;-------------------------------------------------------------------------------
; su_effects_stereohelper: moves the workspace to next, does the filtering for
; right channel (pulling the calling address from stack), rewinds the
; workspace and returns
;-------------------------------------------------------------------------------
%ifdef INCLUDE_EFFECTS_STEREOHELPER
su_effects_stereohelper:
jnc su_effects_stereohelper_mono ; carry is still the stereo bit
add WRK, 16
fxch ; r l
call [_SP] ; call whoever called me...
fxch ; l r
sub WRK, 16 ; move WRK back to where it was
su_effects_stereohelper_mono:
ret ; return to process l/mono sound
%endif
;-------------------------------------------------------------------------------
; DELAY opcode: adds delay effect to the signal
;-------------------------------------------------------------------------------
; Mono: perform delay on ST0, using delaycount delaylines starting
; at delayindex from the delaytable
; Stereo: perform delay on ST1, using delaycount delaylines starting
; at delayindex + delaycount from the delaytable (so the right delays
; can be different)
;-------------------------------------------------------------------------------
%if DELAY_ID > -1
SECT_TEXT(sudelay)
EXPORT MANGLE_FUNC(su_op_delay,0)
lodsw ; al = delay index, ah = delay count
push_registers VAL, COM ; these are non-volatile according to our convention
movzx ebx, al
%ifdef RUNTIME_TABLES ; when using runtime tables, delaytimes is pulled from the stack so can be a pointer to heap
mov _SI, [_SP + su_stack.delaytimes + PUSH_REG_SIZE(2)]
lea _BX, [_SI + _BX*2]
%else
do{lea _BX,[},MANGLE_DATA(su_delay_times),_BX*2,] ; _BP now points to the right position within delay time table
%endif
movzx esi, word [_SP + su_stack.tick + PUSH_REG_SIZE(2)] ; notice that we load word, so we wrap at 65536
mov _CX, PTRWORD [_SP + su_stack.delaywrk + PUSH_REG_SIZE(2)] ; WRK is now the separate delay workspace, as they require a lot more space
%ifdef INCLUDE_STEREO_DELAY
jnc su_op_delay_mono
push _AX ; save _ah (delay count)
fxch ; r l
call su_op_delay_do ; D(r) l process delay for the right channel
pop _AX ; restore the count for second run
fxch ; l D(r)
su_op_delay_mono: ; flow into mono delay
%endif
call su_op_delay_do ; when stereo delay is not enabled, we could inline this to save 5 bytes, but I expect stereo delay to be farely popular so maybe not worth the hassle
mov PTRWORD [_SP + su_stack.delaywrk + PUSH_REG_SIZE(2)],_CX ; move delay workspace pointer back to stack.
pop_registers VAL, COM
%ifdef INCLUDE_DELAY_MODULATION
xor eax, eax
mov dword [WRK+su_unit.ports+su_delay_ports.delaymod], eax ; zero it
%endif
ret
%ifdef INCLUDE_DELAY_MODULATION
%define INCLUDE_DELAY_FLOAT_TIME
%endif
;-------------------------------------------------------------------------------
; su_op_delay_do: executes the actual delay
;-------------------------------------------------------------------------------
; Pseudocode:
; q = dr*x
; for (i = 0;i < count;i++)
; s = b[(t-delaytime[i+offset])&65535]
; q += s
; o[i] = o[i]*da+s*(1-da)
; b[t] = f*o[i] +p^2*x
; Perform dc-filtering q and output q
;-------------------------------------------------------------------------------
su_op_delay_do: ; x y
fld st0
fmul dword [INP+su_delay_ports.pregain] ; p*x y
fmul dword [INP+su_delay_ports.pregain] ; p*p*x y
fxch ; y p*p*x
fmul dword [INP+su_delay_ports.dry] ; dr*y p*p*x
su_op_delay_loop:
%ifdef INCLUDE_DELAY_FLOAT_TIME ; delaytime modulation or note syncing require computing the delay time in floats
fild word [_BX] ; k dr*y p*p*x, where k = delay time
%ifdef INCLUDE_DELAY_NOTETRACKING
test ah, 1 ; note syncing is the least significant bit of ah, 0 = ON, 1 = OFF
jne su_op_delay_skipnotesync
fild dword [INP-su_voice.inputs+su_voice.note]
do fmul dword [,c_i12,]
call MANGLE_FUNC(su_power,0)
fdivp st1, st0 ; use 10787 for delaytime to have neutral transpose
su_op_delay_skipnotesync:
%endif
%ifdef INCLUDE_DELAY_MODULATION
fld dword [WRK+su_unit.ports+su_delay_ports.delaymod]
do fmul dword [,c_32767,] ; scale it up, as the modulations would be too small otherwise
faddp st1, st0
%define USE_C_32767
%endif
fistp dword [_SP-4] ; dr*y p*p*x, dword [_SP-4] = integer amount of delay (samples)
mov edi, esi ; edi = esi = current time
sub di, word [_SP-4] ; we perform the math in 16-bit to wrap around
%else
mov edi, esi
sub di, word [_BX] ; we perform the math in 16-bit to wrap around
%endif
fld dword [_CX+su_delayline_wrk.buffer+_DI*4]; s dr*y p*p*x, where s is the sample from delay buffer
fadd st1, st0 ; s dr*y+s p*p*x (add comb output to current output)
fld1 ; 1 s dr*y+s p*p*x
fsub dword [INP+su_delay_ports.damp] ; 1-da s dr*y+s p*p*x
fmulp st1, st0 ; s*(1-da) dr*y+s p*p*x
fld dword [INP+su_delay_ports.damp] ; da s*(1-da) dr*y+s p*p*x
fmul dword [_CX+su_delayline_wrk.filtstate] ; o*da s*(1-da) dr*y+s p*p*x, where o is stored
faddp st1, st0 ; o*da+s*(1-da) dr*y+s p*p*x
fst dword [_CX+su_delayline_wrk.filtstate] ; o'=o*da+s*(1-da), o' dr*y+s p*p*x
fmul dword [INP+su_delay_ports.feedback] ; f*o' dr*y+s p*p*x
fadd st0, st2 ; f*o'+p*p*x dr*y+s p*p*x
fstp dword [_CX+su_delayline_wrk.buffer+_SI*4]; save f*o'+p*p*x to delay buffer
add _BX,2 ; move to next index
add _CX, su_delayline_wrk.size ; go to next delay delay workspace
sub ah, 2
jg su_op_delay_loop ; if ah > 0, goto loop
fstp st1 ; dr*y+s1+s2+s3+...
; DC-filtering
fld dword [_CX+su_delayline_wrk.dcout] ; o s
do fmul dword [,c_dc_const,] ; c*o s
fsub dword [_CX+su_delayline_wrk.dcin] ; c*o-i s
fxch ; s c*o-i
fst dword [_CX+su_delayline_wrk.dcin] ; i'=s, s c*o-i
faddp st1 ; s+c*o-i
do fadd dword [,c_0_5,] ; add and sub small offset to prevent denormalization
do fsub dword [,c_0_5,]
fst dword [_CX+su_delayline_wrk.dcout] ; o'=s+c*o-i
ret
%define USE_C_DC_CONST
%endif ; DELAY_ID > -1
;-------------------------------------------------------------------------------
; COMPRES opcode: push compressor gain to stack
;-------------------------------------------------------------------------------
; Mono: push g on stack, where g is a suitable gain for the signal
; you can either MULP to compress the signal or SEND it to a GAIN
; somewhere else for compressor side-chaining.
; Stereo: push g g on stack, where g is calculated using l^2 + r^2
;-------------------------------------------------------------------------------
%if COMPRES_ID > -1
SECT_TEXT(sucompr)
EXPORT MANGLE_FUNC(su_op_compressor,0)
fdiv dword [INP+su_compres_ports.invgain]; l/g, we'll call this pre inverse gained signal x from now on
fld st0 ; x x
fmul st0, st0 ; x^2 x
%ifdef INCLUDE_STEREO_COMPRES
jnc su_op_compressor_mono
fld st2 ; r x^2 l/g r
fdiv dword [INP+su_compres_ports.invgain]; r/g, we'll call this pre inverse gained signal y from now on
fst st3 ; y x^2 l/g r/g
fmul st0, st0 ; y^2 x^2 l/g r/g
faddp st1, st0 ; y^2+x^2 l/g r/g
call su_op_compressor_mono ; So, for stereo, we square both left & right and add them up
fld st0 ; and return the computed gain two times, ready for MULP STEREO
ret
su_op_compressor_mono:
%endif
fld dword [WRK+su_compres_wrk.level] ; l x^2 x
fucomi st0, st1
setnb al ; if (st0 >= st1) al = 1; else al = 0;
fsubp st1, st0 ; x^2-l x
call su_nonlinear_map ; c x^2-l x, c is either attack or release parameter mapped in a nonlinear way
fmulp st1, st0 ; c*(x^2-l) x
fadd dword [WRK+su_compres_wrk.level] ; l+c*(x^2-l) x // we could've kept level in the stack and save a few bytes, but su_env_map uses 3 stack (c + 2 temp), so the stack was getting quite big.
fst dword [WRK+su_compres_wrk.level] ; l'=l+c*(x^2-l), l' x
fld dword [INP+su_compres_ports.threshold] ; t l' x
fmul st0, st0 ; t*t l' x
fxch ; l' t*t x
fucomi st0, st1 ; if l' < t*t
fcmovb st0, st1 ; l'=t*t
fdivp st1, st0 ; t*t/l' x
fld dword [INP+su_compres_ports.ratio] ; r t*t/l' x
do fmul dword [,c_0_5,] ; p=r/2 t*t/l' x
fxch ; t*t/l' p x
fyl2x ; p*log2(t*t/l') x
jmp MANGLE_FUNC(su_power,0) ; 2^(p*log2(t*t/l')) x
; tail call ; Equal to:
; (t*t/l')^p x
; if ratio is at minimum => p=0 => 1 x
; if ratio is at maximum => p=0.5 => t/x => t/x*x=t
%endif ; COMPRES_ID > -1

View File

@ -1,388 +0,0 @@
;-------------------------------------------------------------------------------
; Filter (LOWPASS, BANDPASS...) effect related defines
;-------------------------------------------------------------------------------
%assign FILTER_ID -1
%macro USE_FILTER 0
%if FILTER_ID == -1
%assign FILTER_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_filter,0),
%xdefine NUMPARAMS NUMPARAMS 2,
%endif
%endmacro
%define LOWPASSFLAG 0x40
%define BANDPASSFLAG 0x20
%define HIGHPASSFLAG 0x10
%define NEGBANDPASSFLAG 0x08
%define NEGHIGHPASSFLAG 0x04
%macro SU_FILTER 8
db %2
db %3
db (%4*LOWPASSFLAG)+(%5*BANDPASSFLAG)+(%6*HIGHPASSFLAG)+(%7*NEGBANDPASSFLAG)+(%8*NEGHIGHPASSFLAG)
USE_FILTER
%xdefine CMDS CMDS FILTER_ID + %1,
%if %1 == 1
%define INCLUDE_STEREO_FILTER
%endif
%if %4 > 0
%define INCLUDE_LOWPASS
%endif
%if %5 > 0
%define INCLUDE_BANDPASS
%endif
%if %6 > 0
%define INCLUDE_HIGHPASS
%endif
%if %7 > 0
%define INCLUDE_NEGBANDPASS
%endif
%if %8 > 0
%define INCLUDE_NEGHIGHPASS
%endif
%endmacro
%define FREQUENCY(val) val
%define RESONANCE(val) val
%define FLAGS(val) val
%define LOWPASS(val) val
%define BANDPASS(val) val
%define HIGHPASS(val) val
%define NEGBANDPASS(val) val
%define NEGHIGHPASS(val) val
struc su_filter_ports
.freq resd 1
.res resd 1
endstruc
struc su_filter_wrk
.low resd 1
.high resd 1
.band resd 1
endstruc
;-------------------------------------------------------------------------------
; PAN effect related defines
;-------------------------------------------------------------------------------
%assign PAN_ID -1
%macro USE_PAN 0
%if PAN_ID == -1
%assign PAN_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_pan,0),
%xdefine NUMPARAMS NUMPARAMS 1,
%endif
%endmacro
%macro SU_PAN 2
db %2
USE_PAN
%xdefine CMDS CMDS PAN_ID + %1,
%if %1 == 1
%define INCLUDE_STEREO_PAN
%endif
%endmacro
%define PANNING(val) val
struc su_pan_ports
.panning resd 1
endstruc
;-------------------------------------------------------------------------------
; DISTORT effect related defines
;-------------------------------------------------------------------------------
%assign DISTORT_ID -1
%macro USE_DISTORT 0
%if DISTORT_ID == -1
%assign DISTORT_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_distort,0),
%xdefine NUMPARAMS NUMPARAMS 1,
%endif
%endmacro
%macro SU_DISTORT 2
db %2
USE_DISTORT
%xdefine CMDS CMDS DISTORT_ID + %1,
%if %1 == 1
%define INCLUDE_STEREO_DISTORT
%endif
%endmacro
%define DRIVE(val) val
struc su_distort_ports
.drive resd 1
endstruc
;-------------------------------------------------------------------------------
; HOLD effect related defines
;-------------------------------------------------------------------------------
%assign HOLD_ID -1
%macro USE_HOLD 0
%if HOLD_ID == -1
%assign HOLD_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_hold,0),
%xdefine NUMPARAMS NUMPARAMS 1,
%endif
%endmacro
%macro SU_HOLD 2
db %2
USE_HOLD
%xdefine CMDS CMDS HOLD_ID + %1,
%if %1 == 1
%define INCLUDE_STEREO_HOLD
%endif
%endmacro
%define HOLDFREQ(val) val
struc su_hold_ports
.freq resd 1
endstruc
struc su_hold_wrk
.phase resd 1
.holdval resd 1
endstruc
;-------------------------------------------------------------------------------
; CRUSH effect related defines
;-------------------------------------------------------------------------------
%assign CRUSH_ID -1
%macro USE_CRUSH 0
%if CRUSH_ID == -1
%assign CRUSH_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_crush,0),
%xdefine NUMPARAMS NUMPARAMS 1,
%endif
%endmacro
%macro SU_CRUSH 2
db %2
USE_CRUSH
%xdefine CMDS CMDS CRUSH_ID + %1,
%if %1 == 1
%define INCLUDE_STEREO_CRUSH
%endif
%endmacro
%define RESOLUTION(val) val
struc su_crush_ports
.resolution resd 1
endstruc
;-------------------------------------------------------------------------------
; GAIN effect related defines
;-------------------------------------------------------------------------------
%assign GAIN_ID -1
%macro USE_GAIN 0
%if GAIN_ID == -1
%assign GAIN_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_gain,0),
%xdefine NUMPARAMS NUMPARAMS 1,
%endif
%endmacro
%macro SU_GAIN 2
db %2
USE_GAIN
%xdefine CMDS CMDS GAIN_ID + %1,
%if %1 == 1
%define INCLUDE_STEREO_GAIN
%endif
%endmacro
%define GAIN(val) val
struc su_gain_ports
.gain resd 1
endstruc
;-------------------------------------------------------------------------------
; INVGAIN effect related defines
;-------------------------------------------------------------------------------
%assign INVGAIN_ID -1
%macro USE_INVGAIN 0
%if INVGAIN_ID == -1
%assign INVGAIN_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_invgain,0),
%xdefine NUMPARAMS NUMPARAMS 1,
%endif
%endmacro
%macro SU_INVGAIN 2
db %2
USE_INVGAIN
%xdefine CMDS CMDS INVGAIN_ID + %1,
%if %1 == 1
%define INCLUDE_STEREO_INVGAIN
%endif
%endmacro
%define INVGAIN(val) val
struc su_invgain_ports
.invgain resd 1
endstruc
;-------------------------------------------------------------------------------
; CLIP effect related defines
;-------------------------------------------------------------------------------
%assign CLIP_ID -1
%macro USE_CLIP 0
%if CLIP_ID == -1
%assign CLIP_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_clip,0),
%xdefine NUMPARAMS NUMPARAMS 0,
%endif
%endmacro
%macro SU_CLIP 1
USE_CLIP
%xdefine CMDS CMDS CLIP_ID + %1,
%if %1 == 1
%define INCLUDE_STEREO_CLIP
%endif
%endmacro
;-------------------------------------------------------------------------------
; Delay effect related defines
;-------------------------------------------------------------------------------
%assign DELAY_ID -1
%macro USE_DELAY 0
%if DELAY_ID == -1
%assign DELAY_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_delay,0),
%xdefine NUMPARAMS NUMPARAMS 4,
%endif
%endmacro
%define MAX_DELAY 65536 ; warning: this is pretty much fixed, as we use 16-bit math to wraparound the delay buffers
%assign NUM_DELAY_LINES 0
%macro SU_DELAY 8
db %2
db %3
db %4
db %5
db %6
db (2*%7-1)+%8
USE_DELAY
%xdefine CMDS CMDS DELAY_ID + %1,
%assign NUM_DELAY_LINES NUM_DELAY_LINES + %7 * (1+%1)
%if %1 == 1
%define INCLUDE_STEREO_DELAY
%endif
%if %8 > 0
%define INCLUDE_DELAY_NOTETRACKING
%define INCLUDE_DELAY_FLOAT_TIME
%endif
%endmacro
%macro BEGIN_DELTIMES 0
SECT_DATA(sudeltim)
EXPORT MANGLE_DATA(su_delay_times)
%endmacro
%define END_DELTIMES
%macro DELTIME 1-*
%rep %0
dw %1
%rotate 1
%endrep
%endmacro
%define PREGAIN(val) val
%define DRY(val) val
%define FEEDBACK(val) val
%define DEPTH(val) val
%define DAMP(val) val
%define DELAY(val) val
%define COUNT(val) val
%define NOTETRACKING(val) val
struc su_delay_ports
.pregain resd 1
.dry resd 1
.feedback resd 1
.damp resd 1
.freq resd 1
.delaymod resd 1 ; note that this is not converted from integer, only modulated
endstruc
struc su_delayline_wrk
.dcin resd 1
.dcout resd 1
.filtstate resd 1
.buffer resd MAX_DELAY
.size:
endstruc
;-------------------------------------------------------------------------------
; COMPRES effect related defines
;-------------------------------------------------------------------------------
%assign COMPRES_ID -1
%macro USE_COMPRES 0
%if COMPRES_ID == -1
%assign COMPRES_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_compressor,0),
%xdefine NUMPARAMS NUMPARAMS 5,
%endif
%endmacro
%macro SU_COMPRESSOR 6
db %2
db %3
db %4
db %5
db %6
USE_COMPRES
%xdefine CMDS CMDS COMPRES_ID + %1,
%if %1 == 1
%define INCLUDE_STEREO_COMPRES
%endif
%endmacro
%define ATTAC(val) val
%define RELEASE(val) val
%define INVGAIN(val) val
%define THRESHOLD(val) val
%define RATIO(val) val
struc su_compres_ports
.attack resd 1
.release resd 1
.invgain resd 1
.threshold resd 1
.ratio resd 1
endstruc
struc su_compres_wrk
.level resd 1
endstruc

View File

@ -1,65 +0,0 @@
;-------------------------------------------------------------------------------
; ADVANCE opcode: advances from one voice to next
;-------------------------------------------------------------------------------
; Checks if this was the last voice of current instrument. If so, moves to
; next opcodes and updates the stack to reflect the instrument change.
; If this instrument has more voices to process, rolls back the COM and VAL
; pointers to where they were when this instrument started.
;
; There is no stereo version.
;-------------------------------------------------------------------------------
SECT_TEXT(suopadvn)
EXPORT MANGLE_FUNC(su_op_advance,0)
%ifdef INCLUDE_POLYPHONY
mov WRK, [_SP+su_stack.wrk] ; WRK points to start of current voice
add WRK, su_voice.size ; move to next voice
mov [_SP+su_stack.wrk], WRK ; update the pointer in the stack to point to the new voice
mov ecx, [_SP+su_stack.voiceno] ; ecx = how many voices remain to process
dec ecx ; decrement number of voices to process
bt dword [_SP+su_stack.polyphony], ecx ; if voice bit of su_polyphonism not set
jnc su_op_advance_next_instrument ; goto next_instrument
mov VAL, PTRWORD [_SP+su_stack.val] ; if it was set, then repeat the opcodes for the current voice
mov COM, PTRWORD [_SP+su_stack.com]
su_op_advance_next_instrument:
mov PTRWORD [_SP+su_stack.val], VAL ; save current VAL as a checkpoint
mov PTRWORD [_SP+su_stack.com], COM ; save current COM as a checkpoint
su_op_advance_finish:
mov [_SP+su_stack.voiceno], ecx
ret
%else
mov WRK, PTRWORD [_SP+su_stack.wrk] ; WRK = wrkptr
add WRK, su_voice.size ; move to next voice
mov PTRWORD [_SP+su_stack.wrk], WRK ; update stack
dec PTRWORD [_SP+su_stack.voiceno] ; voices--
ret
%endif
;-------------------------------------------------------------------------------
; SPEED opcode: modulate the speed (bpm) of the song based on ST0
;-------------------------------------------------------------------------------
; Mono: adds or subtracts the ticks, a value of 0.5 is neutral & will7
; result in no speed change.
; There is no STEREO version.
;-------------------------------------------------------------------------------
%if SPEED_ID > -1
SECT_TEXT(suspeed)
EXPORT MANGLE_FUNC(su_op_speed,0)
do fmul dword [,c_bpmscale,] ; (2*s-1)*64/24, let's call this p from now on
call MANGLE_FUNC(su_power,0) ; 2^p, this is how many ticks we should be taking
fld1 ; 1 2^p
fsubp st1, st0 ; 2^p-1, the player is advancing 1 tick by its own
fadd dword [WRK+su_speed_wrk.remainder] ; t+2^p-1, t is the remainder from previous rounds as ticks have to be rounded to 1
push _AX
fist dword [_SP] ; Main stack: k=int(t+2^p-1)
fisub dword [_SP] ; t+2^p-1-k, the remainder
pop _AX
add dword [_SP+su_stack.rowtick], eax ; add the whole ticks to row tick count
fstp dword [WRK+su_speed_wrk.remainder] ; save the remainder for future
ret
%define USE_C_BPMSCALE
%endif

View File

@ -1,21 +0,0 @@
;-------------------------------------------------------------------------------
; SPEED related defines
;-------------------------------------------------------------------------------
%assign SPEED_ID -1
%macro USE_SPEED 0
%if SPEED_ID == -1
%assign SPEED_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_speed,0),
%xdefine NUMPARAMS NUMPARAMS 0,
%endif
%endmacro
%macro SU_SPEED 0
USE_SPEED
%xdefine CMDS CMDS SPEED_ID,
%endmacro
struc su_speed_wrk
.remainder resd 1
endstruc

View File

@ -1,559 +0,0 @@
%if BITS == 64
%define WRK rbp ; alias for unit workspace
%define VAL rsi ; alias for unit values (transformed/untransformed)
%define COM rbx ; alias for instrument opcodes
%define INP rdx ; alias for transformed inputs
%define _AX rax ; push and offsets have to be r* on 64-bit and e* on 32-bit
%define _BX rbx
%define _CX rcx
%define _DX rdx
%define _SP rsp
%define _SI rsi
%define _DI rdi
%define _BP rbp
%define PTRSIZE 8
%define PTRWORD qword
%define RESPTR resq
%define DPTR dq
%macro do 2
mov r9, qword %2
%1 r9
%endmacro
%macro do 3
mov r9, qword %2
%1 r9 %3
%endmacro
%macro do 4
mov r9, qword %2
%1 r9+%3 %4
%endmacro
%macro do 5
mov r9, qword %2
lea r9, [r9+%3]
%1 r9+%4 %5
%endmacro
%macro push_registers 1-*
%rep %0
push %1
%rotate 1
%endrep
%endmacro
%macro pop_registers 1-*
%rep %0
%rotate -1
pop %1
%endrep
%endmacro
%define PUSH_REG_SIZE(n) (n*8)
%ifidn __OUTPUT_FORMAT__,win64
%define render_prologue push_registers rcx,rdi,rsi,rbx,rbp ; rcx = ptr to buf. rdi,rsi,rbx,rbp nonvolatile
%macro render_epilogue 0
pop_registers rcx,rdi,rsi,rbx,rbp
ret
%endmacro
%else ; 64 bit mac & linux
%define render_prologue push_registers rdi,rbx,rbp ; rdi = ptr to buf. rbx & rbp nonvolatile
%macro render_epilogue 0
pop_registers rdi,rbx,rbp
ret
%endmacro
%endif
%else
%define WRK ebp ; alias for unit workspace
%define VAL esi ; alias for unit values (transformed/untransformed)
%define COM ebx ; alias for instrument opcodes
%define INP edx ; alias for transformed inputs
%define _AX eax
%define _BX ebx
%define _CX ecx
%define _DX edx
%define _SP esp
%define _SI esi
%define _DI edi
%define _BP ebp
%define PTRSIZE 4
%define PTRWORD dword
%define RESPTR resd
%define DPTR dd
%macro do 2
%1 %2
%endmacro
%macro do 3
%1 %2 %3
%endmacro
%macro do 4
%1 %2+%3 %4
%endmacro
%macro do 5
%1 %2+%3+%4 %5
%endmacro
%macro push_registers 1-*
pushad ; in 32-bit mode, this is the easiest way to store all the registers
%endmacro
%macro pop_registers 1-*
popad
%endmacro
%define PUSH_REG_SIZE(n) 32
%define render_prologue pushad ; stdcall & everything nonvolatile except eax, ecx, edx
%macro render_epilogue 0
popad
ret 4 ; clean the passed parameter from stack.
%endmacro
%endif
section .text ; yasm throws section redeclaration warnings if strucs are defined without a plain .text section
struc su_stack ; the structure of stack _as the units see it_
.retaddr RESPTR 1
%if BITS == 32 ; we dump everything with pushad, so this is unused in 32-bit
RESPTR 1
%endif
.val RESPTR 1
.wrk RESPTR 1
%if BITS == 32 ; we dump everything with pushad, so this is unused in 32-bit
RESPTR 1
%endif
.com RESPTR 1
.synth RESPTR 1
.delaywrk RESPTR 1
%if BITS == 32 ; we dump everything with pushad, so this is unused in 32-bit
RESPTR 1
%endif
.retaddrvm RESPTR 1
.voiceno RESPTR 1
%ifdef INCLUDE_POLYPHONY
.polyphony RESPTR 1
%endif
.output_sound:
.rowtick RESPTR 1 ; which tick within this row are we at
.update_voices:
.row RESPTR 1 ; which total row of the song are we at
.tick RESPTR 1 ; which total tick of the song are we at
.randseed RESPTR 1
%ifdef RUNTIME_TABLES ; RUNTIME_TABLES move these table ptrs to stack
%if DELAY_ID > -1 ; allowing them to be in the heap &
.delaytimes RESPTR 1 ; modifying them safely & having many synths
%endif
%ifdef INCLUDE_SAMPLES
.sampleoffs RESPTR 1
%endif
%endif
%ifdef INCLUDE_MULTIVOICE_TRACKS
.voicetrack RESPTR 1
%endif
.render_epilogue:
%if BITS == 32
RESPTR 8 ; registers
.retaddr_pl RESPTR 1
%elifidn __OUTPUT_FORMAT__,win64
RESPTR 4 ; registers
%else
RESPTR 2 ; registers
%endif
.bufferptr RESPTR 1
.size:
endstruc
;===============================================================================
; Uninitialized data: The one and only synth object
;===============================================================================
SECT_BSS(susynth)
su_synth_obj resb su_synthworkspace.size
%if DELAY_ID > -1 ; if we use delay, then the synth obj should be immediately followed by the delay workspaces
resb NUM_DELAY_LINES*su_delayline_wrk.size
%endif
;===============================================================================
; The opcode table jump table. This is constructed to only include the opcodes
; that are used so that the jump table is as small as possible.
;===============================================================================
SECT_DATA(suoptabl)
su_synth_commands DPTR OPCODES
;===============================================================================
; The number of transformed parameters each opcode takes
;===============================================================================
SECT_DATA(suparcnt)
su_opcode_numparams db NUMPARAMS
;-------------------------------------------------------------------------------
; su_run_vm function: runs the entire virtual machine once, creating 1 sample
;-------------------------------------------------------------------------------
; Input: su_synth_obj.left : Set to 0 before calling
; su_synth_obj.right : Set to 0 before calling
; _CX : Pointer to delay workspace (if needed)
; _DX : Pointer to synth object
; COM : Pointer to command stream
; VAL : Pointer to value stream
; WRK : Pointer to the last workspace processed
; _DI : Number of voices to process
; Output: su_synth_obj.left : left sample
; su_synth_obj.right : right sample
; Dirty: everything
;-------------------------------------------------------------------------------
SECT_TEXT(surunvm)
EXPORT MANGLE_FUNC(su_run_vm,0)
push_registers _CX, _DX, COM, WRK, VAL ; save everything to stack
su_run_vm_loop: ; loop until all voices done
movzx edi, byte [COM] ; edi = command byte
inc COM ; move to next instruction
add WRK, su_unit.size ; move WRK to next unit
shr edi, 1 ; shift out the LSB bit = stereo bit
mov INP, [_SP+su_stack.wrk-PTRSIZE] ; reset INP to point to the inputs part of voice
add INP, su_voice.inputs
xor ecx, ecx ; counter = 0
xor eax, eax ; clear out high bits of eax, as lodsb only sets al
su_transform_values_loop:
do{cmp cl, byte [},su_opcode_numparams,_DI,] ; compare the counter to the value in the param count table
je su_transform_values_out
lodsb ; load the byte value from VAL stream
push _AX ; push it to memory so FPU can read it
fild dword [_SP] ; load the value to FPU stack
do fmul dword [,c_i128,] ; divide it by 128 (0 => 0, 128 => 1.0)
fadd dword [WRK+su_unit.ports+_CX*4] ; add the modulations in the current workspace
fstp dword [INP+_CX*4] ; store the modulated value in the inputs section of voice
xor eax, eax
mov dword [WRK+su_unit.ports+_CX*4], eax ; clear out the modulation ports
pop _AX
inc ecx
jmp su_transform_values_loop
su_transform_values_out:
bt dword [COM-1],0 ; LSB of COM = stereo bit => carry
do call [,su_synth_commands,_DI*PTRSIZE,] ; call the function corresponding to the instruction
cmp dword [_SP+su_stack.voiceno-PTRSIZE],0 ; do we have more voices to process?
jne su_run_vm_loop ; if there's more voices to process, goto vm_loop
pop_registers _CX, _DX, COM, WRK, VAL ; pop everything from stack
ret
;-------------------------------------------------------------------------------
; su_nonlinear_map function: returns 2^(-24*x) of parameter number _AX
;-------------------------------------------------------------------------------
; Input: _AX : parameter number (e.g. for envelope: 0 = attac, 1 = decay...)
; INP : pointer to transformed values
; Output: st0 : 2^(-24*x), where x is the parameter in the range 0-1
;-------------------------------------------------------------------------------
SECT_TEXT(supower)
%if ENVELOPE_ID > -1 || COMPRES_ID > -1
su_nonlinear_map:
fld dword [INP+_AX*4] ; x, where x is the parameter in the range 0-1
do fimul dword [,c_24,] ; 24*x
fchs ; -24*x
; flow into Power function, which outputs 2^(-24*x)
%endif
;-------------------------------------------------------------------------------
; su_power function: computes 2^x
;-------------------------------------------------------------------------------
; Input: st0 : x
; Output: st0 : 2^x
;-------------------------------------------------------------------------------
EXPORT MANGLE_FUNC(su_power,0)
fld1 ; 1 x
fld st1 ; x 1 x
fprem ; mod(x,1) 1 x
f2xm1 ; 2^mod(x,1)-1 1 x
faddp st1,st0 ; 2^mod(x,1) x
fscale ; 2^mod(x,1)*2^trunc(x) x
; Equal to:
; 2^x x
fstp st1 ; 2^x
ret
%ifndef SU_DISABLE_PLAYER
;-------------------------------------------------------------------------------
; output_sound macro: used by the render function to write sound to buffer
;-------------------------------------------------------------------------------
; The macro contains the ifdef hell to handle 16bit output and clipping cases
; to keep the main function more readable
; Stack : sample row pushad output_ptr
;-------------------------------------------------------------------------------
%macro output_sound 0
%ifndef SU_OUTPUT_16BIT
%ifndef SU_CLIP_OUTPUT ; The modern way. No need to clip; OS can do it.
mov _DI, [_SP+su_stack.bufferptr - su_stack.output_sound] ; edi containts ptr
mov _SI, PTRWORD su_synth_obj + su_synthworkspace.left
movsd ; copy left channel to output buffer
movsd ; copy right channel to output buffer
mov [_SP+su_stack.bufferptr - su_stack.output_sound], _DI ; save back the updated ptr
lea _DI, [_SI-8]
xor eax, eax
stosd ; clear left channel so the VM is ready to write them again
stosd ; clear right channel so the VM is ready to write them again
%else
mov _SI, qword [_SP+su_stack.bufferptr - su_stack.output_sound] ; esi points to the output buffer
xor _CX,_CX
xor eax,eax
%%loop: ; loop over two channels, left & right
do fld dword [,su_synth_obj+su_synthworkspace.left,_CX*4,]
call su_clip
fstp dword [_SI]
do mov dword [,su_synth_obj+su_synthworkspace.left,_CX*4,{],eax} ; clear the sample so the VM is ready to write it
add _SI,4
cmp ecx,2
jl %%loop
mov dword [_SP+su_stack.bufferptr - su_stack.output_sound], _SI ; save esi back to stack
%define SU_INCLUDE_CLIP
%endif
%else ; 16-bit output, always clipped. This is a bit legacy method.
mov _SI, [_SP+su_stack.bufferptr - su_stack.output_sound] ; esi points to the output buffer
mov _DI, PTRWORD su_synth_obj+su_synthworkspace.left
mov ecx, 2
%%loop: ; loop over two channels, left & right
fld dword [_DI]
call su_clip
do fmul dword [,c_32767,]
push _AX
fistp dword [_SP]
pop _AX
mov word [_SI],ax ; // store integer converted right sample
xor eax,eax
stosd
add _SI,2
loop %%loop
mov [_SP+su_stack.bufferptr - su_stack.output_sound], _SI ; save esi back to stack
%define USE_C_32767
%define SU_INCLUDE_CLIP
%endif
%endmacro
;-------------------------------------------------------------------------------
; su_render_song function: the entry point for the synth
;-------------------------------------------------------------------------------
; Has the signature su_render_song(void *ptr), where ptr is a pointer to
; the output buffer. Renders the compile time hard-coded song to the buffer.
; Stack: output_ptr
;-------------------------------------------------------------------------------
SECT_TEXT(surensng)
EXPORT MANGLE_FUNC(su_render_song,PTRSIZE) ; Stack: ptr
render_prologue
xor eax, eax
%ifdef INCLUDE_MULTIVOICE_TRACKS
push VOICETRACK_BITMASK
%endif
%ifdef RUNTIME_TABLES ; runtime tables is actually only useful in the api use case, but it's nice it works also in the render case
%ifdef INCLUDE_SAMPLES
do push ,MANGLE_DATA(su_sample_offsets) ; &su_sample_offsets is in the stack, instead of hard coded
%endif
%if DELAY_ID > -1
do push ,MANGLE_DATA(su_delay_times) ; &su_delay_times is in the stack, instead of hard coded
%endif
%endif
push 1 ; randseed
push _AX ; global tick time
su_render_rowloop: ; loop through every row in the song
push _AX ; Stack: row pushad ptr
call su_update_voices ; update instruments for the new row
xor eax, eax ; ecx is the current sample within row
su_render_sampleloop: ; loop through every sample in the row
push _AX ; Stack: sample row pushad ptr
%ifdef INCLUDE_POLYPHONY
push POLYPHONY_BITMASK ; does the next voice reuse the current opcodes?
%endif
push MAX_VOICES
mov _DX, PTRWORD su_synth_obj ; _DX points to the synth object
mov COM, PTRWORD MANGLE_DATA(su_commands) ; COM points to vm code
mov VAL, PTRWORD MANGLE_DATA(su_params) ; VAL points to unit params
%if DELAY_ID > -1
lea _CX, [_DX + su_synthworkspace.size - su_delayline_wrk.filtstate]
%endif
lea WRK, [_DX + su_synthworkspace.voices] ; WRK points to the first voice
call MANGLE_FUNC(su_run_vm,0) ; run through the VM code
pop _AX
%ifdef INCLUDE_POLYPHONY
pop _AX
%endif
output_sound ; *ptr++ = left, *ptr++ = right
pop _AX
inc dword [_SP + PTRSIZE] ; increment global time, used by delays
inc eax
cmp eax, SAMPLES_PER_ROW
jl su_render_sampleloop
pop _AX ; Stack: pushad ptr
inc eax
cmp eax, TOTAL_ROWS
jl su_render_rowloop
; rewind the stack. Use add when there's a lot to clean, repeated pops if only a little
%if su_stack.render_epilogue - su_stack.tick > 3*PTRSIZE
add _SP, su_stack.render_epilogue - su_stack.tick ; rewind the remaining tack
%else
%rep (su_stack.render_epilogue - su_stack.tick)/PTRSIZE
pop _AX ; the entropy of 3 x pop _AX is probably lower than 3 byte add
%endrep ; but this needs to be confirmed
%endif
render_epilogue
;-------------------------------------------------------------------------------
; su_update_voices function: polyphonic & chord implementation
;-------------------------------------------------------------------------------
; Input: eax : current row within song
; Dirty: pretty much everything
;-------------------------------------------------------------------------------
SECT_TEXT(suupdvce)
%ifdef INCLUDE_MULTIVOICE_TRACKS
su_update_voices: ; Stack: retaddr row
xor edx, edx
mov ebx, PATTERN_SIZE ; we could do xor ebx,ebx; mov bl,PATTERN_SIZE, but that would limit patternsize to 256...
div ebx ; eax = current pattern, edx = current row in pattern
do{lea _SI, [},MANGLE_DATA(su_tracks),_AX,] ; esi points to the pattern data for current track
xor eax, eax ; eax is the first voice of next track
xor ebx, ebx ; ebx is the first voice of current track
mov _BP, PTRWORD su_synth_obj ; ebp points to the current_voiceno array
su_update_voices_trackloop:
movzx eax, byte [_SI] ; eax = current pattern
imul eax, PATTERN_SIZE ; eax = offset to current pattern data
do{movzx eax,byte [},MANGLE_DATA(su_patterns),_AX,_DX,] ; eax = note
push _DX ; Stack: ptrnrow
xor edx, edx ; edx=0
mov ecx, ebx ; ecx=first voice of the track to be done
su_calculate_voices_loop: ; do {
bt dword [_SP + su_stack.voicetrack - su_stack.update_voices + 2*PTRSIZE],ecx ; test voicetrack_bitmask// notice that the incs don't set carry
inc edx ; edx++ // edx=numvoices
inc ecx ; ecx++ // ecx=the first voice of next track
jc su_calculate_voices_loop ; } while bit ecx-1 of bitmask is on
push _CX ; Stack: next_instr ptrnrow
cmp al, SU_HOLDVALUE ; anything but hold causes action
je short su_update_voices_nexttrack
mov cl, byte [_BP]
mov edi, ecx
add edi, ebx
shl edi, MAX_UNITS_SHIFT + 6 ; each unit = 64 bytes and there are 1<<MAX_UNITS_SHIFT units + small header
do inc dword [,su_synth_obj+su_synthworkspace.voices+su_voice.release,_DI,] ; set the voice currently active to release; notice that it could increment any number of times
cmp al, SU_HOLDVALUE ; if cl < HLD (no new note triggered)
jl su_update_voices_nexttrack ; goto nexttrack
inc ecx ; curvoice++
cmp ecx, edx ; if (curvoice >= num_voices)
jl su_update_voices_skipreset
xor ecx,ecx ; curvoice = 0
su_update_voices_skipreset:
mov byte [_BP],cl
add ecx, ebx
shl ecx, MAX_UNITS_SHIFT + 6 ; each unit = 64 bytes and there are 1<<MAX_UNITS_SHIFT units + small header
do{lea _DI,[},su_synth_obj+su_synthworkspace.voices,_CX,]
stosd ; save note
mov ecx, (su_voice.size - su_voice.release)/4
xor eax, eax
rep stosd ; clear the workspace of the new voice, retriggering oscillators
su_update_voices_nexttrack:
pop _BX ; ebx=first voice of next instrument, Stack: ptrnrow
pop _DX ; edx=patrnrow
add _SI, MAX_PATTERNS
inc _BP
do{cmp _BP,},su_synth_obj+MAX_TRACKS
jl su_update_voices_trackloop
ret
%else ; INCLUDE_MULTIVOICE_TRACKS not defined -> one voice per track ve_SIon
su_update_voices: ; Stack: retaddr row
xor edx, edx
xor ebx, ebx
mov bl, PATTERN_SIZE
div ebx ; eax = current pattern, edx = current row in pattern
do{lea _SI, [},MANGLE_DATA(su_tracks),_AX,]; esi points to the pattern data for current track
mov _DI, PTRWORD su_synth_obj+su_synthworkspace.voices
mov bl, MAX_TRACKS ; MAX_TRACKS is always <= 32 so this is ok
su_update_voices_trackloop:
movzx eax, byte [_SI] ; eax = current pattern
imul eax, PATTERN_SIZE ; eax = offset to current pattern data
do{movzx eax, byte [}, MANGLE_DATA(su_patterns),_AX,_DX,] ; ecx = note
cmp al, SU_HOLDVALUE ; anything but hold causes action
je short su_update_voices_nexttrack
inc dword [_DI+su_voice.release] ; set the voice currently active to release; notice that it could increment any number of times
jb su_update_voices_nexttrack ; if cl < HLD (no new note triggered) goto nexttrack
su_update_voices_retrigger:
stosd ; save note
mov ecx, (su_voice.size - su_voice.release)/4 ; could be xor ecx, ecx; mov ch,...>>8, but will it actually be smaller after compression?
xor eax, eax
rep stosd ; clear the workspace of the new voice, retriggering oscillators
jmp short su_update_voices_skipadd
su_update_voices_nexttrack:
add _DI, su_voice.size
su_update_voices_skipadd:
add _SI, MAX_PATTERNS
dec ebx
jnz short su_update_voices_trackloop
ret
%endif ;INCLUDE_MULTIVOICE_TRACKS
%endif ; SU_DISABLE_PLAYER
;-------------------------------------------------------------------------------
; Include the rest of the code
;-------------------------------------------------------------------------------
%include "sointu/arithmetic_footer.inc"
%include "sointu/flowcontrol_footer.inc"
%include "sointu/sources_footer.inc"
%include "sointu/sinks_footer.inc"
; warning: at the moment effects has to be assembled after
; sources, as sources.asm defines SU_USE_WAVESHAPER
; if needed.
%include "sointu/effects_footer.inc"
%ifidn __OUTPUT_FORMAT__,win64
%include "sointu/win64/gmdls_win64_footer.inc"
%endif
%ifidn __OUTPUT_FORMAT__,win32
%include "sointu/win32/gmdls_win32_footer.inc"
%endif
;-------------------------------------------------------------------------------
; Constants
;-------------------------------------------------------------------------------
SECT_DATA(suconst)
c_24 dd 24
c_i128 dd 0.0078125
c_RandDiv dd 65536*32768
c_0_5 dd 0.5
c_i12 dd 0x3DAAAAAA
c_lfo_normalize dd 0.000038
c_freq_normalize dd 0.000092696138 ; // 220.0/(2^(69/12)) / 44100.0
%ifdef USE_C_DC_CONST
c_dc_const dd 0.99609375 ; R = 1 - (pi*2 * frequency /samplerate)
%endif
%ifdef USE_C_32767
c_32767 dd 32767.0
%endif
%ifdef USE_C_BPMSCALE
c_bpmscale dd 2.206896551724138 ; 64/29, 29 values will be double speed, so you can go from ~ 1/2.2 speed to 2.2x speed
%endif
%ifdef USE_C_16
c_16 dd 16.0
%endif
%ifdef USE_C_SAMPLEFREQ_SCALING
c_samplefreq_scaling dd 84.28074964676522 ; o = 0.000092696138, n = 72, f = 44100*o*2**(n/12), scaling = 22050/f <- so note 72 plays at the "normal rate"
%endif

View File

@ -1,279 +0,0 @@
%ifndef SOINTU_INC
%define SOINTU_INC
%macro EXPORT 1
global %1
%1:
%endmacro
%ifidn __OUTPUT_FORMAT__,win32
%define WINDOWS
%assign BITS 32
%define MANGLE_FUNC(f,n) _ %+ f %+ @ %+ n
%define MANGLE_DATA(d) _ %+ d
%endif
%ifidn __OUTPUT_FORMAT__,win64
%define WINDOWS
%assign BITS 64
%define MANGLE_FUNC(f,n) f
%define MANGLE_DATA(d) d
%endif
%ifidn __OUTPUT_FORMAT__,macho32
%define MACOS
%assign BITS 32
%endif
%ifidn __OUTPUT_FORMAT__,macho64
%define MACOS
%assign BITS 64
%endif
%ifidn __OUTPUT_FORMAT__,elf
%define LINUX
%assign BITS 32
%endif
%ifidn __OUTPUT_FORMAT__,elf64
%define LINUX
%assign BITS 64
%endif
%ifdef WINDOWS
; Windows has crinkler so one may put everything in custom sections to aid crinkler.
; Maybe mac users need it too
%ifndef DISABLE_SECTIONS
%define SECT_BSS(n) section . %+ n bss align=256 ; a high alignment on the uninitialized sections should compress better
%define SECT_DATA(n) section . %+ n data align=1
%define SECT_TEXT(n) section . %+ n code align=1
%else
%define SECT_BSS(n) section .bss align=256
%define SECT_DATA(n) section .data align=1
%define SECT_TEXT(n) section .code align=1
%endif
%elifdef MACOS
%define MANGLE_FUNC(f,n) _ %+ f
%define MANGLE_DATA(d) _ %+ d
; macho does not seem to support named sections, so DISABLE_SECTIONS
; is "always on" / ignored
%define SECT_BSS(n) section .bss align=256
%define SECT_DATA(n) section .data align=1
%define SECT_TEXT(n) section .text align=1
%else ; Linux, or hopefully something similar
%define MANGLE_FUNC(f,n) f
%define MANGLE_DATA(d) d
%ifndef DISABLE_SECTIONS
%define SECT_BSS(n) section .bss. %+ n nobits alloc noexec write align=256
%define SECT_DATA(n) section .data. %+ n progbits alloc noexec write align=1
%define SECT_TEXT(n) section .text. %+ n progbits alloc exec nowrite align=1
%else
%define SECT_BSS(n) section .bss. nobits alloc noexec write align=256
%define SECT_DATA(n) section .data. progbits alloc noexec write align=1
%define SECT_TEXT(n) section .text. progbits alloc exec nowrite align=1
%endif
%endif
%assign CUR_ID 2
%define CMDS ; CMDS is empty at first, no commands defined
%define OPCODES MANGLE_FUNC(su_op_advance,0),
%define NUMPARAMS 0,
%define SU_ADVANCE_ID 0
%define STEREO(val) val
section .text ; yasm throws section redeclaration warnings if strucs are defined without a plain .text section
%include "sointu/flowcontrol_header.inc"
%include "sointu/arithmetic_header.inc"
%include "sointu/effects_header.inc"
%include "sointu/sources_header.inc"
%include "sointu/sinks_header.inc"
;-------------------------------------------------------------------------------
; synth defines
;-------------------------------------------------------------------------------
%define MAX_DELAY 65536
%assign MAX_UNITS_SHIFT 6
%assign MAX_UNITS ((1 << MAX_UNITS_SHIFT)-1) ; this is carefully chosen to align su_unit to 2^n boundary
%define ABSOLUTE_MAX_VOICES 32
%ifndef SAMPLE_RATE
%define SAMPLE_RATE 44100
%endif
%define TOTAL_ROWS (MAX_PATTERNS*PATTERN_SIZE)
%define SAMPLES_PER_ROW (SAMPLE_RATE*4*60/(SU_BPM*16))
%macro BEGIN_SONG 5
%xdefine SU_BPM %1
%if %2 == 1
%define SU_OUTPUT_16BIT
%endif
%if %3 == 1
%define SU_CLIP_OUTPUT
%endif
%if %4 == 1
%define INCLUDE_DELAY_MODULATION
%endif
%xdefine SU_HOLDVALUE %5
%endmacro
%macro END_SONG 0
%include "sointu/footer.inc"
%endmacro
%define BPM(val) val
%define OUTPUT_16BIT(val) val
%define CLIP_OUTPUT(val) val
%define DELAY_MODULATION(val) val
%define HOLD(val) val
%macro BEGIN_PATCH 0
SECT_DATA(params)
EXPORT MANGLE_DATA(su_params)
%endmacro
%macro END_PATCH 0 ; After the patch is finished, saves the accumulated commands
SECT_DATA(sucomnds)
EXPORT MANGLE_DATA(su_commands)
db CMDS
%endmacro
%define POLYPHONY_BITMASK 0
%assign MAX_VOICES 0
%assign MAX_TRACKS 0
%macro BEGIN_INSTRUMENT 1
; increment MAX_VOICES equal to %1 and construct the POLYPHONY_BITMASK so that
; for every except the last, the bit is on
%rep %1-1
%assign POLYPHONY_BITMASK (POLYPHONY_BITMASK << 1) + 1
%assign MAX_VOICES MAX_VOICES + 1
%endrep
%assign POLYPHONY_BITMASK (POLYPHONY_BITMASK << 1)
%assign MAX_VOICES MAX_VOICES + 1 ; the last voice increment, without adding one bit to the mask
%if MAX_VOICES > 32
%error Error: cannot have more than 32 voices!
%endif
%if %1 > 1
%define INCLUDE_POLYPHONY
%endif
%endmacro
%define VOICES(val) val
%define TRACKS(val) val
%macro END_INSTRUMENT 0
%xdefine CMDS CMDS SU_ADVANCE_ID,
%endmacro
%assign PATTERN_LENGTH -1
%macro BEGIN_PATTERNS 0
SECT_DATA(supatrns)
EXPORT MANGLE_DATA(su_patterns)
%define USE_PLAYER
%endmacro
%define END_PATTERNS
%assign PATTERN_SIZE -1
%macro PATTERN 1-*
%rep %0
db %1
%rotate 1
%endrep
%if %0 >= 256
%error 'Pattern size should be < 256'
%endif
%if PATTERN_SIZE == -1
%assign PATTERN_SIZE %0
%else
%if %0 != PATTERN_SIZE
%error 'All patterns should have the same length!'
%endif
%endif
%endmacro
%macro BEGIN_TRACKS 0
SECT_DATA(sutracks)
EXPORT MANGLE_DATA(su_tracks)
%define USE_PLAYER
%endmacro
%assign MAX_PATTERNS -1
%assign MAX_TRACKS 0
%assign VOICETRACK_BITMASK 0
%assign VOICETRACK_COUNT 0
%macro TRACK 2-* ; first param number of voices, rest are the patterns
%rep %0-1
db %2
%rotate 1
%endrep
%rotate 1
%if MAX_PATTERNS == -1
%assign MAX_PATTERNS %0-1
%else
%if %0-1 != MAX_PATTERNS
%error 'All tracks should have same number of patterns!'
%endif
%endif
%assign MAX_TRACKS MAX_TRACKS + 1
%if MAX_TRACKS > 32
%error Error: cannot have more than 32 tracks!
%endif
; increment MAX_TRACKS equal to %2 and construct the CHORD_BITMASK so that
; for every track except the last track of an instrument, the bit is on
%rep %1-1
%assign VOICETRACK_BITMASK VOICETRACK_BITMASK + (1 << VOICETRACK_COUNT)
%assign VOICETRACK_COUNT VOICETRACK_COUNT + 1
%endrep
%assign VOICETRACK_COUNT VOICETRACK_COUNT + 1 ; the last voice increment, without adding bit mask
%if VOICETRACK_COUNT > 32
%error Error: cannot have more than a total of 32 voices assigned to tracks.
%endif
%if %1 > 1
%define INCLUDE_MULTIVOICE_TRACKS
%endif
%endmacro
%define END_TRACKS
;-------------------------------------------------------------------------------
; unit struct
;-------------------------------------------------------------------------------
struc su_unit
.state resd 8
.ports resd 8
.size:
endstruc
;-------------------------------------------------------------------------------
; voice struct
;-------------------------------------------------------------------------------
struc su_voice
.note resd 1
.release resd 1
.inputs resd 8
.reserved resd 6 ; this is done to so the whole voice is 2^n long, see polyphonic player
.workspace resb MAX_UNITS * su_unit.size
.size:
endstruc
;-------------------------------------------------------------------------------
; synthworkspace struct
;-------------------------------------------------------------------------------
struc su_synthworkspace
.curvoices resb 32 ; these are used by the multitrack player to store which voice is playing on which track
.left resd 1
.right resd 1
.aux resd 6 ; 3 auxiliary signals
.voices resb ABSOLUTE_MAX_VOICES * su_voice.size
.size:
endstruc
%endif ; SOINTU_INC

View File

@ -1,125 +0,0 @@
;-------------------------------------------------------------------------------
; OUT opcode: outputs and pops the signal
;-------------------------------------------------------------------------------
; Mono: add ST0 to main left port
; Stereo: also add ST1 to main right port
;-------------------------------------------------------------------------------
%if OUT_ID > -1
SECT_TEXT(suopout)
EXPORT MANGLE_FUNC(su_op_out,0) ; l r
mov _AX, [_SP + su_stack.synth]
%ifdef INCLUDE_STEREO_OUT
jnc su_op_out_mono
call su_op_out_mono
add _AX, 4
su_op_out_mono:
%endif
fmul dword [INP + su_out_ports.gain] ; g*l
fadd dword [_AX + su_synthworkspace.left] ; g*l+o
fstp dword [_AX + su_synthworkspace.left] ; o'=g*l+o
ret
%endif ; SU_OUT_ID > -1
;-------------------------------------------------------------------------------
; OUTAUX opcode: outputs to main and aux1 outputs and pops the signal
;-------------------------------------------------------------------------------
; Mono: add outgain*ST0 to main left port and auxgain*ST0 to aux1 left
; Stereo: also add outgain*ST1 to main right port and auxgain*ST1 to aux1 right
;-------------------------------------------------------------------------------
%if OUTAUX_ID > -1
SECT_TEXT(suoutaux)
EXPORT MANGLE_FUNC(su_op_outaux,0) ; l r
mov _AX, [_SP + su_stack.synth]
%ifdef INCLUDE_STEREO_OUTAUX
jnc su_op_outaux_mono
call su_op_outaux_mono
add _AX, 4
su_op_outaux_mono:
%endif
fld st0 ; l l
fmul dword [INP + su_outaux_ports.outgain] ; g*l
fadd dword [_AX + su_synthworkspace.left] ; g*l+o
fstp dword [_AX + su_synthworkspace.left] ; o'=g*l+o
fmul dword [INP + su_outaux_ports.auxgain] ; h*l
fadd dword [_AX + su_synthworkspace.aux] ; h*l+a
fstp dword [_AX + su_synthworkspace.aux] ; a'=h*l+a
ret
%endif ; SU_OUTAUX_ID > -1
;-------------------------------------------------------------------------------
; AUX opcode: outputs the signal to aux (or main) port and pops the signal
;-------------------------------------------------------------------------------
; Mono: add gain*ST0 to left port
; Stereo: also add gain*ST1 to right port
;-------------------------------------------------------------------------------
%if AUX_ID > -1
SECT_TEXT(suopaux)
EXPORT MANGLE_FUNC(su_op_aux,0) ; l r
lodsb
mov _DI, [_SP + su_stack.synth]
%ifdef INCLUDE_STEREO_AUX
jnc su_op_aux_mono
call su_op_aux_mono
add _DI, 4
su_op_aux_mono:
%endif
fmul dword [INP + su_aux_ports.gain] ; g*l
fadd dword [_DI + su_synthworkspace.left + _AX*4] ; g*l+o
fstp dword [_DI + su_synthworkspace.left + _AX*4] ; o'=g*l+o
ret
%endif ; SU_AUX_ID > -1
;-------------------------------------------------------------------------------
; SEND opcode: adds the signal to a port
;-------------------------------------------------------------------------------
; Mono: adds signal to a memory address, defined by a word in VAL stream
; Stereo: also add right signal to the following address
;-------------------------------------------------------------------------------
%if SEND_ID > -1
SECT_TEXT(susend)
EXPORT MANGLE_FUNC(su_op_send,0)
lodsw
mov _CX, [_SP + su_stack.wrk]
%ifdef INCLUDE_STEREO_SEND
jnc su_op_send_mono
mov _DI, _AX
inc _AX ; send the right channel first
fxch ; r l
call su_op_send_mono ; (r) l
mov _AX, _DI ; move back to original address
test _AX, SENDPOPFLAG ; if r was not popped and is still in the stack
jnz su_op_send_mono
fxch ; swap them back: l r
su_op_send_mono:
%endif
%ifdef INCLUDE_GLOBAL_SEND
test _AX, SEND_GLOBAL
jz su_op_send_skipglobal
mov _CX, [_SP + su_stack.synth]
su_op_send_skipglobal:
%endif
test _AX, SENDPOPFLAG ; if the SEND_POP bit is not set
jnz su_op_send_skippush
fld st0 ; duplicate the signal on stack: s s
su_op_send_skippush: ; there is signal s, but maybe also another: s (s)
fld dword [INP+su_send_ports.amount] ; a l (l)
do fsub dword [,c_0_5,] ; a-.5 l (l)
fadd st0 ; g=2*a-1 l (l)
and _AX, 0x0000ffff - SENDPOPFLAG - SEND_GLOBAL ; eax = send address
fmulp st1, st0 ; g*l (l)
fadd dword [_CX + _AX*4] ; g*l+L (l),where L is the current value
fstp dword [_CX + _AX*4] ; (l)
ret
%endif ; SU_USE_SEND > -1

View File

@ -1,128 +0,0 @@
;-------------------------------------------------------------------------------
; OUT structs
;-------------------------------------------------------------------------------
%assign OUT_ID -1
%macro USE_OUT 0
%if OUT_ID == -1
%assign OUT_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_out,0),
%xdefine NUMPARAMS NUMPARAMS 1,
%endif
%endmacro
%macro SU_OUT 2
db %2
USE_OUT
%xdefine CMDS CMDS OUT_ID+%1,
%if %1 == 1
%define INCLUDE_STEREO_OUT
%endif
%endmacro
%define GAIN(val) val
struc su_out_ports
.gain resd 1
endstruc
;-------------------------------------------------------------------------------
; OUTAUX structs
;-------------------------------------------------------------------------------
%assign OUTAUX_ID -1
%macro USE_OUTAUX 0
%if OUTAUX_ID == -1
%assign OUTAUX_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_outaux,0),
%xdefine NUMPARAMS NUMPARAMS 2,
%endif
%endmacro
%macro SU_OUTAUX 3
db %2
db %3
USE_OUTAUX
%xdefine CMDS CMDS OUTAUX_ID+%1,
%if %1 == 1
%define INCLUDE_STEREO_OUTAUX
%endif
%endmacro
%define OUTGAIN(val) val
%define AUXGAIN(val) val
struc su_outaux_ports
.outgain resd 1
.auxgain resd 1
endstruc
;-------------------------------------------------------------------------------
; AUX defines
;-------------------------------------------------------------------------------
%assign AUX_ID -1
%macro USE_AUX 0
%if AUX_ID == -1
%assign AUX_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_aux,0),
%xdefine NUMPARAMS NUMPARAMS 1,
%endif
%endmacro
%macro SU_AUX 3
db %2
db %3
USE_AUX
%xdefine CMDS CMDS AUX_ID+%1,
%if %1 == 1
%define INCLUDE_STEREO_AUX
%endif
%endmacro
%define CHANNEL(val) val
struc su_aux_ports
.gain resd 1
endstruc
;-------------------------------------------------------------------------------
; SEND structs
;-------------------------------------------------------------------------------
%assign SEND_ID -1
%macro USE_SEND 0
%if SEND_ID == -1
%assign SEND_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_send,0),
%xdefine NUMPARAMS NUMPARAMS 1,
%endif
%endmacro
%macro SU_SEND 6 ; global send (params: STEREO, AMOUNT, VOICE, UNIT, PORT, SENDPOP)
db %2
%if (%3) > 0
dw SEND_GLOBAL + (su_synthworkspace.voices+(%3-1)*su_voice.size+su_voice.workspace+%4*su_unit.size + su_unit.ports)/4 + %5 + (SENDPOPFLAG * %6)
%define INCLUDE_GLOBAL_SEND
%else
dw ((%4+1)*su_unit.size + su_unit.ports)/4 + %5 + (SENDPOPFLAG * %6)
%endif
USE_SEND
%xdefine CMDS CMDS SEND_ID + %1,
%if %1 == 1
%define INCLUDE_STEREO_SEND
%endif
%endmacro
%define VOICE(val) val
%define UNIT(val) val
%define PORT(val) val
%define AMOUNT(val) val
%define OUTPORT 0
%define SENDPOP(val) val
%define SENDPOPFLAG 0x8000
%define SEND_GLOBAL 0x4000
struc su_send_ports
.amount resd 1
endstruc

View File

@ -1,422 +0,0 @@
;-------------------------------------------------------------------------------
; ENVELOPE opcode: pushes an ADSR envelope value on stack [0,1]
;-------------------------------------------------------------------------------
; Mono: push the envelope value on stack
; Stereo: push the envelope valeu on stack twice
;-------------------------------------------------------------------------------
%if ENVELOPE_ID > -1
SECT_TEXT(suenvelo)
EXPORT MANGLE_FUNC(su_op_envelope,0)
%ifdef INCLUDE_STEREO_ENVELOPE
jnc su_op_envelope_mono
call su_op_envelope_mono
fld st0
ret
su_op_envelope_mono:
%endif
mov eax, dword [INP-su_voice.inputs+su_voice.release] ; eax = su_instrument.release
test eax, eax ; if (eax == 0)
je su_op_envelope_process ; goto process
mov dword [WRK+su_env_work.state], ENV_STATE_RELEASE ; [state]=RELEASE
su_op_envelope_process:
mov eax, dword [WRK+su_env_work.state] ; al=[state]
fld dword [WRK+su_env_work.level] ; x=[level]
cmp al, ENV_STATE_SUSTAIN ; if (al==SUSTAIN)
je short su_op_envelope_leave2 ; goto leave2
su_op_envelope_attac:
cmp al, ENV_STATE_ATTAC ; if (al!=ATTAC)
jne short su_op_envelope_decay ; goto decay
call su_nonlinear_map ; a x, where a=attack
faddp st1, st0 ; a+x
fld1 ; 1 a+x
fucomi st1 ; if (a+x<=1) // is attack complete?
fcmovnb st0, st1 ; a+x a+x
jbe short su_op_envelope_statechange ; else goto statechange
su_op_envelope_decay:
cmp al, ENV_STATE_DECAY ; if (al!=DECAY)
jne short su_op_envelope_release ; goto release
call su_nonlinear_map ; d x, where d=decay
fsubp st1, st0 ; x-d
fld dword [INP+su_env_ports.sustain] ; s x-d, where s=sustain
fucomi st1 ; if (x-d>s) // is decay complete?
fcmovb st0, st1 ; x-d x-d
jnc short su_op_envelope_statechange ; else goto statechange
su_op_envelope_release:
cmp al, ENV_STATE_RELEASE ; if (al!=RELEASE)
jne short su_op_envelope_leave ; goto leave
call su_nonlinear_map ; r x, where r=release
fsubp st1, st0 ; x-r
fldz ; 0 x-r
fucomi st1 ; if (x-r>0) // is release complete?
fcmovb st0, st1 ; x-r x-r, then goto leave
jc short su_op_envelope_leave
su_op_envelope_statechange:
inc dword [WRK+su_env_work.state] ; [state]++
su_op_envelope_leave:
fstp st1 ; x', where x' is the new value
fst dword [WRK+su_env_work.level] ; [level]=x'
su_op_envelope_leave2:
fmul dword [INP+su_env_ports.gain] ; [gain]*x'
ret
%endif ; SU_USE_ENVELOPE
;-------------------------------------------------------------------------------
; NOISE opcode: creates noise
;-------------------------------------------------------------------------------
; Mono: push a random value [-1,1] value on stack
; Stereo: push two (differeent) random values on stack
;-------------------------------------------------------------------------------
%if NOISE_ID > -1
SECT_TEXT(sunoise)
EXPORT MANGLE_FUNC(su_op_noise,0)
mov _CX,_SP
%ifdef INCLUDE_STEREO_NOISE
jnc su_op_noise_mono
call su_op_noise_mono
su_op_noise_mono:
%endif
imul eax, [_CX + su_stack.randseed],16007
mov [_CX + su_stack.randseed],eax
fild dword [_CX + su_stack.randseed]
do fidiv dword [,c_RandDiv,]
fld dword [INP+su_noise_ports.shape]
call su_waveshaper
fld dword [INP+su_noise_ports.gain]
fmulp st1, st0
ret
%define SU_INCLUDE_WAVESHAPER
%endif
;-------------------------------------------------------------------------------
; OSCILLAT opcode: oscillator, the heart of the synth
;-------------------------------------------------------------------------------
; Mono: push oscillator value on stack
; Stereo: push l r on stack, where l has opposite detune compared to r
;-------------------------------------------------------------------------------
%if OSCILLAT_ID > -1
SECT_TEXT(suoscill)
EXPORT MANGLE_FUNC(su_op_oscillat,0)
lodsb ; load the flags
%ifdef RUNTIME_TABLES
%ifdef INCLUDE_SAMPLES
mov _DI, [_SP + su_stack.sampleoffs]; we need to put this in a register, as the stereo & unisons screw the stack positions
%endif ; ain't we lucky that _DI was unused throughout
%endif
fld dword [INP+su_osc_ports.detune] ; e, where e is the detune [0,1]
do fsub dword [,c_0_5,] ; e-.5
fadd st0, st0 ; d=2*e-.5, where d is the detune [-1,1]
%ifdef INCLUDE_STEREO_OSCILLAT
jnc su_op_oscillat_mono
fld st0 ; d d
call su_op_oscillat_mono ; r d
add WRK, 4 ; state vars: r1 l1 r2 l2 r3 l3 r4 l4, for the unison osc phases
fxch ; d r
fchs ; -d r, negate the detune for second round
su_op_oscillat_mono:
%endif
%ifdef INCLUDE_UNISONS
push_registers _AX, WRK, _AX
fldz ; 0 d
fxch ; d a=0, "accumulated signal"
su_op_oscillat_unison_loop:
fst dword [_SP] ; save the current detune, d. We could keep it in fpu stack but it was getting big.
call su_op_oscillat_single ; s a
faddp st1, st0 ; a+=s
test al, UNISONBITS
je su_op_oscillat_unison_out
add WRK, 8
fld dword [INP+su_osc_ports.phaseofs] ; p s
do fadd dword [,c_i12,] ; p s, add some little phase offset to unison oscillators so they don't start in sync
fstp dword [INP+su_osc_ports.phaseofs] ; s note that this changes the phase for second, possible stereo run. That's probably ok
fld dword [_SP] ; d s
do fmul dword [,c_0_5,] ; .5*d s // negate and halve the detune of each oscillator
fchs ; -.5*d s // negate and halve the detune of each oscillator
dec eax
jmp short su_op_oscillat_unison_loop
su_op_oscillat_unison_out:
pop_registers _AX, WRK, _AX
ret
su_op_oscillat_single:
%endif
fld dword [INP+su_osc_ports.transpose]
do fsub dword [,c_0_5,]
do fdiv dword [,c_i128,]
faddp st1
test al, byte LFOFLAG
jnz su_op_oscillat_skipnote
fiadd dword [INP-su_voice.inputs+su_voice.note] ; // st0 is note, st1 is t+d offset
su_op_oscillat_skipnote:
do fmul dword [,c_i12,]
call MANGLE_FUNC(su_power,0)
test al, byte LFOFLAG
jz short su_op_oscillat_normalize_note
do fmul dword [,c_lfo_normalize,] ; // st0 is now frequency for lfo
jmp short su_op_oscillat_normalized
su_op_oscillat_normalize_note:
do fmul dword [,c_freq_normalize,] ; // st0 is now frequency
su_op_oscillat_normalized:
fadd dword [WRK+su_osc_wrk.phase]
fst dword [WRK+su_osc_wrk.phase]
fadd dword [INP+su_osc_ports.phaseofs]
%ifdef INCLUDE_SAMPLES
test al, byte SAMPLE
jz short su_op_oscillat_not_sample
call su_oscillat_sample
jmp su_op_oscillat_shaping ; skip the rest to avoid color phase normalization and colorloading
su_op_oscillat_not_sample:
%endif
fld1
fadd st1, st0
fxch
fprem
fstp st1
fld dword [INP+su_osc_ports.color] ; // c p
; every oscillator test included if needed
%ifdef INCLUDE_SINE
test al, byte SINE
jz short su_op_oscillat_notsine
call su_oscillat_sine
su_op_oscillat_notsine:
%endif
%ifdef INCLUDE_TRISAW
test al, byte TRISAW
jz short su_op_oscillat_not_trisaw
call su_oscillat_trisaw
su_op_oscillat_not_trisaw:
%endif
%ifdef INCLUDE_PULSE
test al, byte PULSE
jz short su_op_oscillat_not_pulse
call su_oscillat_pulse
su_op_oscillat_not_pulse:
%endif
%ifdef INCLUDE_GATE
test al, byte GATE
jz short su_op_oscillat_not_gate
call su_oscillat_gate
jmp su_op_oscillat_gain ; skip waveshaping as the shape parameter is reused for gateshigh
su_op_oscillat_not_gate:
%endif
su_op_oscillat_shaping:
; finally, shape the oscillator and apply gain
fld dword [INP+su_osc_ports.shape]
call su_waveshaper
su_op_oscillat_gain:
fld dword [INP+su_osc_ports.gain]
fmulp st1, st0
ret
%define SU_INCLUDE_WAVESHAPER
%endif
; PULSE
%ifdef INCLUDE_PULSE
SECT_TEXT(supulse)
su_oscillat_pulse:
fucomi st1 ; // c p
fld1
jnc short su_oscillat_pulse_up ; // +1 c p
fchs ; // -1 c p
su_oscillat_pulse_up:
fstp st1 ; // +-1 p
fstp st1 ; // +-1
ret
%endif
; TRISAW
%ifdef INCLUDE_TRISAW
SECT_TEXT(sutrisaw)
su_oscillat_trisaw:
fucomi st1 ; // c p
jnc short su_oscillat_trisaw_up
fld1 ; // 1 c p
fsubr st2, st0 ; // 1 c 1-p
fsubrp st1, st0 ; // 1-c 1-p
su_oscillat_trisaw_up:
fdivp st1, st0 ; // tp'/tc
fadd st0 ; // 2*''
fld1 ; // 1 2*''
fsubp st1, st0 ; // 2*''-1
ret
%endif
; SINE
%ifdef INCLUDE_SINE
SECT_TEXT(susine)
su_oscillat_sine:
fucomi st1 ; // c p
jnc short su_oscillat_sine_do
fstp st1
fsub st0, st0 ; // 0
ret
su_oscillat_sine_do:
fdivp st1, st0 ; // p/c
fldpi ; // pi p
fadd st0 ; // 2*pi p
fmulp st1, st0 ; // 2*pi*p
fsin ; // sin(2*pi*p)
ret
%endif
%ifdef INCLUDE_GATE
SECT_TEXT(sugate)
su_oscillat_gate:
fxch ; p c
fstp st1 ; p
do fmul dword [,c_16,] ; 16*p
push _AX
push _AX
fistp dword [_SP] ; s=int(16*p), stack empty
fld1 ; 1
pop _AX
and al, 0xf ; ax=int(16*p) & 15, stack: 1
bt word [VAL-4],ax ; if bit ax of the gate word is set
jc go4kVCO_gate_bit ; goto gate_bit
fsub st0, st0 ; stack: 0
go4kVCO_gate_bit: ; stack: 0/1, let's call it x
fld dword [WRK+su_osc_wrk.gatestate] ; g x, g is gatestate, x is the input to this filter 0/1
fsub st1 ; g-x x
do fmul dword [,c_dc_const,] ; c(g-x) x
faddp st1, st0 ; x+c(g-x)
fst dword [WRK+su_osc_wrk.gatestate]; g'=x+c(g-x)
pop _AX ; Another way to see this (c~0.996)
ret ; g'=cg+(1-c)x
; This is a low-pass to smooth the gate transitions
%define USE_C_16
%define USE_C_DC_CONST
%endif
; SAMPLES
%ifdef INCLUDE_SAMPLES
SECT_TEXT(suoscsam)
su_oscillat_sample: ; p
push_registers _AX,_DX,_CX,_BX ; edx must be saved, eax & ecx if this is stereo osc
push _AX
mov al, byte [VAL-4] ; reuse "color" as the sample number
%ifdef RUNTIME_TABLES ; when using RUNTIME_TABLES, assumed the sample_offset ptr is in _DI
do{lea _DI, [}, _DI, _AX*8,] ; edi points now to the sample table entry
%else
do{lea _DI, [}, MANGLE_DATA(su_sample_offsets), _AX*8,]; edi points now to the sample table entry
%endif
do fmul dword [,c_samplefreq_scaling,] ; p*r
fistp dword [_SP]
pop _DX ; edx is now the sample number
movzx ebx, word [_DI + su_sample_offset.loopstart] ; ecx = loopstart
sub edx, ebx ; if sample number < loop start
jl su_oscillat_sample_not_looping ; then we're not looping yet
mov eax, edx ; eax = sample number
movzx ecx, word [_DI + su_sample_offset.looplength] ; edi is now the loop length
xor edx, edx ; div wants edx to be empty
div ecx ; edx is now the remainder
su_oscillat_sample_not_looping:
add edx, ebx ; sampleno += loopstart
add edx, dword [_DI + su_sample_offset.start]
do fild word [,MANGLE_DATA(su_sample_table),_DX*2,]
do fdiv dword [,c_32767,]
pop_registers _AX,_DX,_CX,_BX
ret
%define USE_C_32767
%define USE_C_SAMPLEFREQ_SCALING
%endif
;-------------------------------------------------------------------------------
; LOADVAL opcode
;-------------------------------------------------------------------------------
; Mono: push 2*v-1 on stack, where v is the input to port "value"
; Stereo: push 2*v-1 twice on stack
;-------------------------------------------------------------------------------
%if LOADVAL_ID > -1
SECT_TEXT(suloadvl)
EXPORT MANGLE_FUNC(su_op_loadval,0)
%ifdef INCLUDE_STEREO_LOADVAL
jnc su_op_loadval_mono
call su_op_loadval_mono
su_op_loadval_mono:
%endif
fld dword [INP+su_load_val_ports.value] ; v
do fsub dword [,c_0_5,]
fadd st0 ; 2*v-1
ret
%endif ; SU_USE_LOAD_VAL
;-------------------------------------------------------------------------------
; RECEIVE opcode
;-------------------------------------------------------------------------------
; Mono: push l on stack, where l is the left channel received
; Stereo: push l r on stack
;-------------------------------------------------------------------------------
%if RECEIVE_ID > -1
SECT_TEXT(sureceiv)
EXPORT MANGLE_FUNC(su_op_receive,0)
lea _DI, [WRK+su_unit.ports]
%ifdef INCLUDE_STEREO_RECEIVE
jnc su_op_receive_mono
xor ecx,ecx
fld dword [_DI+su_receive_ports.right]
mov dword [_DI+su_receive_ports.right],ecx
su_op_receive_mono:
%endif
xor ecx,ecx
fld dword [_DI+su_receive_ports.left]
mov dword [_DI+su_receive_ports.left],ecx
ret
%endif ; RECEIVE_ID > -1
;-------------------------------------------------------------------------------
; IN opcode: inputs and clears a global port
;-------------------------------------------------------------------------------
; Mono: push the left channel of a global port (out or aux)
; Stereo: also push the right channel (stack in l r order)
;-------------------------------------------------------------------------------
%if IN_ID > -1
SECT_TEXT(suopin)
EXPORT MANGLE_FUNC(su_op_in,0)
lodsb
mov _DI, [_SP + su_stack.synth]
%ifdef INCLUDE_STEREO_IN
jnc su_op_in_mono
xor ecx, ecx ; we cannot xor before jnc, so we have to do it mono & stereo. LAHF / SAHF could do it, but is the same number of bytes with more entropy
fld dword [_DI + su_synthworkspace.right + _AX*4]
mov dword [_DI + su_synthworkspace.right + _AX*4], ecx
su_op_in_mono:
%endif
xor ecx, ecx
fld dword [_DI + su_synthworkspace.left + _AX*4]
mov dword [_DI + su_synthworkspace.left + _AX*4], ecx
ret
%endif ; SU_IN_ID > -1

View File

@ -1,262 +0,0 @@
;-------------------------------------------------------------------------------
; ENVELOPE structs
;-------------------------------------------------------------------------------
%assign ENVELOPE_ID -1
%macro USE_ENVELOPE 0
%if ENVELOPE_ID == -1
%assign ENVELOPE_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_envelope,0),
%xdefine NUMPARAMS NUMPARAMS 5,
%endif
%endmacro
%macro SU_ENVELOPE 6
db %2
db %3
db %4
db %5
db %6
USE_ENVELOPE
%xdefine CMDS CMDS ENVELOPE_ID+%1,
%if %1 == 1
%define INCLUDE_STEREO_ENVELOPE
%endif
%endmacro
%define ATTACK(val) val
%define DECAY(val) val
%define SUSTAIN(val) val
%define RELEASE(val) val
%define GAIN(val) val
struc su_env_ports
.attac resd 1
.decay resd 1
.sustain resd 1
.release resd 1
.gain resd 1
endstruc
struc su_env_work
.state resd 1
.level resd 1
endstruc
%define ENV_STATE_ATTAC 0
%define ENV_STATE_DECAY 1
%define ENV_STATE_SUSTAIN 2
%define ENV_STATE_RELEASE 3
%define ENV_STATE_OFF 4
;-------------------------------------------------------------------------------
; OSCILLAT structs
;-------------------------------------------------------------------------------
%assign OSCILLAT_ID -1
%macro USE_OSCILLAT 0
%if OSCILLAT_ID == -1
%assign OSCILLAT_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_oscillat,0),
%xdefine NUMPARAMS NUMPARAMS 6,
%endif
%endmacro
%define SAMPLE 0x80
%define SINE 0x40
%define TRISAW 0x20
%define PULSE 0x10
%define LFOFLAG 0x08
%define GATE 0x04
%define UNISONBITS 0x03
%macro SU_OSCILLATOR 10
db %2
db %3
db %4
db %5
db %6
db %7
USE_OSCILLAT
%xdefine CMDS CMDS OSCILLAT_ID + %1,
%if %1 == 1
%define INCLUDE_STEREO_OSCILLAT
%endif
%if %8 == 0 ; 0 = SINE
db SINE + (LFOFLAG * %9) + %10 ; TYPE + LFO + UNISON
%define INCLUDE_SINE
%elif %8 == 1 ; 1 = TRISAW
db TRISAW + (LFOFLAG * %9) + %10 ; TYPE + LFO + UNISON
%define INCLUDE_TRISAW
%elif %8 == 2
db PULSE + (LFOFLAG * %9) + %10 ; TYPE + LFO + UNISON
%define INCLUDE_PULSE
%elif %8 == 3
db GATE + (LFOFLAG * %9) + %10 ; TYPE + LFO + UNISON
%define INCLUDE_GATE
%elif %8 == 4
db SAMPLE + (LFOFLAG * %9) + %10 ; TYPE + LFO + UNISON
%define INCLUDE_SAMPLES
%else
%error Unknown oscillator type!
%endif
%if (%10) > 0
%define INCLUDE_UNISONS
%endif
%endmacro
struc su_osc_ports
.transpose resd 1
.detune resd 1
.phaseofs resd 1
.color resd 1
.shape resd 1
.gain resd 1
endstruc
struc su_osc_wrk
.phase resd 1
.gatestate equ 16 ; we put is late so only UNISON3 and UNISON4 are unusable with gate
endstruc
%define TRANSPOSE(val) val
%define DETUNE(val) val
%define PHASE(val) val
%define COLOR(val) val
%define SHAPE(val) val
%define TYPE(val) val
%define LFO(val) val
%define UNISON(val) val
%define FLAGS(val) val
;-------------------------------------------------------------------------------
; Sample related defines
;-------------------------------------------------------------------------------
%macro BEGIN_SAMPLE_OFFSETS 0
SECT_DATA(susamoff)
EXPORT MANGLE_DATA(su_sample_offsets)
%endmacro
%macro SAMPLE_OFFSET 3
dd %1
dw %2
dw %3
%endmacro
%define START(val) val
%define LOOPSTART(val) val
%define LOOPLENGTH(val) val
%define END_SAMPLE_OFFSETS
struc su_sample_offset ; length conveniently 8, so easy to index
.start resd 1
.loopstart resw 1
.looplength resw 1
endstruc
;-------------------------------------------------------------------------------
; NOISE structs
;-------------------------------------------------------------------------------
%assign NOISE_ID -1
%macro USE_NOISE 0
%if NOISE_ID == -1
%assign NOISE_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_noise,0),
%xdefine NUMPARAMS NUMPARAMS 2,
%endif
%endmacro
%macro SU_NOISE 3
db %2
db %3
USE_NOISE
%xdefine CMDS CMDS NOISE_ID + %1,
%if %1 == 1
%define INCLUDE_STEREO_NOISE
%endif
%endmacro
struc su_noise_ports
.shape resd 1
.gain resd 1
endstruc
;-------------------------------------------------------------------------------
; LOAD_VAL structs
;-------------------------------------------------------------------------------
%assign LOADVAL_ID -1
%macro USE_LOAD_VAL 0
%if LOADVAL_ID == -1
%assign LOADVAL_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_loadval,0),
%xdefine NUMPARAMS NUMPARAMS 1,
%endif
%endmacro
%macro SU_LOADVAL 2
db %2
USE_LOAD_VAL
%xdefine CMDS CMDS LOADVAL_ID+%1,
%if %1 == 1
%define INCLUDE_STEREO_LOADVAL
%endif
%endmacro
%define VALUE(val) val
struc su_load_val_ports
.value resd 1
endstruc
;-------------------------------------------------------------------------------
; RECEIVE structs
;-------------------------------------------------------------------------------
%assign RECEIVE_ID -1
%macro USE_RECEIVE 0
%if RECEIVE_ID == -1
%assign RECEIVE_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_receive,0),
%xdefine NUMPARAMS NUMPARAMS 0,
%endif
%endmacro
%macro SU_RECEIVE 1
USE_RECEIVE
%xdefine CMDS CMDS RECEIVE_ID+%1,
%if %1 == 1
%define INCLUDE_STEREO_RECEIVE
%endif
%endmacro
struc su_receive_ports
.left resd 1
.right resd 1
endstruc
;-------------------------------------------------------------------------------
; IN defines
;-------------------------------------------------------------------------------
%assign IN_ID -1
%macro USE_IN 0
%if IN_ID == -1
%assign IN_ID CUR_ID
%assign CUR_ID CUR_ID + 2
%xdefine OPCODES OPCODES MANGLE_FUNC(su_op_in,0),
%xdefine NUMPARAMS NUMPARAMS 0,
%endif
%endmacro
%macro SU_IN 2
db %2
USE_IN
%xdefine CMDS CMDS IN_ID+%1,
%if %1 == 1
%define INCLUDE_STEREO_IN
%endif
%endmacro

View File

@ -1,41 +0,0 @@
%ifdef INCLUDE_SAMPLES
%define SAMPLE_TABLE_SIZE 3440660 ; size of gmdls
extern _OpenFile@12 ; requires windows
extern _ReadFile@20 ; requires windows
SECT_TEXT(sugmdls)
EXPORT MANGLE_FUNC(su_load_gmdls,0)
mov edx, MANGLE_DATA(su_sample_table)
mov ecx, su_gmdls_path1
su_gmdls_pathloop:
push 0 ; OF_READ
push edx ; &ofstruct, blatantly reuse the sample table
push ecx ; path
call _OpenFile@12 ; eax = OpenFile(path,&ofstruct,OF_READ)
add ecx, su_gmdls_path2 - su_gmdls_path1 ; if we ever get to third, then crash
cmp eax, -1 ; eax == INVALID?
je su_gmdls_pathloop
push 0 ; NULL
push edx ; &bytes_read, reusing sample table again; it does not matter that the first four bytes are trashed
push SAMPLE_TABLE_SIZE ; number of bytes to read
push edx ; here we actually pass the sample table to readfile
push eax ; handle to file
call _ReadFile@20 ; Readfile(handle,&su_sample_table,SAMPLE_TABLE_SIZE,&bytes_read,NULL)
ret
SECT_DATA(sugmpath)
su_gmdls_path1:
db 'drivers/gm.dls',0
su_gmdls_path2:
db 'drivers/etc/gm.dls',0
SECT_BSS(susamtbl)
EXPORT MANGLE_DATA(su_sample_table)
resb SAMPLE_TABLE_SIZE ; size of gmdls.
%endif

View File

@ -1,45 +0,0 @@
%ifdef INCLUDE_SAMPLES
%define SAMPLE_TABLE_SIZE 3440660 ; size of gmdls
extern OpenFile ; requires windows
extern ReadFile ; requires windows
SECT_TEXT(sugmdls)
EXPORT MANGLE_FUNC(su_load_gmdls,0)
; Win64 ABI: RCX, RDX, R8, and R9
sub rsp, 40 ; Win64 ABI requires "shadow space" + space for one parameter.
mov rdx, PTRWORD MANGLE_DATA(su_sample_table)
mov rcx, PTRWORD su_gmdls_path1
su_gmdls_pathloop:
xor r8,r8 ; OF_READ
push rdx ; &ofstruct, blatantly reuse the sample table
push rcx
call OpenFile ; eax = OpenFile(path,&ofstruct,OF_READ)
pop rcx
add rcx, su_gmdls_path2 - su_gmdls_path1 ; if we ever get to third, then crash
pop rdx
cmp eax, -1 ; ecx == INVALID?
je su_gmdls_pathloop
movsxd rcx, eax
mov qword [rsp+32],0
mov r9, rdx
mov r8d, SAMPLE_TABLE_SIZE ; number of bytes to read
call ReadFile ; Readfile(handle,&su_sample_table,SAMPLE_TABLE_SIZE,&bytes_read,NULL)
add rsp, 40 ; shadow space, as required by Win64 ABI
ret
SECT_DATA(sugmpath)
su_gmdls_path1:
db 'drivers/gm.dls',0
su_gmdls_path2:
db 'drivers/etc/gm.dls',0
SECT_BSS(susamtbl)
EXPORT MANGLE_DATA(su_sample_table)
resb SAMPLE_TABLE_SIZE ; size of gmdls.
%endif

View File

@ -1,304 +0,0 @@
; source file for compiling sointu as a library
%define SU_DISABLE_PLAYER
%include "sointu/header.inc"
; use every opcode
USE_ADD
USE_ADDP
USE_POP
USE_LOADNOTE
USE_MUL
USE_MULP
USE_PUSH
USE_XCH
USE_DISTORT
USE_HOLD
USE_CRUSH
USE_GAIN
USE_INVGAIN
USE_FILTER
USE_CLIP
USE_PAN
USE_DELAY
USE_COMPRES
USE_SPEED
USE_OUT
USE_OUTAUX
USE_AUX
USE_SEND
USE_ENVELOPE
USE_NOISE
USE_OSCILLAT
USE_LOAD_VAL
USE_RECEIVE
USE_IN
; include stereo variant of each opcode
%define INCLUDE_STEREO_ADD
%define INCLUDE_STEREO_ADDP
%define INCLUDE_STEREO_POP
%define INCLUDE_STEREO_LOADNOTE
%define INCLUDE_STEREO_MUL
%define INCLUDE_STEREO_MULP
%define INCLUDE_STEREO_PUSH
%define INCLUDE_STEREO_XCH
%define INCLUDE_STEREO_DISTORT
%define INCLUDE_STEREO_HOLD
%define INCLUDE_STEREO_CRUSH
%define INCLUDE_STEREO_GAIN
%define INCLUDE_STEREO_INVGAIN
%define INCLUDE_STEREO_FILTER
%define INCLUDE_STEREO_CLIP
%define INCLUDE_STEREO_PAN
%define INCLUDE_STEREO_DELAY
%define INCLUDE_STEREO_COMPRES
%define INCLUDE_STEREO_SPEED
%define INCLUDE_STEREO_OUT
%define INCLUDE_STEREO_OUTAUX
%define INCLUDE_STEREO_AUX
%define INCLUDE_STEREO_SEND
%define INCLUDE_STEREO_ENVELOPE
%define INCLUDE_STEREO_NOISE
%define INCLUDE_STEREO_OSCILLAT
%define INCLUDE_STEREO_LOADVAL
%define INCLUDE_STEREO_RECEIVE
%define INCLUDE_STEREO_IN
; include all features inside all opcodes
%define INCLUDE_TRISAW
%define INCLUDE_SINE
%define INCLUDE_PULSE
%define INCLUDE_GATE
%define INCLUDE_UNISONS
%define INCLUDE_POLYPHONY
%define INCLUDE_MULTIVOICE_TRACKS
%define INCLUDE_DELAY_MODULATION
%define INCLUDE_LOWPASS
%define INCLUDE_BANDPASS
%define INCLUDE_HIGHPASS
%define INCLUDE_NEGBANDPASS
%define INCLUDE_NEGHIGHPASS
%define INCLUDE_GLOBAL_SEND
%define INCLUDE_DELAY_NOTETRACKING
%define INCLUDE_DELAY_FLOAT_TIME
%ifidn __OUTPUT_FORMAT__,win32
%define INCLUDE_SAMPLES
%define INCLUDE_GMDLS
%endif
%ifidn __OUTPUT_FORMAT__,win64
%define INCLUDE_SAMPLES
%define INCLUDE_GMDLS
%endif
%include "sointu/footer.inc"
section .text
struc su_sampleoff
.start resd 1
.loopstart resw 1
.looplength resw 1
.size:
endstruc
struc su_synth
.synthwrk resb su_synthworkspace.size
.delaywrks resb su_delayline_wrk.size * 64
.delaytimes resw 768
.sampleoffs resb su_sampleoff.size * 256
.randseed resd 1
.globaltime resd 1
.commands resb 32 * 64
.values resb 32 * 64 * 8
.polyphony resd 1
.numvoices resd 1
endstruc
SECT_TEXT(sursampl)
EXPORT MANGLE_FUNC(su_render,16)
%if BITS == 32 ; stdcall
pushad ; push registers
mov ecx, [esp + 4 + 32] ; ecx = &synthState
mov edx, [esp + 8 + 32] ; edx = &buffer
mov esi, [esp + 12 + 32] ; esi = &samples
mov ebx, [esp + 16 + 32] ; ebx = &time
%else
%ifidn __OUTPUT_FORMAT__,win64 ; win64 ABI: rcx = &synth, rdx = &buffer, r8 = &bufsize, r9 = &time
push_registers rdi, rsi, rbx, rbp ; win64 ABI: these registers are non-volatile
mov rsi, r8 ; rsi = &samples
mov rbx, r9 ; rbx = &time
%else ; System V ABI: rdi = &synth, rsi = &buffer, rdx = &samples, rcx = &time
push_registers rbx, rbp ; System V ABI: these registers are non-volatile
mov rbx, rcx ; rbx points to time
xchg rsi, rdx ; rdx points to buffer, rsi points to samples
mov rcx, rdi ; rcx = &Synthstate
%endif
%endif
sub _SP,108 ; allocate space on stack for the FPU state
fsave [_SP] ; save the FPU state to stack & reset the FPU
push _SI ; push the pointer to samples
push _BX ; push the pointer to time
xor eax, eax ; samplenumber starts at 0
push _AX ; push samplenumber to stack
mov esi, [_SI] ; zero extend dereferenced pointer
push _SI ; push bufsize
push _DX ; push bufptr
push _CX ; this takes place of the voicetrack
lea _AX, [_CX + su_synth.sampleoffs]
push _AX
lea _AX, [_CX + su_synth.delaytimes]
push _AX
mov eax, [_CX + su_synth.randseed]
push _AX ; randseed
mov eax, [_CX + su_synth.globaltime]
push _AX ; global tick time
mov ebx, dword [_BX] ; zero extend dereferenced pointer
push _BX ; the nominal rowlength should be time_in
xor eax, eax ; rowtick starts at 0
su_render_samples_loop:
push _DI
fnstsw [_SP] ; store the FPU status flag to stack top
pop _DI ; _DI = FPU status flag
and _DI, 0011100001000101b ; mask TOP pointer, stack error, zero divide and invalid operation
test _DI,_DI ; all the aforementioned bits should be 0!
jne su_render_samples_time_finish ; otherwise, we exit due to error
cmp eax, [_SP] ; if rowtick >= maxtime
jge su_render_samples_time_finish ; goto finish
mov ecx, [_SP + PTRSIZE*7] ; ecx = buffer length in samples
cmp [_SP + PTRSIZE*8], ecx ; if samples >= maxsamples
jge su_render_samples_time_finish ; goto finish
inc eax ; time++
inc dword [_SP + PTRSIZE*8] ; samples++
mov _CX, [_SP + PTRSIZE*5]
push _AX ; push rowtick
mov eax, [_CX + su_synth.polyphony]
push _AX ;polyphony
mov eax, [_CX + su_synth.numvoices]
push _AX ;numvoices
lea _DX, [_CX+ su_synth.synthwrk]
lea COM, [_CX+ su_synth.commands]
lea VAL, [_CX+ su_synth.values]
lea WRK, [_DX + su_synthworkspace.voices]
lea _CX, [_CX+ su_synth.delaywrks - su_delayline_wrk.filtstate]
call MANGLE_FUNC(su_run_vm,0)
pop _AX
pop _AX
mov _DI, [_SP + PTRSIZE*7] ; edi containts buffer ptr
mov _CX, [_SP + PTRSIZE*6]
lea _SI, [_CX + su_synth.synthwrk + su_synthworkspace.left]
movsd ; copy left channel to output buffer
movsd ; copy right channel to output buffer
mov [_SP + PTRSIZE*7], _DI ; save back the updated ptr
lea _DI, [_SI-8]
xor eax, eax
stosd ; clear left channel so the VM is ready to write them again
stosd ; clear right channel so the VM is ready to write them again
pop _AX
inc dword [_SP + PTRSIZE] ; increment global time, used by delays
jmp su_render_samples_loop
su_render_samples_time_finish:
pop _CX
pop _BX
pop _DX
pop _CX ; discard delaytimes ptr
pop _CX ; discard samplesoffs ptr
pop _CX
mov [_CX + su_synth.randseed], edx
mov [_CX + su_synth.globaltime], ebx
pop _BX
pop _BX
pop _DX
pop _BX ; pop the pointer to time
pop _SI ; pop the pointer to samples
mov dword [_SI], edx ; *samples = samples rendered
mov dword [_BX], eax ; *time = time ticks rendered
mov _AX,_DI ; _DI was the masked FPU status flag, _AX is return value
frstor [_SP] ; restore fpu state
add _SP,108 ; rewind the stack allocate for FPU state
%if BITS == 32 ; stdcall
mov [_SP + 28],eax ; we want to return eax, but popad pops everything, so put eax to stack for popad to pop
popad
ret 16
%else
%ifidn __OUTPUT_FORMAT__,win64
pop_registers rdi, rsi, rbx, rbp ; win64 ABI: these registers are non-volatile
%else
pop_registers rbx, rbp ; System V ABI: these registers are non-volatile
%endif
ret
%endif
SECT_DATA(opcodeid)
; Arithmetic opcode ids
EXPORT MANGLE_DATA(su_add_id)
dd ADD_ID
EXPORT MANGLE_DATA(su_addp_id)
dd ADDP_ID
EXPORT MANGLE_DATA(su_pop_id)
dd POP_ID
EXPORT MANGLE_DATA(su_loadnote_id)
dd LOADNOTE_ID
EXPORT MANGLE_DATA(su_mul_id)
dd MUL_ID
EXPORT MANGLE_DATA(su_mulp_id)
dd MULP_ID
EXPORT MANGLE_DATA(su_push_id)
dd PUSH_ID
EXPORT MANGLE_DATA(su_xch_id)
dd XCH_ID
; Effect opcode ids
EXPORT MANGLE_DATA(su_distort_id)
dd DISTORT_ID
EXPORT MANGLE_DATA(su_hold_id)
dd HOLD_ID
EXPORT MANGLE_DATA(su_crush_id)
dd CRUSH_ID
EXPORT MANGLE_DATA(su_gain_id)
dd GAIN_ID
EXPORT MANGLE_DATA(su_invgain_id)
dd INVGAIN_ID
EXPORT MANGLE_DATA(su_filter_id)
dd FILTER_ID
EXPORT MANGLE_DATA(su_clip_id)
dd CLIP_ID
EXPORT MANGLE_DATA(su_pan_id)
dd PAN_ID
EXPORT MANGLE_DATA(su_delay_id)
dd DELAY_ID
EXPORT MANGLE_DATA(su_compres_id)
dd COMPRES_ID
; Flowcontrol opcode ids
EXPORT MANGLE_DATA(su_advance_id)
dd SU_ADVANCE_ID
EXPORT MANGLE_DATA(su_speed_id)
dd SPEED_ID
; Sink opcode ids
EXPORT MANGLE_DATA(su_out_id)
dd OUT_ID
EXPORT MANGLE_DATA(su_outaux_id)
dd OUTAUX_ID
EXPORT MANGLE_DATA(su_aux_id)
dd AUX_ID
EXPORT MANGLE_DATA(su_send_id)
dd SEND_ID
; Source opcode ids
EXPORT MANGLE_DATA(su_envelope_id)
dd ENVELOPE_ID
EXPORT MANGLE_DATA(su_noise_id)
dd NOISE_ID
EXPORT MANGLE_DATA(su_oscillat_id)
dd OSCILLAT_ID
EXPORT MANGLE_DATA(su_loadval_id)
dd LOADVAL_ID
EXPORT MANGLE_DATA(su_receive_id)
dd RECEIVE_ID
EXPORT MANGLE_DATA(su_in_id)
dd IN_ID

View File

@ -1,4 +1,4 @@
{{- if .Opcode "pop"}}
{{- if .HasOp "pop"}}
;-------------------------------------------------------------------------------
; POP opcode: remove (discard) the topmost signal from the stack
;-------------------------------------------------------------------------------
@ -24,7 +24,7 @@ su_op_pop_mono:
{{end}}
{{- if .Opcode "add"}}
{{- if .HasOp "add"}}
;-------------------------------------------------------------------------------
; ADD opcode: add the two top most signals on the stack
;-------------------------------------------------------------------------------
@ -58,7 +58,7 @@ su_op_add_mono:
{{end}}
{{- if .Opcode "addp"}}
{{- if .HasOp "addp"}}
;-------------------------------------------------------------------------------
; ADDP opcode: add the two top most signals on the stack and pop
;-------------------------------------------------------------------------------
@ -84,7 +84,7 @@ su_op_addp_mono:
{{end}}
{{- if .Opcode "loadnote"}}
{{- if .HasOp "loadnote"}}
;-------------------------------------------------------------------------------
; LOADNOTE opcode: load the current note, scaled to [-1,1]
;-------------------------------------------------------------------------------
@ -109,7 +109,7 @@ su_op_addp_mono:
{{end}}
{{- if .Opcode "mul"}}
{{- if .HasOp "mul"}}
;-------------------------------------------------------------------------------
; MUL opcode: multiply the two top most signals on the stack
;-------------------------------------------------------------------------------
@ -129,7 +129,7 @@ su_op_mul_mono:
{{end}}
{{- if .Opcode "mulp"}}
{{- if .HasOp "mulp"}}
;-------------------------------------------------------------------------------
; MULP opcode: multiply the two top most signals on the stack and pop
;-------------------------------------------------------------------------------
@ -155,7 +155,7 @@ su_op_mulp_mono:
{{end}}
{{- if .Opcode "push"}}
{{- if .HasOp "push"}}
;-------------------------------------------------------------------------------
; PUSH opcode: push the topmost signal on the stack
;-------------------------------------------------------------------------------
@ -181,7 +181,7 @@ su_op_push_mono:
{{end}}
{{- if .Opcode "xch"}}
{{- if .HasOp "xch"}}
;-------------------------------------------------------------------------------
; XCH opcode: exchange the signals on the stack
;-------------------------------------------------------------------------------

View File

@ -1,4 +1,4 @@
{{- if .Opcode "distort"}}
{{- if .HasOp "distort"}}
;-------------------------------------------------------------------------------
; DISTORT opcode: apply distortion on the signal
;-------------------------------------------------------------------------------
@ -14,7 +14,7 @@
{{end}}
{{- if .Opcode "hold"}}
{{- if .HasOp "hold"}}
;-------------------------------------------------------------------------------
; HOLD opcode: sample and hold the signal, reducing sample rate
;-------------------------------------------------------------------------------
@ -46,7 +46,7 @@ su_op_hold_holding:
{{end}}
{{- if .Opcode "crush"}}
{{- if .HasOp "crush"}}
;-------------------------------------------------------------------------------
; CRUSH opcode: quantize the signal to finite number of levels
;-------------------------------------------------------------------------------
@ -64,7 +64,7 @@ su_op_hold_holding:
{{end}}
{{- if .Opcode "gain"}}
{{- if .HasOp "gain"}}
;-------------------------------------------------------------------------------
; GAIN opcode: apply gain on the signal
;-------------------------------------------------------------------------------
@ -88,7 +88,7 @@ su_op_gain_mono:
{{end}}
{{- if .Opcode "invgain"}}
{{- if .HasOp "invgain"}}
;-------------------------------------------------------------------------------
; INVGAIN opcode: apply inverse gain on the signal
;-------------------------------------------------------------------------------
@ -112,7 +112,7 @@ su_op_invgain_mono:
{{end}}
{{- if .Opcode "filter"}}
{{- if .HasOp "filter"}}
;-------------------------------------------------------------------------------
; FILTER opcode: perform low/high/band-pass/notch etc. filtering on the signal
;-------------------------------------------------------------------------------
@ -139,31 +139,31 @@ su_op_invgain_mono:
fadd dword [{{.WRK}}+8] ; f2*h'+b
fstp dword [{{.WRK}}+8] ; b'=f2*h'+b
fldz ; 0
{{- if .HasParamValue "filter" "lowpass" 1}}
{{- if .SupportsParamValue "filter" "lowpass" 1}}
test al, byte 0x40
jz short su_op_filter_skiplowpass
fadd dword [{{.WRK}}]
su_op_filter_skiplowpass:
{{- end}}
{{- if .HasParamValue "filter" "bandpass" 1}}
{{- if .SupportsParamValue "filter" "bandpass" 1}}
test al, byte 0x20
jz short su_op_filter_skipbandpass
fadd dword [{{.WRK}}+8]
su_op_filter_skipbandpass:
{{- end}}
{{- if .HasParamValue "filter" "highpass" 1}}
{{- if .SupportsParamValue "filter" "highpass" 1}}
test al, byte 0x10
jz short su_op_filter_skiphighpass
fadd dword [{{.WRK}}+4]
su_op_filter_skiphighpass:
{{- end}}
{{- if .HasParamValue "filter" "negbandpass" 1}}
{{- if .SupportsParamValue "filter" "negbandpass" 1}}
test al, byte 0x08
jz short su_op_filter_skipnegbandpass
fsub dword [{{.WRK}}+8]
su_op_filter_skipnegbandpass:
{{- end}}
{{- if .HasParamValue "filter" "neghighpass" 1}}
{{- if .SupportsParamValue "filter" "neghighpass" 1}}
test al, byte 0x04
jz short su_op_filter_skipneghighpass
fsub dword [{{.WRK}}+4]
@ -173,7 +173,7 @@ su_op_filter_skipneghighpass:
{{end}}
{{- if .Opcode "clip"}}
{{- if .HasOp "clip"}}
;-------------------------------------------------------------------------------
; CLIP opcode: clips the signal into [-1,1] range
;-------------------------------------------------------------------------------
@ -188,7 +188,7 @@ su_op_filter_skipneghighpass:
{{end}}
{{- if .Opcode "pan" -}}
{{- if .HasOp "pan" -}}
;-------------------------------------------------------------------------------
; PAN opcode: pan the signal
;-------------------------------------------------------------------------------
@ -220,7 +220,7 @@ su_op_pan_do:
{{end}}
{{- if .Opcode "delay"}}
{{- if .HasOp "delay"}}
;-------------------------------------------------------------------------------
; DELAY opcode: adds delay effect to the signal
;-------------------------------------------------------------------------------
@ -234,12 +234,13 @@ su_op_pan_do:
lodsw ; al = delay index, ah = delay count
{{- .PushRegs .VAL "DelayVal" .COM "DelayCom" | indent 4}}
movzx ebx, al
; %ifdef RUNTIME_TABLES ; when using runtime tables, delaytimes is pulled from the stack so can be a pointer to heap
; mov _SI, [{{.SP}} + su_stack.delaytimes + PUSH_REG_SIZE(2)]
; lea _BX, [_SI + _BX*2]
; %else
{{.Prepare "su_delay_times" | indent 4}}
{{- if .Library}}
mov {{.SI}}, [{{.Stack "DelayTable"}}] ; when using runtime tables, delaytimes is pulled from the stack so can be a pointer to heap
lea {{.BX}}, [{{.SI}} + {{.BX}}*2]
{{- else}}
{{- .Prepare "su_delay_times" | indent 4}}
lea {{.BX}},[{{.Use "su_delay_times"}} + {{.BX}}*2] ; BX now points to the right position within delay time table
{{- end}}
movzx esi, word [{{.Stack "GlobalTick"}}] ; notice that we load word, so we wrap at 65536
mov {{.CX}}, {{.PTRWORD}} [{{.Stack "DelayWorkSpace"}}] ; {{.WRK}} is now the separate delay workspace, as they require a lot more space
{{- if .StereoAndMono "delay"}}
@ -256,7 +257,7 @@ su_op_delay_mono: ; flow into mono delay
call su_op_delay_do ; when stereo delay is not enabled, we could inline this to save 5 bytes, but I expect stereo delay to be farely popular so maybe not worth the hassle
mov {{.PTRWORD}} [{{.Stack "DelayWorkSpace"}}],{{.CX}} ; move delay workspace pointer back to stack.
{{- .PopRegs .VAL .COM | indent 4}}
{{- if .UsesDelayModulation}}
{{- if .SupportsModulation "delay" "delaytime"}}
xor eax, eax
mov dword [{{.Modulation "delay" "delaytime"}}], eax
{{- end}}
@ -281,9 +282,9 @@ su_op_delay_mono: ; flow into mono delay
fxch ; y p*p*x
fmul dword [{{.Input "delay" "dry"}}] ; dr*y p*p*x
su_op_delay_loop:
{{- if or .UsesDelayModulation (.HasParamValue "delay" "notetracking" 1)}} ; delaytime modulation or note syncing require computing the delay time in floats
{{- if or (.SupportsModulation "delay" "delaytime") (.SupportsParamValue "delay" "notetracking" 1)}} ; delaytime modulation or note syncing require computing the delay time in floats
fild word [{{.BX}}] ; k dr*y p*p*x, where k = delay time
{{- if .HasParamValue "delay" "notetracking" 1}}
{{- if .SupportsParamValue "delay" "notetracking" 1}}
test ah, 1 ; note syncing is the least significant bit of ah, 0 = ON, 1 = OFF
jne su_op_delay_skipnotesync
fild dword [{{.INP}}-su_voice.inputs+su_voice.note]
@ -293,7 +294,7 @@ su_op_delay_loop:
fdivp st1, st0 ; use 10787 for delaytime to have neutral transpose
su_op_delay_skipnotesync:
{{- end}}
{{- if .UsesDelayModulation}}
{{- if .SupportsModulation "delay" "delaytime"}}
fld dword [{{.Modulation "delay" "delaytime"}}]
{{- .Float 32767.0 | .Prepare | indent 8}}
fmul dword [{{.Float 32767.0 | .Use}}] ; scale it up, as the modulations would be too small otherwise
@ -339,7 +340,7 @@ su_op_delay_loop:
{{end}}
{{- if .Opcode "compressor"}}
{{- if .HasOp "compressor"}}
;-------------------------------------------------------------------------------
; COMPRES opcode: push compressor gain to stack
;-------------------------------------------------------------------------------

View File

@ -1,4 +1,4 @@
{{- if .Opcode "speed" -}}
{{- if .HasOp "speed" -}}
;-------------------------------------------------------------------------------
; SPEED opcode: modulate the speed (bpm) of the song based on ST0
;-------------------------------------------------------------------------------

View File

@ -1,3 +1,5 @@
{{- if .SupportsParamValue "oscillator" "type" .Sample}}
{{- if eq .OS "windows"}}
{{.ExportFunc "su_load_gmdls"}}
{{- if .Amd64}}
@ -24,7 +26,7 @@
call ReadFile ; Readfile(handle,&su_sample_table,SAMPLE_TABLE_SIZE,&bytes_read,NULL)
add rsp, 40 ; shadow space, as required by Win64 ABI
ret
{{- else}}
{{else}}
mov edx, su_sample_table
mov ecx, su_gmdls_path1
su_gmdls_pathloop:
@ -50,9 +52,9 @@ extern _ReadFile@20 ; requires windows
db 'drivers/gm.dls',0
su_gmdls_path2:
db 'drivers/etc/gm.dls',0
{{end}}
{{.SectBss "susamtable"}}
su_sample_table:
resb 3440660 ; size of gmdls.
{{end}}

View File

@ -1,113 +1,10 @@
; source file for compiling sointu as a library
%define SU_DISABLE_PLAYER
%include "sointu/header.inc"
; use every opcode
USE_ADD
USE_ADDP
USE_POP
USE_LOADNOTE
USE_MUL
USE_MULP
USE_PUSH
USE_XCH
USE_DISTORT
USE_HOLD
USE_CRUSH
USE_GAIN
USE_INVGAIN
USE_FILTER
USE_CLIP
USE_PAN
USE_DELAY
USE_COMPRES
USE_SPEED
USE_OUT
USE_OUTAUX
USE_AUX
USE_SEND
USE_ENVELOPE
USE_NOISE
USE_OSCILLAT
USE_LOAD_VAL
USE_RECEIVE
USE_IN
; include stereo variant of each opcode
%define INCLUDE_STEREO_ADD
%define INCLUDE_STEREO_ADDP
%define INCLUDE_STEREO_POP
%define INCLUDE_STEREO_LOADNOTE
%define INCLUDE_STEREO_MUL
%define INCLUDE_STEREO_MULP
%define INCLUDE_STEREO_PUSH
%define INCLUDE_STEREO_XCH
%define INCLUDE_STEREO_DISTORT
%define INCLUDE_STEREO_HOLD
%define INCLUDE_STEREO_CRUSH
%define INCLUDE_STEREO_GAIN
%define INCLUDE_STEREO_INVGAIN
%define INCLUDE_STEREO_FILTER
%define INCLUDE_STEREO_CLIP
%define INCLUDE_STEREO_PAN
%define INCLUDE_STEREO_DELAY
%define INCLUDE_STEREO_COMPRES
%define INCLUDE_STEREO_SPEED
%define INCLUDE_STEREO_OUT
%define INCLUDE_STEREO_OUTAUX
%define INCLUDE_STEREO_AUX
%define INCLUDE_STEREO_SEND
%define INCLUDE_STEREO_ENVELOPE
%define INCLUDE_STEREO_NOISE
%define INCLUDE_STEREO_OSCILLAT
%define INCLUDE_STEREO_LOADVAL
%define INCLUDE_STEREO_RECEIVE
%define INCLUDE_STEREO_IN
; include all features inside all opcodes
%define INCLUDE_TRISAW
%define INCLUDE_SINE
%define INCLUDE_PULSE
%define INCLUDE_GATE
%define INCLUDE_UNISONS
%define INCLUDE_POLYPHONY
%define INCLUDE_MULTIVOICE_TRACKS
%define INCLUDE_DELAY_MODULATION
%define INCLUDE_LOWPASS
%define INCLUDE_BANDPASS
%define INCLUDE_HIGHPASS
%define INCLUDE_NEGBANDPASS
%define INCLUDE_NEGHIGHPASS
%define INCLUDE_GLOBAL_SEND
%define INCLUDE_DELAY_NOTETRACKING
%define INCLUDE_DELAY_FLOAT_TIME
%ifidn __OUTPUT_FORMAT__,win32
%define INCLUDE_SAMPLES
%define INCLUDE_GMDLS
%endif
%ifidn __OUTPUT_FORMAT__,win64
%define INCLUDE_SAMPLES
%define INCLUDE_GMDLS
%endif
%include "sointu/footer.inc"
section .text
struc su_sampleoff
.start resd 1
.loopstart resw 1
.looplength resw 1
.size:
endstruc
{{template "structs.asm" .}}
struc su_synth
.synthwrk resb su_synthworkspace.size
.delaywrks resb su_delayline_wrk.size * 64
.synth_wrk resb su_synthworkspace.size
.delay_wrks resb su_delayline_wrk.size * 64
.delaytimes resw 768
.sampleoffs resb su_sampleoff.size * 256
.sampleoffs resb su_sample_offset.size * 256
.randseed resd 1
.globaltime resd 1
.commands resb 32 * 64
@ -116,189 +13,122 @@ struc su_synth
.numvoices resd 1
endstruc
SECT_TEXT(sursampl)
EXPORT MANGLE_FUNC(su_render,16)
%if BITS == 32 ; stdcall
pushad ; push registers
mov ecx, [esp + 4 + 32] ; ecx = &synthState
mov edx, [esp + 8 + 32] ; edx = &buffer
mov esi, [esp + 12 + 32] ; esi = &samples
mov ebx, [esp + 16 + 32] ; ebx = &time
%else
%ifidn __OUTPUT_FORMAT__,win64 ; win64 ABI: rcx = &synth, rdx = &buffer, r8 = &bufsize, r9 = &time
push_registers rdi, rsi, rbx, rbp ; win64 ABI: these registers are non-volatile
mov rsi, r8 ; rsi = &samples
mov rbx, r9 ; rbx = &time
%else ; System V ABI: rdi = &synth, rsi = &buffer, rdx = &samples, rcx = &time
push_registers rbx, rbp ; System V ABI: these registers are non-volatile
mov rbx, rcx ; rbx points to time
xchg rsi, rdx ; rdx points to buffer, rsi points to samples
mov rcx, rdi ; rcx = &Synthstate
%endif
%endif
sub _SP,108 ; allocate space on stack for the FPU state
fsave [_SP] ; save the FPU state to stack & reset the FPU
push _SI ; push the pointer to samples
push _BX ; push the pointer to time
{{.ExportFunc "su_render" "SynthStateParam" "BufferPtrParam" "SamplesParam" "TimeParam"}}
{{- if .Amd64}}
{{- if eq .OS "windows"}}
{{- .PushRegs "rdi" "NonVolatileRDI" "rsi" "NonVolatileRSI" "rbx" "NonVolatileRBX" "rbp" "NonVolatileRBP" | indent 4}}
mov rsi, r8 ; rsi = &samples
mov rbx, r9 ; rbx = &time
{{- else}} ; SystemV amd64 ABI, linux mac or hopefully something similar
{{- .PushRegs "rbx" "NonVolatileRBX" "rbp" "NonVolatileRBP" | indent 4}}
mov rbx, rcx ; rbx points to time
xchg rsi, rdx ; rdx points to buffer, rsi points to samples
mov rcx, rdi ; rcx = &Synthstate
{{- end}}
{{- else}}
{{- .PushRegs | indent 4 }} ; push registers
mov ecx, [{{.Stack "SynthStateParam"}}] ; ecx = &synthState
mov edx, [{{.Stack "BufferPtrParam"}}] ; edx = &buffer
mov esi, [{{.Stack "SamplesParam"}}] ; esi = &samples
mov ebx, [{{.Stack "TimeParam"}}] ; ebx = &time
{{- end}}
{{.SaveFPUState | indent 4}} ; save the FPU state to stack & reset the FPU
{{.Push .SI "Samples"}}
{{.Push .BX "Time"}}
xor eax, eax ; samplenumber starts at 0
push _AX ; push samplenumber to stack
mov esi, [_SI] ; zero extend dereferenced pointer
push _SI ; push bufsize
push _DX ; push bufptr
push _CX ; this takes place of the voicetrack
lea _AX, [_CX + su_synth.sampleoffs]
push _AX
lea _AX, [_CX + su_synth.delaytimes]
push _AX
mov eax, [_CX + su_synth.randseed]
push _AX ; randseed
mov eax, [_CX + su_synth.globaltime]
push _AX ; global tick time
mov ebx, dword [_BX] ; zero extend dereferenced pointer
push _BX ; the nominal rowlength should be time_in
{{.Push .AX "BufSample"}}
mov esi, [{{.SI}}] ; zero extend dereferenced pointer
{{.Push .SI "BufSize"}}
{{.Push .DX "BufPtr"}}
{{.Push .CX "SynthState"}}
lea {{.AX}}, [{{.CX}} + su_synth.sampleoffs]
{{.Push .AX "SampleTable"}}
lea {{.AX}}, [{{.CX}} + su_synth.delaytimes]
{{.Push .AX "DelayTable"}}
mov eax, [{{.CX}} + su_synth.randseed]
{{.Push .AX "RandSeed"}}
mov eax, [{{.CX}} + su_synth.globaltime]
{{.Push .AX "GlobalTick"}}
mov ebx, dword [{{.BX}}] ; zero extend dereferenced pointer
{{.Push .BX "RowLength"}} ; the nominal rowlength should be time_in
xor eax, eax ; rowtick starts at 0
su_render_samples_loop:
push _DI
fnstsw [_SP] ; store the FPU status flag to stack top
pop _DI ; _DI = FPU status flag
and _DI, 0011100001000101b ; mask TOP pointer, stack error, zero divide and invalid operation
test _DI,_DI ; all the aforementioned bits should be 0!
push {{.DI}}
fnstsw [{{.SP}}] ; store the FPU status flag to stack top
pop {{.DI}} ; {{.DI}} = FPU status flag
and {{.DI}}, 0b0011100001000101 ; mask TOP pointer, stack error, zero divide and in{{.VAL}}id operation
test {{.DI}},{{.DI}} ; all the aforementioned bits should be 0!
jne su_render_samples_time_finish ; otherwise, we exit due to error
cmp eax, [_SP] ; if rowtick >= maxtime
cmp eax, [{{.Stack "RowLength"}}] ; if rowtick >= maxtime
jge su_render_samples_time_finish ; goto finish
mov ecx, [_SP + PTRSIZE*7] ; ecx = buffer length in samples
cmp [_SP + PTRSIZE*8], ecx ; if samples >= maxsamples
mov ecx, [{{.Stack "BufSize"}}] ; ecx = buffer length in samples
cmp [{{.Stack "BufSample"}}], ecx ; if samples >= maxsamples
jge su_render_samples_time_finish ; goto finish
inc eax ; time++
inc dword [_SP + PTRSIZE*8] ; samples++
mov _CX, [_SP + PTRSIZE*5]
push _AX ; push rowtick
mov eax, [_CX + su_synth.polyphony]
push _AX ;polyphony
mov eax, [_CX + su_synth.numvoices]
push _AX ;numvoices
lea _DX, [_CX+ su_synth.synthwrk]
lea COM, [_CX+ su_synth.commands]
lea VAL, [_CX+ su_synth.values]
lea WRK, [_DX + su_synthworkspace.voices]
lea _CX, [_CX+ su_synth.delaywrks - su_delayline_wrk.filtstate]
call MANGLE_FUNC(su_run_vm,0)
pop _AX
pop _AX
mov _DI, [_SP + PTRSIZE*7] ; edi containts buffer ptr
mov _CX, [_SP + PTRSIZE*6]
lea _SI, [_CX + su_synth.synthwrk + su_synthworkspace.left]
inc dword [{{.Stack "BufSample"}}] ; samples++
mov {{.CX}}, [{{.Stack "SynthState"}}]
{{.Push .AX "Sample"}}
mov eax, [{{.CX}} + su_synth.polyphony]
{{.Push .AX "PolyphonyBitmask"}}
mov eax, [{{.CX}} + su_synth.numvoices]
{{.Push .AX "VoicesRemain"}}
lea {{.DX}}, [{{.CX}}+ su_synth.synth_wrk]
lea {{.COM}}, [{{.CX}}+ su_synth.commands]
lea {{.VAL}}, [{{.CX}}+ su_synth.values]
lea {{.WRK}}, [{{.DX}} + su_synthworkspace.voices]
lea {{.CX}}, [{{.CX}}+ su_synth.delay_wrks - su_delayline_wrk.filtstate]
{{.Call "su_run_vm"}}
{{.Pop .AX}}
{{.Pop .AX}}
mov {{.DI}}, [{{.Stack "BufPtr"}}] ; edi containts buffer ptr
mov {{.CX}}, [{{.Stack "SynthState"}}]
lea {{.SI}}, [{{.CX}} + su_synth.synth_wrk + su_synthworkspace.left]
movsd ; copy left channel to output buffer
movsd ; copy right channel to output buffer
mov [_SP + PTRSIZE*7], _DI ; save back the updated ptr
lea _DI, [_SI-8]
mov [{{.Stack "BufPtr"}}], {{.DI}} ; save back the updated ptr
lea {{.DI}}, [{{.SI}}-8]
xor eax, eax
stosd ; clear left channel so the VM is ready to write them again
stosd ; clear right channel so the VM is ready to write them again
pop _AX
inc dword [_SP + PTRSIZE] ; increment global time, used by delays
{{.Pop .AX}}
inc dword [{{.Stack "GlobalTick"}}] ; increment global time, used by delays
jmp su_render_samples_loop
su_render_samples_time_finish:
pop _CX
pop _BX
pop _DX
pop _CX ; discard delaytimes ptr
pop _CX ; discard samplesoffs ptr
pop _CX
mov [_CX + su_synth.randseed], edx
mov [_CX + su_synth.globaltime], ebx
pop _BX
pop _BX
pop _DX
pop _BX ; pop the pointer to time
pop _SI ; pop the pointer to samples
mov dword [_SI], edx ; *samples = samples rendered
mov dword [_BX], eax ; *time = time ticks rendered
mov _AX,_DI ; _DI was the masked FPU status flag, _AX is return value
frstor [_SP] ; restore fpu state
add _SP,108 ; rewind the stack allocate for FPU state
%if BITS == 32 ; stdcall
mov [_SP + 28],eax ; we want to return eax, but popad pops everything, so put eax to stack for popad to pop
popad
ret 16
%else
%ifidn __OUTPUT_FORMAT__,win64
pop_registers rdi, rsi, rbx, rbp ; win64 ABI: these registers are non-volatile
%else
pop_registers rbx, rbp ; System V ABI: these registers are non-volatile
%endif
{{.Pop .CX}}
{{.Pop .BX}}
{{.Pop .DX}}
{{.Pop .CX}}
{{.Pop .CX}}
{{.Pop .CX}}
mov [{{.CX}} + su_synth.randseed], edx
mov [{{.CX}} + su_synth.globaltime], ebx
{{.Pop .BX}}
{{.Pop .BX}}
{{.Pop .DX}}
{{.Pop .BX}}
{{.Pop .SI}}
mov dword [{{.SI}}], edx ; *samples = samples rendered
mov dword [{{.BX}}], eax ; *time = time ticks rendered
mov {{.AX}},{{.DI}} ; {{.DI}} was the masked FPU status flag, {{.AX}} is return {{.VAL}}ue
{{.LoadFPUState | indent 4}} ; load the FPU state from stack
{{- if .Amd64}}
{{- if eq .OS "windows"}}
{{- .PopRegs "rdi" "rsi" "rbx" "rbp" | indent 4}}
{{- else}} ; SystemV amd64 ABI, linux mac or hopefully something similar
{{- .PopRegs "rbx" "rbp" | indent 4}}
{{- end}}
ret
%endif
{{- else}}
mov [{{.Stack "eax"}}],eax ; we want to return eax, but popad pops everything, so put eax to stack for popad to pop
{{- .PopRegs | indent 4 }} ; popad
ret 16
{{- end}}
SECT_DATA(opcodeid)
; Arithmetic opcode ids
EXPORT MANGLE_DATA(su_add_id)
dd ADD_ID
EXPORT MANGLE_DATA(su_addp_id)
dd ADDP_ID
EXPORT MANGLE_DATA(su_pop_id)
dd POP_ID
EXPORT MANGLE_DATA(su_loadnote_id)
dd LOADNOTE_ID
EXPORT MANGLE_DATA(su_mul_id)
dd MUL_ID
EXPORT MANGLE_DATA(su_mulp_id)
dd MULP_ID
EXPORT MANGLE_DATA(su_push_id)
dd PUSH_ID
EXPORT MANGLE_DATA(su_xch_id)
dd XCH_ID
{{template "patch.asm" .}}
; Effect opcode ids
EXPORT MANGLE_DATA(su_distort_id)
dd DISTORT_ID
EXPORT MANGLE_DATA(su_hold_id)
dd HOLD_ID
EXPORT MANGLE_DATA(su_crush_id)
dd CRUSH_ID
EXPORT MANGLE_DATA(su_gain_id)
dd GAIN_ID
EXPORT MANGLE_DATA(su_invgain_id)
dd INVGAIN_ID
EXPORT MANGLE_DATA(su_filter_id)
dd FILTER_ID
EXPORT MANGLE_DATA(su_clip_id)
dd CLIP_ID
EXPORT MANGLE_DATA(su_pan_id)
dd PAN_ID
EXPORT MANGLE_DATA(su_delay_id)
dd DELAY_ID
EXPORT MANGLE_DATA(su_compres_id)
dd COMPRES_ID
; Flowcontrol opcode ids
EXPORT MANGLE_DATA(su_advance_id)
dd SU_ADVANCE_ID
EXPORT MANGLE_DATA(su_speed_id)
dd SPEED_ID
; Sink opcode ids
EXPORT MANGLE_DATA(su_out_id)
dd OUT_ID
EXPORT MANGLE_DATA(su_outaux_id)
dd OUTAUX_ID
EXPORT MANGLE_DATA(su_aux_id)
dd AUX_ID
EXPORT MANGLE_DATA(su_send_id)
dd SEND_ID
; Source opcode ids
EXPORT MANGLE_DATA(su_envelope_id)
dd ENVELOPE_ID
EXPORT MANGLE_DATA(su_noise_id)
dd NOISE_ID
EXPORT MANGLE_DATA(su_oscillat_id)
dd OSCILLAT_ID
EXPORT MANGLE_DATA(su_loadval_id)
dd LOADVAL_ID
EXPORT MANGLE_DATA(su_receive_id)
dd RECEIVE_ID
EXPORT MANGLE_DATA(su_in_id)
dd IN_ID
;-------------------------------------------------------------------------------
; Constants
;-------------------------------------------------------------------------------
{{.SectData "constants"}}
{{.Constants}}

View File

@ -38,11 +38,11 @@ typedef struct SampleOffset {
typedef struct Synth {
struct SynthWorkspace SynthWrk;
struct DelayWorkspace DelayWrks[64]; // let's keep this as 64 for now, so the delays take 16 meg. If that's too little or too much, we can change this in future.
struct DelayWorkspace DelayWrks[64]; // let's keep this as 64 for now, so the delays take 16 meg. If that's too little or too much, we can change this in future.
unsigned short DelayTimes[768];
struct SampleOffset SampleOffsets[256];
unsigned int RandSeed;
unsigned int GlobalTick;
unsigned int GlobalTick;
unsigned char Commands[32 * 64];
unsigned char Values[32 * 64 * 8];
unsigned int Polyphony;
@ -69,7 +69,7 @@ void CALLCONV su_load_gmdls(void);
// actually rendered and how many time ticks were actually advanced.
//
// Parameters:
// synth pointer to the synthesizer used. RandSeed should be > 0 e.g. 1
// synth pointer to the synthesizer used. RandSeed should be > 0 e.g. 1
// buffer audio sample buffer, L R L R ...
// samples pointer to the maximum number of samples to be rendered.
// buffer should have a length of 2 * maxsamples as the audio
@ -92,44 +92,9 @@ void CALLCONV su_load_gmdls(void);
// bits 11-13 The top pointer of the fpu stack. Any other value than 0 indicates that some values were left on the stack.
int CALLCONV su_render(Synth* synth, float* buffer, int* samples, int* time);
// Arithmetic opcode ids
extern const int su_add_id;
extern const int su_addp_id;
extern const int su_pop_id;
extern const int su_loadnote_id;
extern const int su_mul_id;
extern const int su_mulp_id;
extern const int su_push_id;
extern const int su_xch_id;
// Effect opcode ids
extern const int su_distort_id;
extern const int su_hold_id;
extern const int su_crush_id;
extern const int su_gain_id;
extern const int su_invgain_id;
extern const int su_filter_id;
extern const int su_clip_id;
extern const int su_pan_id;
extern const int su_delay_id;
extern const int su_compres_id;
// Flowcontrol opcode ids
extern const int su_advance_id;
extern const int su_speed_id;
// Sink opcode ids
extern const int su_out_id;
extern const int su_outaux_id;
extern const int su_aux_id;
extern const int su_send_id;
// Source opcode ids
extern const int su_envelope_id;
extern const int su_noise_id;
extern const int su_oscillat_id;
extern const int su_loadval_id;
extern const int su_receive_id;
extern const int su_in_id;
#define SU_ADVANCE_ID 0
{{- range $index, $element := .Instructions}}
#define {{printf "su_%v_id" $element | upper | printf "%-20v"}}{{add1 $index | mul 2}}
{{- end}}
#endif // _SOINTU_H

View File

@ -25,8 +25,8 @@ su_run_vm_loop: ; loop until all voices done
xor ecx, ecx ; counter = 0
xor eax, eax ; clear out high bits of eax, as lodsb only sets al
su_transform_values_loop:
{{- .Prepare "su_vm_transformcounts" | indent 4}}
cmp cl, byte [{{.Use "su_vm_transformcounts"}}+{{.DI}}] ; compare the counter to the value in the param count table
{{- .Prepare "su_vm_transformcounts-1" | indent 4}}
cmp cl, byte [{{.Use "su_vm_transformcounts-1"}}+{{.DI}}] ; compare the counter to the value in the param count table
je su_transform_values_out
lodsb ; load the byte value from VAL stream
push {{.AX}} ; push it to memory so FPU can read it
@ -43,11 +43,12 @@ su_transform_values_loop:
su_transform_values_out:
bt dword [{{.COM}}-1],0 ; LSB of COM = stereo bit => carry
{{- .SaveStack "Opcode"}}
{{- .Prepare "su_vm_jumptable" | indent 4}}
call [{{.Use "su_vm_jumptable"}}+{{.DI}}*{{.PTRSIZE}}] ; call the function corresponding to the instruction
{{- $x := printf "su_vm_jumptable-%v" .PTRSIZE}}
{{- .Prepare $x | indent 4}}
call [{{.Use $x}}+{{.DI}}*{{.PTRSIZE}}] ; call the function corresponding to the instruction
jmp su_run_vm_loop
su_run_vm_advance:
{{- if .Polyphony}}
{{- if .SupportsPolyphony}}
mov {{.WRK}}, [{{.Stack "Voice"}}] ; WRK points to start of current voice
add {{.WRK}}, su_voice.size ; move to next voice
mov [{{.Stack "Voice"}}], {{.WRK}} ; update the pointer in the stack to point to the new voice
@ -173,18 +174,15 @@ su_clip_do:
; The opcode table jump table. This is constructed to only include the opcodes
; that are used so that the jump table is as small as possible.
;-------------------------------------------------------------------------------
{{.Data "su_vm_jumptable_offset"}}
su_vm_jumptable equ $ - {{.PTRSIZE}} ; Advance is not in the opcode table
{{- $x := .}}
{{- range .Opcodes}}
{{$x.DPTR}} su_op_{{.Type}}
{{.Data "su_vm_jumptable"}}
{{- range .Instructions}}
{{$.DPTR}} su_op_{{.}}
{{- end}}
;-------------------------------------------------------------------------------
; The number of transformed parameters each opcode takes
;-------------------------------------------------------------------------------
{{.Data "su_vm_transformcounts_offset"}}
su_vm_transformcounts equ $ - 1 ; Advance is not in the opcode table
{{- range .Opcodes}}
db {{.NumParams}}
{{.Data "su_vm_transformcounts"}}
{{- range .Instructions}}
db {{$.TransformCount .}}
{{- end}}

View File

@ -26,7 +26,7 @@ su_synth_obj:
{{- end}}
{{- $prologsize := len .Stacklocs}}
xor eax, eax
{{- if .MultivoiceTracks}}
{{- if ne .VoiceTrackBitmask 0}}
{{.Push (.VoiceTrackBitmask | printf "%v") "VoiceTrackBitmask"}}
{{- end}}
{{.Push "1" "RandSeed"}}
@ -37,20 +37,20 @@ su_render_rowloop: ; loop through every row in the song
xor eax, eax ; ecx is the current sample within row
su_render_sampleloop: ; loop through every sample in the row
{{.Push .AX "Sample"}}
{{- if .Polyphony}}
{{- if .SupportsPolyphony}}
{{.Push (.PolyphonyBitmask | printf "%v") "PolyphonyBitmask"}} ; does the next voice reuse the current opcodes?
{{- end}}
{{.Push (.Song.Patch.TotalVoices | printf "%v") "VoicesRemain"}}
mov {{.DX}}, {{.PTRWORD}} su_synth_obj ; {{.DX}} points to the synth object
mov {{.COM}}, {{.PTRWORD}} su_patch_code ; COM points to vm code
mov {{.VAL}}, {{.PTRWORD}} su_patch_parameters ; VAL points to unit params
{{- if .Opcode "delay"}}
{{- if .HasOp "delay"}}
lea {{.CX}}, [{{.DX}} + su_synthworkspace.size - su_delayline_wrk.filtstate]
{{- end}}
lea {{.WRK}}, [{{.DX}} + su_synthworkspace.voices] ; WRK points to the first voice
{{.Call "su_run_vm"}} ; run through the VM code
{{.Pop .AX}}
{{- if .Polyphony}}
{{- if .SupportsPolyphony}}
{{.Pop .AX}}
{{- end}}
{{- template "output_sound.asm" .}} ; *ptr++ = left, *ptr++ = right
@ -64,9 +64,8 @@ su_render_sampleloop: ; loop through every sample in the row
cmp eax, {{.Song.TotalRows}}
jl su_render_rowloop
; rewind the stack the entropy of multiple pop {{.AX}} is probably lower than add
{{- $x := .}}
{{- range (.Sub (len .Stacklocs) $prologsize | .Count)}}
{{$x.Pop $x.AX}}
{{- range slice .Stacklocs $prologsize}}
{{$.Pop $.AX}}
{{- end}}
{{- if .Amd64}}
{{- if eq .OS "windows"}}
@ -89,7 +88,7 @@ su_render_sampleloop: ; loop through every sample in the row
; Dirty: pretty much everything
;-------------------------------------------------------------------------------
{{.Func "su_update_voices"}}
{{- if .MultivoiceTracks}}
{{- if ne .VoiceTrackBitmask 0}}
; The more complicated implementation: one track can trigger multiple voices
xor edx, edx
mov ebx, {{.Song.PatternRows}} ; we could do xor ebx,ebx; mov bl,PATTERN_SIZE, but that would limit patternsize to 256...
@ -198,31 +197,31 @@ su_update_voices_skipadd:
db {{.Sequence | toStrings | join ","}}
{{- end}}
{{- if gt (.Song.Patch.SampleOffsets | len) 0}}
{{- if gt (.SampleOffsets | len) 0}}
;-------------------------------------------------------------------------------
; Sample offsets
;-------------------------------------------------------------------------------
{{.Data "su_sample_offsets"}}
{{- range .Song.Patch.SampleOffsets}}
{{- range .SampleOffsets}}
dd {{.Start}}
dw {{.LoopStart}}
dw {{.LoopLength}}
{{- end}}
{{end}}
{{- if gt (.Song.Patch.DelayTimes | len ) 0}}
{{- if gt (.DelayTimes | len ) 0}}
;-------------------------------------------------------------------------------
; Delay times
;-------------------------------------------------------------------------------
{{.Data "su_delay_times"}}
dw {{.Song.Patch.DelayTimes | toStrings | join ","}}
dw {{.DelayTimes | toStrings | join ","}}
{{end}}
;-------------------------------------------------------------------------------
; The code for this patch, basically indices to vm jump table
;-------------------------------------------------------------------------------
{{.Data "su_patch_code"}}
db {{.Code | toStrings | join ","}}
db {{.Commands | toStrings | join ","}}
;-------------------------------------------------------------------------------
; The parameters / inputs to each opcode

50
templates/player.h Normal file
View File

@ -0,0 +1,50 @@
// auto-generated by Sointu, editing not recommended
#ifndef SU_RENDER_H
#define SU_RENDER_H
#define SU_MAX_SAMPLES {{.MaxSamples}}
#define SU_BUFFER_LENGTH (SU_MAX_SAMPLES*2)
#define SU_SAMPLE_RATE 44100
#define SU_BPM {{.Song.BPM}}
#define SU_PATTERN_SIZE {{.Song.PatternRows}}
#define SU_MAX_PATTERNS {{.Song.SequenceLength}}
#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
{{- if .Output16Bit}}
typedef short SUsample;
#define SU_SAMPLE_RANGE 32767.0
{{- else}}
typedef float SUsample;
#define SU_SAMPLE_RANGE 1.0
{{- end}}
#ifdef __cplusplus
extern "C" {
#endif
void SU_CALLCONV su_render_song(SUsample *buffer);
{{- if gt (.SampleOffsets | len) 0}}
void SU_CALLCONV su_load_gmdls();
#define SU_LOAD_GMDLS
{{- end}}
#ifdef __cplusplus
}
#endif
#endif

View File

@ -1,4 +1,4 @@
{{- if .Opcode "out"}}
{{- if .HasOp "out"}}
;-------------------------------------------------------------------------------
; OUT opcode: outputs and pops the signal
;-------------------------------------------------------------------------------
@ -26,7 +26,7 @@ su_op_out_mono:
{{end}}
{{- if .Opcode "outaux"}}
{{- if .HasOp "outaux"}}
;-------------------------------------------------------------------------------
; OUTAUX opcode: outputs to main and aux1 outputs and pops the signal
;-------------------------------------------------------------------------------
@ -54,7 +54,7 @@ su_op_outaux_mono:
{{end}}
{{- if .Opcode "aux"}}
{{- if .HasOp "aux"}}
;-------------------------------------------------------------------------------
; AUX opcode: outputs the signal to aux (or main) port and pops the signal
;-------------------------------------------------------------------------------
@ -79,7 +79,7 @@ su_op_aux_mono:
{{end}}
{{- if .Opcode "send"}}
{{- if .HasOp "send"}}
;-------------------------------------------------------------------------------
; SEND opcode: adds the signal to a port
;-------------------------------------------------------------------------------
@ -103,7 +103,7 @@ su_op_aux_mono:
fxch ; swap them back: l r
su_op_send_mono:
{{- end}}
{{- if .HasParamValueOtherThan "send" "voice" 0}}
{{- if .SupportsParamValueOtherThan "send" "voice" 0}}
test {{.AX}}, 0x8000
jz su_op_send_skipglobal
mov {{.CX}}, [{{.Stack "Synth"}}]

View File

@ -1,4 +1,4 @@
{{if .Opcode "envelope" -}}
{{if .HasOp "envelope" -}}
;-------------------------------------------------------------------------------
; ENVELOPE opcode: pushes an ADSR envelope value on stack [0,1]
;-------------------------------------------------------------------------------
@ -62,7 +62,7 @@ su_op_envelope_leave2:
{{end}}
{{- if .Opcode "noise"}}
{{- if .HasOp "noise"}}
;-------------------------------------------------------------------------------
; NOISE opcode: creates noise
;-------------------------------------------------------------------------------
@ -91,7 +91,7 @@ su_op_noise_mono:
{{end}}
{{- if .Opcode "oscillator"}}
{{- if .HasOp "oscillator"}}
;-------------------------------------------------------------------------------
; OSCILLAT opcode: oscillator, the heart of the synth
;-------------------------------------------------------------------------------
@ -100,11 +100,10 @@ su_op_noise_mono:
;-------------------------------------------------------------------------------
{{.Func "su_op_oscillator" "Opcode"}}
lodsb ; load the flags
%ifdef RUNTIME_TABLES
%ifdef INCLUDE_SAMPLES
mov {{.DI}}, [{{.SP}} + su_stack.sampleoffs]; we need to put this in a register, as the stereo & unisons screw the stack positions
%endif ; ain't we lucky that {{.DI}} was unused throughout
%endif
{{- if .Library}}
mov {{.DI}}, [{{.Stack "SampleTable"}}]; we need to put this in a register, as the stereo & unisons screw the stack positions
; ain't we lucky that {{.DI}} was unused throughout
{{- end}}
fld dword [{{.Input "oscillator" "detune"}}] ; e, where e is the detune [0,1]
{{- .Prepare (.Float 0.5)}}
fsub dword [{{.Use (.Float 0.5)}}] ; e-.5
@ -120,7 +119,7 @@ su_op_noise_mono:
fchs ; -d r, negate the detune for second round
su_op_oscillat_mono:
{{- end}}
{{- if .HasParamValueOtherThan "oscillator" "unison" 0}}
{{- if .SupportsParamValueOtherThan "oscillator" "unison" 0}}
{{.PushRegs .AX "" .WRK "OscWRK" .AX "OscFlags"}}
fldz ; 0 d
fxch ; d a=0, "accumulated signal"
@ -171,7 +170,7 @@ su_op_oscillat_normalized:
fadd dword [{{.WRK}}]
fst dword [{{.WRK}}]
fadd dword [{{.Input "oscillator" "phase"}}]
{{- if .HasParamValue "oscillator" "type" .Sample}}
{{- if .SupportsParamValue "oscillator" "type" .Sample}}
test al, byte 0x80
jz short su_op_oscillat_not_sample
{{.Call "su_oscillat_sample"}}
@ -185,25 +184,25 @@ su_op_oscillat_not_sample:
fstp st1
fld dword [{{.Input "oscillator" "color"}}] ; // c p
; every oscillator test included if needed
{{- if .HasParamValue "oscillator" "type" .Sine}}
{{- if .SupportsParamValue "oscillator" "type" .Sine}}
test al, byte 0x40
jz short su_op_oscillat_notsine
{{.Call "su_oscillat_sine"}}
su_op_oscillat_notsine:
{{- end}}
{{- if .HasParamValue "oscillator" "type" .Trisaw}}
{{- if .SupportsParamValue "oscillator" "type" .Trisaw}}
test al, byte 0x20
jz short su_op_oscillat_not_trisaw
{{.Call "su_oscillat_trisaw"}}
su_op_oscillat_not_trisaw:
{{- end}}
{{- if .HasParamValue "oscillator" "type" .Pulse}}
{{- if .SupportsParamValue "oscillator" "type" .Pulse}}
test al, byte 0x10
jz short su_op_oscillat_not_pulse
{{.Call "su_oscillat_pulse"}}
su_op_oscillat_not_pulse:
{{- end}}
{{- if .HasParamValue "oscillator" "type" .Gate}}
{{- if .SupportsParamValue "oscillator" "type" .Gate}}
test al, byte 0x04
jz short su_op_oscillat_not_gate
{{.Call "su_oscillat_gate"}}
@ -300,12 +299,12 @@ go4kVCO_gate_bit: ; stack: 0/1, let's call it x
{{- .PushRegs .AX "SampleAx" .DX "SampleDx" .CX "SampleCx" .BX "SampleBx" | indent 4}} ; edx must be saved, eax & ecx if this is stereo osc
push {{.AX}}
mov al, byte [{{.VAL}}-4] ; reuse "color" as the sample number
%ifdef RUNTIME_TABLES ; when using RUNTIME_TABLES, assumed the sample_offset ptr is in {{.DI}}
do{lea {{.DI}}, [}, {{.DI}}, {{.AX}}*8,] ; edi points now to the sample table entry
%else
{{- if .Library}}
lea {{.DI}}, [{{.DI}} + {{.AX}}*8] ; edi points now to the sample table entry
{{- else}}
{{- .Prepare "su_sample_offsets" | indent 4}}
lea {{.DI}}, [{{.Use "su_sample_offsets"}} + {{.AX}}*8]; edi points now to the sample table entry
%endif
{{- end}}
{{- .Float 84.28074964676522 | .Prepare | indent 4}}
fmul dword [{{.Float 84.28074964676522 | .Use}}] ; p*r
fistp dword [{{.SP}}]
@ -329,7 +328,7 @@ su_oscillat_sample_not_looping:
{{end}}
{{- if .Opcode "loadval"}}
{{- if .HasOp "loadval"}}
;-------------------------------------------------------------------------------
; LOADVAL opcode
;-------------------------------------------------------------------------------
@ -356,7 +355,7 @@ su_op_loadval_mono:
{{end}}
{{- if .Opcode "receive"}}
{{- if .HasOp "receive"}}
;-------------------------------------------------------------------------------
; RECEIVE opcode
;-------------------------------------------------------------------------------
@ -387,7 +386,7 @@ su_op_receive_mono:
{{end}}
{{- if .Opcode "in"}}
{{- if .HasOp "in"}}
;-------------------------------------------------------------------------------
; IN opcode: inputs and clears a global port
;-------------------------------------------------------------------------------

View File

@ -41,3 +41,13 @@ struc su_delayline_wrk
.buffer resd 65536
.size:
endstruc
;-------------------------------------------------------------------------------
; su_sample_offset struct
;-------------------------------------------------------------------------------
struc su_sample_offset ; length conveniently 8 bytes, so easy to index
.start resd 1
.loopstart resw 1
.looplength resw 1
.size:
endstruc

View File

@ -5,17 +5,9 @@ function(regression_test testname)
set(asmfile ${testname}.asm)
set (headerfile ${CMAKE_CURRENT_BINARY_DIR}/${testname}.h)
if(DEFINED CMAKE_C_SIZEOF_DATA_PTR AND CMAKE_C_SIZEOF_DATA_PTR EQUAL 8)
set(arch "-arch=amd64")
elseif(DEFINED CMAKE_CXX_SIZEOF_DATA_PTR AND CMAKE_CXX_SIZEOF_DATA_PTR EQUAL 8)
set(arch "-arch=amd64")
else()
set(arch "-arch=386")
endif()
add_custom_command(
OUTPUT ${asmfile}
COMMAND ${sointuexe} -a -c -w ${arch} -d ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/${source}
COMMAND ${sointuexe} -a -c -w -arch=${arch} -d ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/${source}
DEPENDS ${source} ${sointuexe}
)
@ -56,25 +48,13 @@ function(regression_test testname)
endif()
endfunction(regression_test)
if(WIN32)
set(sointuexe ${CMAKE_CURRENT_BINARY_DIR}/sointu-cli.exe)
else()
set(sointuexe ${CMAKE_CURRENT_BINARY_DIR}/sointu-cli)
endif()
# the tests include the entire ASM but we still want to rebuild when they change
file(GLOB templates ${PROJECT_SOURCE_DIR}/templates/*.asm)
file(GLOB go4k "${PROJECT_SOURCE_DIR}/go4k/*.go" "${PROJECT_SOURCE_DIR}/go4k/cmd/sointu-cli/*.go")
message("templates=${templates}")
message("go4k=${go4k}")
# Build sointu-cli only once because go run has everytime quite a bit of delay when
# starting
add_custom_command(
OUTPUT ${sointuexe}
COMMAND go build -o ${sointuexe} ${PROJECT_SOURCE_DIR}/go4k/cmd/sointu-cli/main.go
DEPENDS "${templates}" "${go4k}"
DEPENDS "${templates}" "${go4k}" "${compilersrc}" ${STATICLIB}
)
regression_test(test_envelope "" ENVELOPE)

View File

@ -17,5 +17,3 @@ patch:
parameters: {stereo: 0}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -27,5 +27,3 @@ patch:
parameters: {stereo: 0}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -23,5 +23,3 @@ patch:
parameters: {stereo: 0}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -17,5 +17,3 @@ patch:
parameters: {stereo: 0, value: 96}
- type: aux
parameters: {channel: 0, gain: 128, stereo: 0}
delaytimes: []
sampleoffsets: []

View File

@ -29,5 +29,3 @@ patch:
parameters: {stereo: 1}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -25,5 +25,3 @@ patch:
parameters: {stereo: 1}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -27,5 +27,3 @@ patch:
parameters: {gain: 64, stereo: 1}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -27,5 +27,3 @@ patch:
parameters: {gain: 64, stereo: 1}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -51,5 +51,3 @@ patch:
parameters: {stereo: 0}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -51,5 +51,3 @@ patch:
parameters: {stereo: 1}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -27,5 +27,3 @@ patch:
parameters: {resolution: 64, stereo: 0}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -25,5 +25,3 @@ patch:
parameters: {resolution: 32, stereo: 1}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -16,10 +16,9 @@ patch:
- type: mulp
parameters: {stereo: 0}
- type: delay
parameters: {count: 1, damp: 64, delay: 0, dry: 128, feedback: 125, notetracking: 0, pregain: 40, stereo: 0}
parameters: {damp: 64, dry: 128, feedback: 125, notetracking: 0, pregain: 40, stereo: 0}
varargs: [11025]
- type: pan
parameters: {panning: 64, stereo: 0}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: [11025]
sampleoffsets: []

View File

@ -16,7 +16,8 @@ patch:
- type: mulp
parameters: {stereo: 0}
- type: delay
parameters: {count: 1, damp: 64, delay: 0, dry: 128, feedback: 125, notetracking: 0, pregain: 40, stereo: 0}
parameters: {damp: 64, dry: 128, feedback: 125, notetracking: 0, pregain: 40, stereo: 0}
varargs: [11025]
- type: pan
parameters: {panning: 64, stereo: 0}
- type: out
@ -25,5 +26,3 @@ patch:
parameters: {color: 128, detune: 64, gain: 128, lfo: 1, phase: 64, shape: 64, stereo: 0, transpose: 70, type: 0, unison: 0}
- type: send
parameters: {amount: 32, port: 3, sendpop: 1, stereo: 0, unit: 3, voice: 0}
delaytimes: [11025]
sampleoffsets: []

View File

@ -16,7 +16,8 @@ patch:
- type: mulp
parameters: {stereo: 0}
- type: delay
parameters: {count: 1, damp: 64, delay: 0, dry: 128, feedback: 125, notetracking: 0, pregain: 40, stereo: 0}
parameters: {damp: 64, dry: 128, feedback: 125, notetracking: 0, pregain: 40, stereo: 0}
varargs: [11025]
- type: pan
parameters: {panning: 64, stereo: 0}
- type: out
@ -25,5 +26,3 @@ patch:
parameters: {color: 128, detune: 64, gain: 128, lfo: 1, phase: 64, shape: 64, stereo: 0, transpose: 70, type: 0, unison: 0}
- type: send
parameters: {amount: 32, port: 1, sendpop: 1, stereo: 0, unit: 3, voice: 0}
delaytimes: [11025]
sampleoffsets: []

View File

@ -16,7 +16,8 @@ patch:
- type: mulp
parameters: {stereo: 0}
- type: delay
parameters: {count: 1, damp: 64, delay: 0, dry: 128, feedback: 125, notetracking: 0, pregain: 40, stereo: 0}
parameters: {damp: 64, dry: 128, feedback: 125, notetracking: 0, pregain: 40, stereo: 0}
varargs: [11025]
- type: pan
parameters: {panning: 64, stereo: 0}
- type: out
@ -25,5 +26,3 @@ patch:
parameters: {color: 128, detune: 64, gain: 128, lfo: 1, phase: 64, shape: 64, stereo: 0, transpose: 70, type: 0, unison: 0}
- type: send
parameters: {amount: 32, port: 2, sendpop: 1, stereo: 0, unit: 3, voice: 0}
delaytimes: [11025]
sampleoffsets: []

View File

@ -16,7 +16,8 @@ patch:
- type: mulp
parameters: {stereo: 0}
- type: delay
parameters: {count: 1, damp: 64, delay: 0, dry: 128, feedback: 0, notetracking: 0, pregain: 40, stereo: 0}
parameters: {damp: 64, dry: 128, feedback: 0, notetracking: 0, pregain: 40, stereo: 0}
varargs: [1000]
- type: pan
parameters: {panning: 64, stereo: 0}
- type: out
@ -25,5 +26,3 @@ patch:
parameters: {color: 128, detune: 64, gain: 128, lfo: 1, phase: 64, shape: 64, stereo: 0, transpose: 50, type: 0, unison: 0}
- type: send
parameters: {amount: 65, port: 4, sendpop: 1, stereo: 0, unit: 3, voice: 0}
delaytimes: [1000]
sampleoffsets: []

View File

@ -20,7 +20,8 @@ patch:
- type: filter
parameters: {bandpass: 1, frequency: 32, highpass: 1, lowpass: 1, negbandpass: 0, neghighpass: 0, resonance: 128, stereo: 0}
- type: delay
parameters: {count: 1, damp: 16, delay: 0, dry: 128, feedback: 128, notetracking: 1, pregain: 128, stereo: 0}
parameters: {damp: 16, dry: 128, feedback: 128, notetracking: 1, pregain: 128, stereo: 0}
varargs: [10787]
- type: filter
parameters: {bandpass: 1, frequency: 24, highpass: 1, lowpass: 1, negbandpass: 0, neghighpass: 0, resonance: 128, stereo: 0}
- type: mulp
@ -29,5 +30,3 @@ patch:
parameters: {panning: 64, stereo: 0}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: [10787]
sampleoffsets: []

View File

@ -16,7 +16,8 @@ patch:
- type: mulp
parameters: {stereo: 0}
- type: delay
parameters: {count: 1, damp: 64, delay: 0, dry: 128, feedback: 125, notetracking: 0, pregain: 40, stereo: 0}
parameters: {damp: 64, dry: 128, feedback: 125, notetracking: 0, pregain: 40, stereo: 0}
varargs: [11025]
- type: pan
parameters: {panning: 64, stereo: 0}
- type: out
@ -25,5 +26,3 @@ patch:
parameters: {color: 128, detune: 64, gain: 128, lfo: 1, phase: 64, shape: 64, stereo: 0, transpose: 70, type: 0, unison: 0}
- type: send
parameters: {amount: 32, port: 0, sendpop: 1, stereo: 0, unit: 3, voice: 0}
delaytimes: [11025]
sampleoffsets: []

View File

@ -16,10 +16,9 @@ patch:
- type: mulp
parameters: {stereo: 0}
- type: delay
parameters: {count: 8, damp: 64, delay: 0, dry: 128, feedback: 125, notetracking: 0, pregain: 40, stereo: 0}
parameters: {damp: 64, dry: 128, feedback: 125, notetracking: 0, pregain: 40, stereo: 0}
varargs: [1116, 1188, 1276, 1356, 1422, 1492, 1556, 1618]
- type: pan
parameters: {panning: 64, stereo: 0}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: [1116, 1188, 1276, 1356, 1422, 1492, 1556, 1618]
sampleoffsets: []

View File

@ -18,8 +18,7 @@ patch:
- type: pan
parameters: {panning: 64, stereo: 0}
- type: delay
parameters: {count: 1, damp: 64, delay: 0, dry: 128, feedback: 125, notetracking: 0, pregain: 40, stereo: 1}
parameters: {damp: 64, dry: 128, feedback: 125, notetracking: 0, pregain: 40, stereo: 1}
varargs: [11025, 21025]
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: [11025, 21025]
sampleoffsets: []

View File

@ -19,5 +19,3 @@ patch:
parameters: {drive: 96, stereo: 0}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -25,5 +25,3 @@ patch:
parameters: {amount: 68, port: 0, sendpop: 1, stereo: 0, unit: 3, voice: 0}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -15,5 +15,3 @@ patch:
parameters: {drive: 96, stereo: 1}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -15,5 +15,3 @@ patch:
parameters: {attack: 95, decay: 64, gain: 128, release: 80, stereo: 0, sustain: 64}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -15,5 +15,3 @@ patch:
parameters: {attack: 95, decay: 64, gain: 128, release: 80, stereo: 0, sustain: 64}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -25,5 +25,3 @@ patch:
parameters: {amount: 68, port: 4, sendpop: 1, stereo: 0, unit: 1, voice: 0}
- type: out
parameters: {gain: 110, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -13,5 +13,3 @@ patch:
parameters: {attack: 64, decay: 64, gain: 128, release: 80, stereo: 1, sustain: 64}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -21,5 +21,3 @@ patch:
parameters: {panning: 64, stereo: 0}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -25,5 +25,3 @@ patch:
parameters: {color: 128, detune: 64, gain: 128, lfo: 1, phase: 64, shape: 64, stereo: 0, transpose: 70, type: 0, unison: 0}
- type: send
parameters: {amount: 32, port: 0, sendpop: 1, stereo: 0, unit: 3, voice: 0}
delaytimes: []
sampleoffsets: []

View File

@ -21,5 +21,3 @@ patch:
parameters: {panning: 64, stereo: 0}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -21,5 +21,3 @@ patch:
parameters: {panning: 64, stereo: 0}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -21,5 +21,3 @@ patch:
parameters: {panning: 64, stereo: 0}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -25,5 +25,3 @@ patch:
parameters: {color: 128, detune: 64, gain: 128, lfo: 1, phase: 64, shape: 64, stereo: 0, transpose: 70, type: 0, unison: 0}
- type: send
parameters: {amount: 32, port: 1, sendpop: 1, stereo: 0, unit: 3, voice: 0}
delaytimes: []
sampleoffsets: []

View File

@ -21,5 +21,3 @@ patch:
parameters: {bandpass: 1, frequency: 32, highpass: 0, lowpass: 0, negbandpass: 0, neghighpass: 0, resonance: 64, stereo: 1}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -17,5 +17,3 @@ patch:
parameters: {gain: 64, stereo: 1}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -19,5 +19,3 @@ patch:
parameters: {gain: 64, stereo: 0}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -19,5 +19,3 @@ patch:
parameters: {holdfreq: 3, stereo: 0}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -25,5 +25,3 @@ patch:
parameters: {amount: 68, port: 0, sendpop: 1, stereo: 0, unit: 3, voice: 0}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -17,5 +17,3 @@ patch:
parameters: {holdfreq: 3, stereo: 1}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -27,5 +27,3 @@ patch:
parameters: {stereo: 1}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -25,5 +25,3 @@ patch:
parameters: {stereo: 1}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -19,5 +19,3 @@ patch:
parameters: {invgain: 64, stereo: 0}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -17,5 +17,3 @@ patch:
parameters: {invgain: 64, stereo: 1}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -15,5 +15,3 @@ patch:
parameters: {stereo: 0}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -13,5 +13,3 @@ patch:
parameters: {stereo: 1}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -15,5 +15,3 @@ patch:
parameters: {stereo: 0, value: 80}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -13,5 +13,3 @@ patch:
parameters: {stereo: 1, value: 40}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

View File

@ -17,5 +17,3 @@ patch:
parameters: {stereo: 0}
- type: out
parameters: {gain: 128, stereo: 1}
delaytimes: []
sampleoffsets: []

Some files were not shown because too many files have changed in this diff Show More