mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-25 18:00:37 -04:00
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:
parent
2d00640e06
commit
7f049acf88
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal 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
|
@ -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})
|
||||
|
@ -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
207
cmd/sointu-compile/main.go
Normal 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
220
cmd/sointu-play/main.go
Normal 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
|
||||
}
|
@ -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
|
||||
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)
|
||||
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 "library.asm": %v`, err)
|
||||
return nil, fmt.Errorf(`could not execute template "%v": %v`, templateName, 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)
|
||||
retmap[extension] = populatedTemplate
|
||||
}
|
||||
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))
|
||||
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 "player.asm": %v`, err)
|
||||
return nil, fmt.Errorf(`could not execute template "%v": %v`, templateName, 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)
|
||||
retmap[extension] = populatedTemplate
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
@ -18,6 +18,8 @@ type Macros struct {
|
||||
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
|
||||
@ -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}
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
type Unit struct {
|
||||
Type string
|
||||
Parameters map[string]int `yaml:",flow"`
|
||||
VarArgs []int
|
||||
VarArgs []int `,omitempty`
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user