mirror of
https://github.com/vsariola/sointu.git
synced 2025-07-23 15:34:52 -04:00
feat(go4k&sointu): Export .h C header files from the songs using go, also automatically during build for the tests.
The header files are automatically generated during build. No need to #define anything; everything is fixed by the .asm file. This adds go as a dependency to run the unit tests, but this is probably not a bad thing, as go is probably needed anyway if one wants to actually start developing Sointu.
This commit is contained in:
@ -11,6 +11,7 @@ import (
|
||||
|
||||
func DeserializeAsm(asmcode string) (*Song, error) {
|
||||
var bpm int
|
||||
output16Bit := false
|
||||
scanner := bufio.NewScanner(strings.NewReader(asmcode))
|
||||
patterns := make([][]byte, 0)
|
||||
tracks := make([]Track, 0)
|
||||
@ -86,6 +87,8 @@ func DeserializeAsm(asmcode string) (*Song, error) {
|
||||
return nil, err
|
||||
}
|
||||
bpm = ints[0]
|
||||
} else if defineName == "OUTPUT_16BIT" {
|
||||
output16Bit = true
|
||||
}
|
||||
}
|
||||
case "PATTERN":
|
||||
@ -158,7 +161,7 @@ func DeserializeAsm(asmcode string) (*Song, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
s := Song{BPM: bpm, Patterns: patterns, Tracks: tracks, Patch: patch}
|
||||
s := Song{BPM: bpm, Patterns: patterns, Tracks: tracks, Patch: patch, Output16Bit: output16Bit}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
@ -252,6 +255,9 @@ func SerializeAsm(song *Song) (string, error) {
|
||||
}
|
||||
// The actual printing starts here
|
||||
println("%%define BPM %d", song.BPM)
|
||||
if song.Output16Bit {
|
||||
println("%%define OUTPUT_16BIT")
|
||||
}
|
||||
// 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.
|
||||
@ -369,3 +375,68 @@ func SerializeAsm(song *Song) (string, error) {
|
||||
ret := b.String()
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func ExportCHeader(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
|
||||
}
|
||||
|
@ -46,6 +46,7 @@ func TestAllAsmFiles(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
@ -70,7 +71,12 @@ func TestAllAsmFiles(t *testing.T) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
compareToRaw(t, buffer, testname+".raw")
|
||||
if song.Output16Bit {
|
||||
int16Buffer := convertToInt16Buffer(buffer)
|
||||
compareToRawInt16(t, int16Buffer, testname+".raw")
|
||||
} else {
|
||||
compareToRawFloat32(t, buffer, testname+".raw")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -114,6 +120,7 @@ func TestSerializingAllAsmFiles(t *testing.T) {
|
||||
t.Fatalf("Compiling patch failed: %v", err)
|
||||
}
|
||||
buffer, err := go4k.Play(synth, *song2)
|
||||
buffer = buffer[:song.TotalRows()*song.SamplesPerRow()*2] // extend to the nominal length always.
|
||||
if err != nil {
|
||||
t.Fatalf("Play failed: %v", err)
|
||||
}
|
||||
@ -138,12 +145,17 @@ func TestSerializingAllAsmFiles(t *testing.T) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
compareToRaw(t, buffer, testname+".raw")
|
||||
if song.Output16Bit {
|
||||
int16Buffer := convertToInt16Buffer(buffer)
|
||||
compareToRawInt16(t, int16Buffer, testname+".raw")
|
||||
} else {
|
||||
compareToRawFloat32(t, buffer, testname+".raw")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func compareToRaw(t *testing.T, buffer []float32, rawname string) {
|
||||
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 {
|
||||
@ -164,3 +176,33 @@ func compareToRaw(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))
|
||||
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
|
||||
}
|
||||
|
@ -7,14 +7,20 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/vsariola/sointu/go4k"
|
||||
"github.com/vsariola/sointu/go4k/bridge"
|
||||
)
|
||||
|
||||
func main() {
|
||||
write := flag.Bool("w", false, "Do not print reformatted asm songs to standard output. If a file's formatting is different from asmfmt's, overwrite it with asmfmt's version.")
|
||||
list := flag.Bool("l", false, "Do not print reformatted asm songs to standard output, just list the filenames that reformatting changes.")
|
||||
help := flag.Bool("h", false, "show help")
|
||||
help := flag.Bool("h", false, "Show help.")
|
||||
exactLength := flag.Bool("e", false, "Calculate the exact length of song by rendering it once. Only useful when using SPEED opcodes.")
|
||||
noformat := flag.Bool("d", false, "Disable formatting completely.")
|
||||
header := flag.Bool("c", false, "Generate the .h C-header files.")
|
||||
headeroutdir := flag.String("o", "", "Output directory for C-header files. By default, the headers are put in the same directory as the .asm file.")
|
||||
flag.Usage = printUsage
|
||||
flag.Parse()
|
||||
if flag.NArg() == 0 || *help {
|
||||
@ -31,24 +37,63 @@ func main() {
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse the file (%v)", err)
|
||||
}
|
||||
formattedCode, err := go4k.SerializeAsm(song)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not reformat the file (%v)", err)
|
||||
}
|
||||
if *write {
|
||||
if formattedCode != origCode {
|
||||
err := ioutil.WriteFile(filename, []byte(formattedCode), 0644)
|
||||
if *header {
|
||||
folder, name := filepath.Split(filename)
|
||||
if *headeroutdir != "" {
|
||||
folder = *headeroutdir
|
||||
}
|
||||
name = strings.TrimSuffix(name, filepath.Ext(name)) + ".h"
|
||||
headerfile := filepath.Join(folder, name)
|
||||
maxSamples := 0 // 0 means it is calculated automatically
|
||||
if *exactLength {
|
||||
synth, err := bridge.Synth(song.Patch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could write to file (%v)", err)
|
||||
return fmt.Errorf("could not create synth based on the patch (%v)", err)
|
||||
}
|
||||
buffer, err := go4k.Play(synth, *song) // render the song to calculate its length
|
||||
if err != nil {
|
||||
return fmt.Errorf("error when rendering the song for calculating its length (%v)", err)
|
||||
}
|
||||
maxSamples = len(buffer) / 2
|
||||
}
|
||||
newheader := go4k.ExportCHeader(song, maxSamples)
|
||||
origHeader, err := ioutil.ReadFile(headerfile)
|
||||
if *list {
|
||||
if err != nil || newheader != string(origHeader) {
|
||||
fmt.Println(headerfile)
|
||||
}
|
||||
} else if !*write {
|
||||
fmt.Print(newheader)
|
||||
}
|
||||
if *write {
|
||||
if err != nil || newheader != string(origHeader) {
|
||||
err := ioutil.WriteFile(headerfile, []byte(newheader), 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could write to file (%v)", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if *list {
|
||||
if formattedCode != origCode {
|
||||
fmt.Println(filename)
|
||||
if !*noformat {
|
||||
formattedCode, err := go4k.SerializeAsm(song)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not reformat the file (%v)", err)
|
||||
}
|
||||
if *write {
|
||||
if formattedCode != origCode {
|
||||
err := ioutil.WriteFile(filename, []byte(formattedCode), 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could write to file (%v)", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if *list {
|
||||
if formattedCode != origCode {
|
||||
fmt.Println(filename)
|
||||
}
|
||||
} else if !*write {
|
||||
fmt.Print(formattedCode)
|
||||
}
|
||||
} else if !*write {
|
||||
fmt.Print(formattedCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -6,10 +6,11 @@ import (
|
||||
)
|
||||
|
||||
type Song struct {
|
||||
BPM int
|
||||
Patterns [][]byte
|
||||
Tracks []Track
|
||||
Patch Patch
|
||||
BPM int
|
||||
Patterns [][]byte
|
||||
Tracks []Track
|
||||
Patch Patch
|
||||
Output16Bit bool
|
||||
}
|
||||
|
||||
func (s *Song) PatternRows() int {
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
"github.com/vsariola/sointu/go4k"
|
||||
)
|
||||
|
||||
const expectedMarshaled = `{"BPM":100,"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":[]}}`
|
||||
const expectedMarshaled = `{"BPM":100,"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":[]},"Output16Bit":false}`
|
||||
|
||||
var testSong = go4k.Song{
|
||||
BPM: 100,
|
||||
|
@ -37,7 +37,7 @@ func TestPlayer(t *testing.T) {
|
||||
SampleOffsets: []go4k.SampleOffset{}}
|
||||
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{100, patterns, tracks, patch}
|
||||
song := go4k.Song{100, patterns, tracks, patch, false}
|
||||
synth, err := bridge.Synth(patch)
|
||||
if err != nil {
|
||||
t.Fatalf("Compiling patch failed: %v", err)
|
||||
|
Reference in New Issue
Block a user