mirror of
https://github.com/vsariola/sointu.git
synced 2025-08-30 09:54:36 -04:00
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:
134
go4k/bridge/asmformat_test.go
Normal file
134
go4k/bridge/asmformat_test.go
Normal file
@ -0,0 +1,134 @@
|
||||
package bridge_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/vsariola/sointu/go4k"
|
||||
"github.com/vsariola/sointu/go4k/bridge"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestAllAsmFiles(t *testing.T) {
|
||||
_, myname, _, _ := runtime.Caller(0)
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
var song go4k.Song
|
||||
err = yaml.Unmarshal(asmcode, &song)
|
||||
if err != nil {
|
||||
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 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))
|
||||
if err != nil {
|
||||
t.Fatalf("cannot read expected: %v", err)
|
||||
}
|
||||
expected := make([]float32, len(expectedb)/4)
|
||||
buf := bytes.NewReader(expectedb)
|
||||
err = binary.Read(buf, binary.LittleEndian, &expected)
|
||||
if err != nil {
|
||||
t.Fatalf("error converting expected buffer: %v", err)
|
||||
}
|
||||
if len(expected) != len(buffer) {
|
||||
t.Fatalf("buffer length mismatch, got %v, expected %v", len(buffer), len(expected))
|
||||
}
|
||||
for i, v := range expected {
|
||||
if math.IsNaN(float64(buffer[i])) || math.Abs(float64(v-buffer[i])) > 1e-6 {
|
||||
t.Fatalf("error bigger than 1e-6 detected, at sample position %v", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
if err != nil {
|
||||
t.Fatalf("cannot read expected: %v", err)
|
||||
}
|
||||
expected := make([]int16, len(expectedb)/2)
|
||||
buf := bytes.NewReader(expectedb)
|
||||
err = binary.Read(buf, binary.LittleEndian, &expected)
|
||||
if err != nil {
|
||||
t.Fatalf("error converting expected buffer: %v", err)
|
||||
}
|
||||
if len(expected) != len(buffer) {
|
||||
t.Fatalf("buffer length mismatch, got %v, expected %v", len(buffer), len(expected))
|
||||
}
|
||||
for i, v := range expected {
|
||||
if math.IsNaN(float64(buffer[i])) || v != buffer[i] {
|
||||
t.Fatalf("error at sample position %v", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func convertToInt16Buffer(buffer []float32) []int16 {
|
||||
int16Buffer := make([]int16, len(buffer))
|
||||
for i, v := range buffer {
|
||||
int16Buffer[i] = int16(math.Round(math.Min(math.Max(float64(v), -1.0), 1.0) * 32767))
|
||||
}
|
||||
return int16Buffer
|
||||
}
|
@ -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, ", ")
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -1,6 +0,0 @@
|
||||
// +build !windows
|
||||
|
||||
package bridge
|
||||
|
||||
func Init() {
|
||||
}
|
@ -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
|
||||
}
|
8
go4k/bridge/init_windows.go
Normal file
8
go4k/bridge/init_windows.go
Normal 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
|
||||
}
|
66
go4k/bridge/song_test.go
Normal file
66
go4k/bridge/song_test.go
Normal file
@ -0,0 +1,66 @@
|
||||
package bridge_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/vsariola/sointu/go4k"
|
||||
"github.com/vsariola/sointu/go4k/bridge"
|
||||
// TODO: test the song using a mocks instead
|
||||
)
|
||||
|
||||
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 TestPlayer(t *testing.T) {
|
||||
patch := go4k.Patch{
|
||||
Instruments: []go4k.Instrument{go4k.Instrument{1, []go4k.Unit{
|
||||
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}
|
||||
synth, err := bridge.Synth(patch)
|
||||
if err != nil {
|
||||
t.Fatalf("Compiling patch failed: %v", err)
|
||||
}
|
||||
buffer, err := go4k.Play(synth, song)
|
||||
if err != nil {
|
||||
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"))
|
||||
if err != nil {
|
||||
t.Fatalf("cannot read expected: %v", err)
|
||||
}
|
||||
var createdbuf bytes.Buffer
|
||||
err = binary.Write(&createdbuf, binary.LittleEndian, buffer)
|
||||
if err != nil {
|
||||
t.Fatalf("error converting buffer: %v", err)
|
||||
}
|
||||
createdb := createdbuf.Bytes()
|
||||
if len(createdb) != len(expectedb) {
|
||||
t.Fatalf("buffer length mismatch, got %v, expected %v", len(createdb), len(expectedb))
|
||||
}
|
||||
for i, v := range createdb {
|
||||
if expectedb[i] != v {
|
||||
t.Fatalf("byte mismatch @ %v, got %v, expected %v", i, v, expectedb[i])
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user