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

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

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
}

66
go4k/bridge/song_test.go Normal file
View 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])
}
}
}