feat(cli): Re-engineer CLIs, split play & compile

Play depends on bridge and compile on compiler package. Before, the compiler depended on bridge, but we could not use the compiler to build the library, as the bridge depends on the library. Also, play can now start having slightly more options e.g. wav out etc.
This commit is contained in:
Veikko Sariola 2020-12-18 14:18:00 +02:00
parent 2d00640e06
commit 7f049acf88
11 changed files with 513 additions and 365 deletions

4
.gitattributes vendored Normal file
View File

@ -0,0 +1,4 @@
# Declare files that will always have LF line endings on checkout.
# Necessary on windows, as the sointu-compiler formats .yml using only lf so
# this would trigger changes in the files, even when they didn't really change.
*.yml text eol=lf

View File

@ -35,15 +35,16 @@ enable_language(ASM_NASM)
set(CMAKE_ASM_NASM_COMPILE_OBJECT "<CMAKE_ASM_NASM_COMPILER> <INCLUDES> <DEFINES> <FLAGS> -f ${CMAKE_ASM_NASM_OBJECT_FORMAT} -o <OBJECT> <SOURCE>")
if(WIN32)
set(sointuexe ${CMAKE_CURRENT_BINARY_DIR}/sointu-cli.exe)
set(compilecmd ${CMAKE_CURRENT_BINARY_DIR}/sointu-compile.exe)
else()
set(sointuexe ${CMAKE_CURRENT_BINARY_DIR}/sointu-cli)
set(compilecmd ${CMAKE_CURRENT_BINARY_DIR}/sointu-compile)
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 sointu "${PROJECT_SOURCE_DIR}/*.go" "${PROJECT_SOURCE_DIR}/cmd/sointu-cli/*.go")
file(GLOB sointusrc "${PROJECT_SOURCE_DIR}/*.go")
file(GLOB compilersrc "${PROJECT_SOURCE_DIR}/compiler/*.go")
file(GLOB compilecmdsrc "${PROJECT_SOURCE_DIR}/cmd/sointu-compile/*.go")
if(DEFINED CMAKE_C_SIZEOF_DATA_PTR AND CMAKE_C_SIZEOF_DATA_PTR EQUAL 8)
set(arch "amd64")
@ -57,10 +58,19 @@ endif()
set(STATICLIB sointu)
set(sointuasm sointu.asm)
# Build sointu-cli only once because go run has everytime quite a bit of delay when
# starting
add_custom_command(
OUTPUT ${compilecmd}
COMMAND go build -o ${compilecmd} ${PROJECT_SOURCE_DIR}/cmd/sointu-compile/main.go
DEPENDS "${sointusrc}" "${compilersrc}" "${compilecmdsrc}"
)
add_custom_command(
OUTPUT ${sointuasm}
COMMAND go run ${PROJECT_SOURCE_DIR}/compiler/generate.go -arch=${arch} ${CMAKE_CURRENT_BINARY_DIR}
DEPENDS "${templates}" "${sointu}" "${compilersrc}"
COMMAND ${compilecmd} -arch=${arch} -a -o ${CMAKE_CURRENT_BINARY_DIR}
DEPENDS "${templates}" ${compilecmd}
)
add_library(${STATICLIB} ${sointuasm})

View File

@ -1,227 +0,0 @@
package main
import (
"bytes"
"encoding/binary"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"gopkg.in/yaml.v3"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/audio/oto"
"github.com/vsariola/sointu/bridge"
"github.com/vsariola/sointu/compiler"
)
func main() {
write := flag.Bool("w", false, "Do not output to standard output; (over)write files on disk instead.")
list := flag.Bool("l", false, "Do not output to standard output; list files that change if -w is applied.")
help := flag.Bool("h", false, "Show help.")
play := flag.Bool("p", false, "Play the input songs.")
asmOut := flag.Bool("a", false, "Output the song as .asm file, to standard output unless otherwise specified.")
tmplDir := flag.String("t", "", "Output the song as by parsing the templates in directory, to standard output unless otherwise specified.")
jsonOut := flag.Bool("j", false, "Output the song as .json file, to standard output unless otherwise specified.")
yamlOut := flag.Bool("y", false, "Output the song as .yml file, to standard output unless otherwise specified.")
headerOut := flag.Bool("c", false, "Output .h C header file, to standard output unless otherwise specified.")
exactLength := flag.Bool("e", false, "When outputting the C header file, calculate the exact length of song by rendering it once. Only useful when using SPEED opcodes.")
rawOut := flag.Bool("r", false, "Output the rendered song as .raw stereo float32 buffer, to standard output unless otherwise specified.")
directory := flag.String("d", "", "Directory where to output all files. The directory and its parents are created if needed. By default, everything is placed in the same directory where the original song file is.")
hold := flag.Int("o", -1, "New value to be used as the hold value")
targetArch := flag.String("arch", runtime.GOARCH, "Target architecture. Defaults to OS architecture. Possible values: 386, amd64")
targetOs := flag.String("os", runtime.GOOS, "Target OS. Defaults to current OS. Possible values: windows, darwin, linux (anything else is assumed linuxy)")
flag.Usage = printUsage
flag.Parse()
if flag.NArg() == 0 || *help {
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
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 {
if !*write && !*list {
fmt.Print(string(contents))
return nil
}
dir, name := filepath.Split(filename)
if *directory != "" {
dir = *directory
}
name = strings.TrimSuffix(name, filepath.Ext(name)) + extension
f := filepath.Join(dir, name)
original, err := ioutil.ReadFile(f)
if err == nil {
if bytes.Compare(original, contents) == 0 {
return nil // no need to update
}
}
if *list {
fmt.Println(f)
}
if *write {
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return fmt.Errorf("Could not create output directory %v: %v", dir, err)
}
err := ioutil.WriteFile(f, contents, 0644)
if err != nil {
return fmt.Errorf("Could not write file %v: %v", f, err)
}
}
return nil
}
inputBytes, err := ioutil.ReadFile(filename)
if err != nil {
return fmt.Errorf("Could not read file %v: %v", filename, err)
}
var song sointu.Song
if errJSON := json.Unmarshal(inputBytes, &song); errJSON != nil {
if errYaml := yaml.Unmarshal(inputBytes, &song); errYaml != nil {
return fmt.Errorf("The song could not be parsed as .json (%v) or .yml (%v)", errJSON, errYaml)
}
}
var buffer []float32
if needsRendering {
synth, err := bridge.Synth(song.Patch)
if err != nil {
return fmt.Errorf("Could not create synth based on the patch: %v", err)
}
buffer, err = sointu.Play(synth, song) // render the song to calculate its length
if err != nil {
return fmt.Errorf("sointu.Play failed: %v", err)
}
}
if *play {
player, err := oto.NewPlayer()
if err != nil {
return fmt.Errorf("Error creating oto player: %v", err)
}
defer player.Close()
if err := player.Play(buffer); err != nil {
return fmt.Errorf("Error playing: %v", err)
}
}
if *hold > -1 {
err = song.UpdateHold(byte(*hold))
if err != nil {
return fmt.Errorf("error updating the hold value of the song: %v", err)
}
}
var compiledPlayer map[string]string
if needsCompile {
maxSamples := 0 // 0 means it is calculated automatically
if *exactLength {
maxSamples = len(buffer) / 2
}
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 {
if err := output(".asm", []byte(compiledPlayer["asm"])); err != nil {
return fmt.Errorf("Error outputting asm file: %v", err)
}
}
if *jsonOut {
jsonSong, err := json.Marshal(song)
if err != nil {
return fmt.Errorf("Could not marshal the song as json file: %v", err)
}
if err := output(".json", jsonSong); err != nil {
return fmt.Errorf("Error outputting JSON file: %v", err)
}
}
if *yamlOut {
yamlSong, err := yaml.Marshal(song)
if err != nil {
return fmt.Errorf("Could not marshal the song as yaml file: %v", err)
}
if err := output(".yml", yamlSong); err != nil {
return fmt.Errorf("Error outputting yaml file: %v", err)
}
}
if *rawOut {
buf := new(bytes.Buffer)
err := binary.Write(buf, binary.LittleEndian, buffer)
if err != nil {
return fmt.Errorf("Could not binary write the float32 buffer to a byte buffer: %v", err)
}
if err := output(".raw", buf.Bytes()); err != nil {
return fmt.Errorf("Error outputting raw audio file: %v", err)
}
}
return nil
}
retval := 0
for _, param := range flag.Args() {
if info, err := os.Stat(param); err == nil && info.IsDir() {
asmfiles, err := filepath.Glob(filepath.Join(param, "*.asm"))
if err != nil {
fmt.Fprintf(os.Stderr, "Could not glob the path %v for asm files: %v\n", param, err)
retval = 1
continue
}
jsonfiles, err := filepath.Glob(filepath.Join(param, "*.json"))
if err != nil {
fmt.Fprintf(os.Stderr, "Could not glob the path %v for json files: %v\n", param, err)
retval = 1
continue
}
ymlfiles, err := filepath.Glob(filepath.Join(param, "*.yml"))
if err != nil {
fmt.Fprintf(os.Stderr, "Could not glob the path %v for yml files: %v\n", param, err)
retval = 1
continue
}
files := append(asmfiles, jsonfiles...)
files = append(files, ymlfiles...)
for _, file := range files {
err := process(file)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not process file %v: %v\n", file, err)
retval = 1
}
}
} else {
err := process(param)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not process file %v: %v\n", param, err)
retval = 1
}
}
}
os.Exit(retval)
}
func printUsage() {
fmt.Fprintf(os.Stderr, "Sointu command line utility for processing .asm/.json song files.\nUsage: %s [flags] [path ...]\n", os.Args[0])
flag.PrintDefaults()
}

207
cmd/sointu-compile/main.go Normal file
View File

@ -0,0 +1,207 @@
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"gopkg.in/yaml.v3"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/compiler"
)
func filterExtensions(input map[string]string, extensions []string) map[string]string {
ret := map[string]string{}
for _, ext := range extensions {
extWithDot := "." + ext
if inputVal, ok := input[extWithDot]; ok {
ret[extWithDot] = inputVal
}
}
return ret
}
func main() {
safe := flag.Bool("n", false, "Never overwrite files; if file already exists and would be overwritten, give an error.")
list := flag.Bool("l", false, "Do not write files; just list files that would change instead.")
stdout := flag.Bool("s", false, "Do not write files; write to standard output instead.")
help := flag.Bool("h", false, "Show help.")
library := flag.Bool("a", false, "Compile Sointu into a library. Input files are not needed.")
jsonOut := flag.Bool("j", false, "Output the song as .json file instead of compiling.")
yamlOut := flag.Bool("y", false, "Output the song as .yml file instead of compiling.")
tmplDir := flag.String("t", "", "When compiling, use the templates in this directory instead of the standard templates.")
directory := flag.String("o", "", "Directory where to output all files. The directory and its parents are created if needed. By default, everything is placed in the same directory where the original song file is.")
extensionsOut := flag.String("e", "", "Output only the compiled files with these comma separated extensions. For example: h,asm")
hold := flag.Int("hold", -1, "New value to be used as the hold value. -1 = do not change.")
targetArch := flag.String("arch", runtime.GOARCH, "Target architecture. Defaults to OS architecture. Possible values: 386, amd64")
targetOs := flag.String("os", runtime.GOOS, "Target OS. Defaults to current OS. Possible values: windows, darwin, linux. Anything else is assumed linuxy.")
flag.Usage = printUsage
flag.Parse()
if (flag.NArg() == 0 && !*library) || *help {
flag.Usage()
os.Exit(0)
}
compile := !*jsonOut && !*yamlOut // if the user gives nothing to output, then the default behaviour is to compile the file
var comp *compiler.Compiler
if compile || *library {
var err error
if *tmplDir != "" {
comp, err = compiler.NewFromTemplates(*targetOs, *targetArch, *tmplDir)
} else {
comp, err = compiler.New(*targetOs, *targetArch)
}
if err != nil {
fmt.Fprintf(os.Stderr, `error creating compiler: %v`, err)
os.Exit(1)
}
}
output := func(filename string, extension string, contents []byte) error {
if *stdout {
fmt.Print(string(contents))
return nil
}
dir, name := filepath.Split(filename)
if *directory != "" {
dir = *directory
}
name = strings.TrimSuffix(name, filepath.Ext(name)) + extension
f := filepath.Join(dir, name)
original, err := ioutil.ReadFile(f)
if err == nil {
if bytes.Compare(original, contents) == 0 {
return nil // no need to update
}
if !*list && *safe {
return fmt.Errorf("file %v would be overwritten by compiler", f)
}
}
if *list {
fmt.Println(f)
} else {
if dir != "" {
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return fmt.Errorf("could not create output directory %v: %v", dir, err)
}
}
err := ioutil.WriteFile(f, contents, 0644)
if err != nil {
return fmt.Errorf("could not write file %v: %v", f, err)
}
}
return nil
}
process := func(filename string) error {
inputBytes, err := ioutil.ReadFile(filename)
if err != nil {
return fmt.Errorf("could not read file %v: %v", filename, err)
}
var song sointu.Song
if errJSON := json.Unmarshal(inputBytes, &song); errJSON != nil {
if errYaml := yaml.Unmarshal(inputBytes, &song); errYaml != nil {
return fmt.Errorf("song could not be unmarshaled as a .json (%v) or .yml (%v)", errJSON, errYaml)
}
}
if *hold > -1 {
err = song.UpdateHold(byte(*hold))
if err != nil {
return fmt.Errorf("error updating the hold value of the song: %v", err)
}
}
var compiledPlayer map[string]string
if compile {
var err error
compiledPlayer, err = comp.Song(&song)
if err != nil {
return fmt.Errorf("compiling player failed: %v", err)
}
if len(*extensionsOut) > 0 {
compiledPlayer = filterExtensions(compiledPlayer, strings.Split(*extensionsOut, ","))
}
for extension, code := range compiledPlayer {
if err := output(filename, extension, []byte(code)); err != nil {
return fmt.Errorf("error outputting %v file: %v", extension, err)
}
}
}
if *jsonOut {
jsonSong, err := json.Marshal(song)
if err != nil {
return fmt.Errorf("could not marshal the song as json file: %v", err)
}
if err := output(filename, ".json", jsonSong); err != nil {
return fmt.Errorf("error outputting json file: %v", err)
}
}
if *yamlOut {
yamlSong, err := yaml.Marshal(song)
if err != nil {
return fmt.Errorf("could not marshal the song as yaml file: %v", err)
}
if err := output(filename, ".yml", yamlSong); err != nil {
return fmt.Errorf("error outputting yaml file: %v", err)
}
}
return nil
}
retval := 0
if *library {
compiledLibrary, err := comp.Library()
if err != nil {
fmt.Fprintf(os.Stderr, "compiling library failed: %v\n", err)
retval = 1
} else {
if len(*extensionsOut) > 0 {
compiledLibrary = filterExtensions(compiledLibrary, strings.Split(*extensionsOut, ","))
}
for extension, code := range compiledLibrary {
if err := output("sointu", extension, []byte(code)); err != nil {
fmt.Fprintf(os.Stderr, "error outputting %v file: %v", extension, err)
retval = 1
}
}
}
}
for _, param := range flag.Args() {
if info, err := os.Stat(param); err == nil && info.IsDir() {
jsonfiles, err := filepath.Glob(filepath.Join(param, "*.json"))
if err != nil {
fmt.Fprintf(os.Stderr, "could not glob the path %v for json files: %v\n", param, err)
retval = 1
continue
}
ymlfiles, err := filepath.Glob(filepath.Join(param, "*.yml"))
if err != nil {
fmt.Fprintf(os.Stderr, "could not glob the path %v for yml files: %v\n", param, err)
retval = 1
continue
}
files := append(ymlfiles, jsonfiles...)
for _, file := range files {
err := process(file)
if err != nil {
fmt.Fprintf(os.Stderr, "could not process file %v: %v\n", file, err)
retval = 1
}
}
} else {
err := process(param)
if err != nil {
fmt.Fprintf(os.Stderr, "could not process file %v: %v\n", param, err)
retval = 1
}
}
}
os.Exit(retval)
}
func printUsage() {
fmt.Fprintf(os.Stderr, "Sointu compiler. Input .yml or .json songs, outputs compiled songs (e.g. .asm and .h files).\nUsage: %s [flags] [path ...]\n", os.Args[0])
flag.PrintDefaults()
}

220
cmd/sointu-play/main.go Normal file
View File

@ -0,0 +1,220 @@
package main
import (
"bytes"
"encoding/binary"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"math"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/audio/oto"
"github.com/vsariola/sointu/bridge"
)
func main() {
stdout := flag.Bool("s", false, "Do not write files; write to standard output instead.")
help := flag.Bool("h", false, "Show help.")
directory := flag.String("o", "", "Directory where to output all files. The directory and its parents are created if needed. By default, everything is placed in the same directory where the original song file is.")
play := flag.Bool("p", false, "Play the input songs (default behaviour when no other output is defined).")
//start := flag.Float64("start", 0, "Start playing from part; given in the units defined by parameter `unit`.")
//stop := flag.Float64("stop", -1, "Stop playing at part; given in the units defined by parameter `unit`. Negative values indicate render until end.")
//units := flag.String("unit", "pattern", "Units for parameters start and stop. Possible values: second, sample, pattern, beat. Warning: beat and pattern do not take SPEED modulations into account.")
rawOut := flag.Bool("r", false, "Output the rendered song as .raw file. By default, saves stereo float32 buffer to disk.")
wavOut := flag.Bool("w", false, "Output the rendered song as .wav file. By default, saves stereo float32 buffer to disk.")
pcm := flag.Bool("c", false, "Convert audio to 16-bit signed PCM when outputting.")
flag.Usage = printUsage
flag.Parse()
if flag.NArg() == 0 || *help {
flag.Usage()
os.Exit(0)
}
if !*rawOut && !*wavOut {
*play = true // if the user gives nothing to output, then the default behaviour is just to play the file
}
process := func(filename string) error {
output := func(extension string, contents []byte) error {
if *stdout {
fmt.Print(contents)
return nil
}
dir, name := filepath.Split(filename)
if *directory != "" {
dir = *directory
}
name = strings.TrimSuffix(name, filepath.Ext(name)) + extension
f := filepath.Join(dir, name)
if dir != "" {
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return fmt.Errorf("could not create output directory %v: %v", dir, err)
}
}
err := ioutil.WriteFile(f, contents, 0644)
if err != nil {
return fmt.Errorf("could not write file %v: %v", f, err)
}
return nil
}
inputBytes, err := ioutil.ReadFile(filename)
if err != nil {
return fmt.Errorf("could not read file %v: %v", filename, err)
}
var song sointu.Song
if errJSON := json.Unmarshal(inputBytes, &song); errJSON != nil {
if errYaml := yaml.Unmarshal(inputBytes, &song); errYaml != nil {
return fmt.Errorf("the song could not be parsed as .json (%v) or .yml (%v)", errJSON, errYaml)
}
}
synth, err := bridge.Synth(song.Patch)
if err != nil {
return fmt.Errorf("could not create synth based on the patch: %v", err)
}
buffer, err := sointu.Play(synth, song) // render the song to calculate its length
if err != nil {
return fmt.Errorf("sointu.Play failed: %v", err)
}
if *play {
player, err := oto.NewPlayer()
if err != nil {
return fmt.Errorf("error creating oto player: %v", err)
}
defer player.Close()
if err := player.Play(buffer); err != nil {
return fmt.Errorf("error playing: %v", err)
}
}
var data interface{}
data = buffer
if *pcm {
int16buffer := make([]int16, len(buffer))
for i, v := range buffer {
int16buffer[i] = int16(clamp(int(v*math.MaxInt16), math.MinInt16, math.MaxInt16))
}
data = int16buffer
}
if *rawOut {
buf := new(bytes.Buffer)
err := binary.Write(buf, binary.LittleEndian, data)
if err != nil {
return fmt.Errorf("could not binary write data to binary buffer: %v", err)
}
if err := output(".raw", buf.Bytes()); err != nil {
return fmt.Errorf("error outputting raw audio file: %v", err)
}
}
if *wavOut {
buf := new(bytes.Buffer)
header := createWavHeader(len(buffer), *pcm)
err := binary.Write(buf, binary.LittleEndian, header)
if err != nil {
return fmt.Errorf("could not binary write header to binary buffer: %v", err)
}
err = binary.Write(buf, binary.LittleEndian, data)
if err != nil {
return fmt.Errorf("could not binary write data to binary buffer: %v", err)
}
if err := output(".wav", buf.Bytes()); err != nil {
return fmt.Errorf("error outputting wav audio file: %v", err)
}
}
return nil
}
retval := 0
for _, param := range flag.Args() {
if info, err := os.Stat(param); err == nil && info.IsDir() {
jsonfiles, err := filepath.Glob(filepath.Join(param, "*.json"))
if err != nil {
fmt.Fprintf(os.Stderr, "could not glob the path %v for json files: %v\n", param, err)
retval = 1
continue
}
ymlfiles, err := filepath.Glob(filepath.Join(param, "*.yml"))
if err != nil {
fmt.Fprintf(os.Stderr, "could not glob the path %v for yml files: %v\n", param, err)
retval = 1
continue
}
files := append(ymlfiles, jsonfiles...)
for _, file := range files {
err := process(file)
if err != nil {
fmt.Fprintf(os.Stderr, "could not process file %v: %v\n", file, err)
retval = 1
}
}
} else {
err := process(param)
if err != nil {
fmt.Fprintf(os.Stderr, "could not process file %v: %v\n", param, err)
retval = 1
}
}
}
os.Exit(retval)
}
func printUsage() {
fmt.Fprintf(os.Stderr, "Sointu command line utility for playing .asm/.json song files.\nUsage: %s [flags] [path ...]\n", os.Args[0])
flag.PrintDefaults()
}
func createWavHeader(bufferLength int, pcm bool) []byte {
// Refer to: http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
numChannels := 2
sampleRate := 44100
var bytesPerSample, chunkSize, fmtChunkSize, waveFormat int
var factChunk bool
if pcm {
bytesPerSample = 2
chunkSize = 36 + bytesPerSample*bufferLength
fmtChunkSize = 16
waveFormat = 1 // PCM
factChunk = false
} else {
bytesPerSample = 4
chunkSize = 50 + bytesPerSample*bufferLength
fmtChunkSize = 18
waveFormat = 3 // IEEE float
factChunk = true
}
buf := new(bytes.Buffer)
buf.Write([]byte("RIFF"))
binary.Write(buf, binary.LittleEndian, uint32(chunkSize))
buf.Write([]byte("WAVE"))
buf.Write([]byte("fmt "))
binary.Write(buf, binary.LittleEndian, uint32(fmtChunkSize))
binary.Write(buf, binary.LittleEndian, uint16(waveFormat))
binary.Write(buf, binary.LittleEndian, uint16(numChannels))
binary.Write(buf, binary.LittleEndian, uint32(sampleRate))
binary.Write(buf, binary.LittleEndian, uint32(sampleRate*numChannels*bytesPerSample)) // avgBytesPerSec
binary.Write(buf, binary.LittleEndian, uint16(numChannels*bytesPerSample)) // blockAlign
binary.Write(buf, binary.LittleEndian, uint16(8*bytesPerSample)) // bits per sample
if fmtChunkSize > 16 {
binary.Write(buf, binary.LittleEndian, uint16(0)) // size of extension
}
if factChunk {
buf.Write([]byte("fact"))
binary.Write(buf, binary.LittleEndian, uint32(4)) // fact chunk size
binary.Write(buf, binary.LittleEndian, uint32(bufferLength)) // sample length
}
buf.Write([]byte("data"))
binary.Write(buf, binary.LittleEndian, uint32(bytesPerSample*bufferLength))
return buf.Bytes()
}
func clamp(value, min, max int) int {
if value < min {
return min
}
if value > max {
return max
}
return value
}

View File

@ -12,70 +12,73 @@ import (
"github.com/vsariola/sointu"
)
//go:generate go run generate.go
type Compiler struct {
Template *template.Template
Amd64 bool
OS string
DisableSections bool
Template *template.Template
OS string
Arch string
}
// New returns a new compiler using the default .asm templates
func New() (*Compiler, error) {
func New(os string, arch string) (*Compiler, error) {
_, myname, _, _ := runtime.Caller(0)
templateDir := filepath.Join(path.Dir(myname), "..", "templates")
compiler, err := NewFromTemplates(templateDir)
compiler, err := NewFromTemplates(os, arch, templateDir)
return compiler, err
}
func NewFromTemplates(directory string) (*Compiler, error) {
globPtrn := filepath.Join(directory, "*.*")
func NewFromTemplates(os string, arch string, templateDirectory string) (*Compiler, error) {
globPtrn := filepath.Join(templateDirectory, "*.*")
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 nil, fmt.Errorf(`could not create template based on directory "%v": %v`, templateDirectory, 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
return &Compiler{Template: tmpl, OS: os, Arch: arch}, nil
}
func (com *Compiler) Library() (map[string]string, error) {
if com.Arch != "386" && com.Arch != "amd64" {
return nil, fmt.Errorf(`compiling as a library is supported only on 386 and amd64 architectures (targeted architecture was %v)`, com.Arch)
}
templates := []string{"library.asm", "library.h"}
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)
retmap := map[string]string{}
for _, templateName := range templates {
macros := NewMacros(*com, features)
macros.Library = true
populatedTemplate, extension, err := com.compile(templateName, macros)
if err != nil {
return nil, fmt.Errorf(`could not execute template "%v": %v`, templateName, err)
}
retmap[extension] = populatedTemplate
}
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
return retmap, nil
}
func (com *Compiler) Player(song *sointu.Song, maxSamples int) (map[string]string, error) {
func (com *Compiler) Song(song *sointu.Song) (map[string]string, error) {
if com.Arch != "386" && com.Arch != "amd64" {
return nil, fmt.Errorf(`compiling a song player is supported only on 386 and amd64 architectures (targeted architecture was %v)`, com.Arch)
}
templates := []string{"player.asm", "player.h"}
features := NecessaryFeaturesFor(song.Patch)
retmap := map[string]string{}
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)
for _, templateName := range templates {
macros := NewPlayerMacros(*com, features, song, encodedPatch)
populatedTemplate, extension, err := com.compile(templateName, macros)
if err != nil {
return nil, fmt.Errorf(`could not execute template "%v": %v`, templateName, err)
}
retmap[extension] = populatedTemplate
}
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
return retmap, nil
}
func (com *Compiler) compile(templateName string, data interface{}) (string, string, error) {
result := bytes.NewBufferString("")
err := com.Template.ExecuteTemplate(result, templateName, data)
extension := filepath.Ext(templateName)
return result.String(), extension, err
}

View File

@ -1,61 +0,0 @@
// 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/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

@ -14,21 +14,23 @@ type OplistEntry struct {
}
type Macros struct {
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
Stacklocs []string
Output16Bit bool
Clip bool
Library bool
Amd64 bool
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
stackframes map[string][]string
FeatureSet
Compiler
}
@ -44,6 +46,7 @@ func NewMacros(c Compiler, f FeatureSet) *Macros {
Pulse: sointu.Pulse,
Gate: sointu.Gate,
Sample: sointu.Sample,
Amd64: c.Arch == "amd64",
Compiler: c,
FeatureSet: f,
}
@ -449,10 +452,8 @@ type PlayerMacros struct {
EncodedPatch
}
func NewPlayerMacros(c Compiler, f FeatureSet, s *sointu.Song, e *EncodedPatch, maxSamples int) *PlayerMacros {
if maxSamples == 0 {
maxSamples = s.SamplesPerRow() * s.TotalRows()
}
func NewPlayerMacros(c Compiler, f FeatureSet, s *sointu.Song, e *EncodedPatch) *PlayerMacros {
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}

View File

@ -10,7 +10,7 @@ import (
type Unit struct {
Type string
Parameters map[string]int `yaml:",flow"`
VarArgs []int
VarArgs []int `,omitempty`
}
const (

View File

@ -7,8 +7,8 @@ function(regression_test testname)
add_custom_command(
OUTPUT ${asmfile}
COMMAND ${sointuexe} -a -c -w -arch=${arch} -d ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/${source}
DEPENDS ${source} ${sointuexe}
COMMAND ${compilecmd} -arch=${arch} -o ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/${source}
DEPENDS ${source} ${compilecmd} ${templates}
)
add_executable(${testname} test_renderer.c ${asmfile})
@ -48,15 +48,6 @@ function(regression_test testname)
endif()
endfunction(regression_test)
# 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}/cmd/sointu-cli/main.go
DEPENDS "${templates}" "${sointu}" "${compilersrc}" ${STATICLIB}
)
regression_test(test_envelope "" ENVELOPE)
regression_test(test_envelope_stereo ENVELOPE)
regression_test(test_loadval "" LOADVAL)