feat(sointu-cli): Merge the asmfmt and sointuplayer to generic command line utility for processing song files.

Currently supports: playing, exporting .asm (reformatting), exporting .h, exporting .raw (raw float32 buffer), exporting .json.
This commit is contained in:
Veikko Sariola 2020-12-05 00:01:24 +02:00
parent 726e79809d
commit d19d513ea8
4 changed files with 181 additions and 228 deletions

View File

@ -1,129 +0,0 @@
package main
import (
"flag"
"fmt"
"io/ioutil"
"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.")
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 {
flag.Usage()
os.Exit(0)
}
process := func(filename string) error {
origCodeBytes, err := ioutil.ReadFile(filename)
if err != nil {
return fmt.Errorf("could not read the file (%v)", err)
}
origCode := string(origCodeBytes)
song, err := go4k.DeserializeAsm(origCode)
if err != nil {
return fmt.Errorf("could not parse the file (%v)", err)
}
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 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 !*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)
}
}
return nil
}
retval := 0
for _, param := range flag.Args() {
if info, err := os.Stat(param); err == nil && info.IsDir() {
files, err := filepath.Glob(path.Join(param, "*.asm"))
if err != nil {
fmt.Fprintf(os.Stderr, "could not glob the path %v\n", param)
continue
}
for _, file := range files {
err := process(file)
if err != nil {
fmt.Fprintf(os.Stderr, "%v: %v\n", file, err)
retval = 1
}
}
} else {
err := process(param)
if err != nil {
fmt.Fprintf(os.Stderr, "%v: %v\n", param, err)
retval = 1
}
}
}
os.Exit(retval)
}
func printUsage() {
fmt.Fprintf(os.Stderr, "Usage: %s [flags] [path ...]\n", os.Args[0])
flag.PrintDefaults()
}

180
go4k/cmd/sointu-cli/main.go Normal file
View File

@ -0,0 +1,180 @@
package main
import (
"bytes"
"encoding/binary"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
"github.com/vsariola/sointu/go4k"
"github.com/vsariola/sointu/go4k/audio/oto"
"github.com/vsariola/sointu/go4k/bridge"
)
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.")
jsonOut := flag.Bool("j", false, "Output the song as .json 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.")
flag.Usage = printUsage
flag.Parse()
if flag.NArg() == 0 || *help {
flag.Usage()
os.Exit(0)
}
if !*asmOut && !*jsonOut && !*rawOut && !*headerOut && !*play {
*play = true // if the user gives nothing to output, then the default behaviour is just to play the file
}
needsRendering := *play || *exactLength || *rawOut
if needsRendering {
bridge.Init()
}
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 go4k.Song
if err := json.Unmarshal(inputBytes, &song); err != nil {
song2, err2 := go4k.DeserializeAsm(string(inputBytes))
if err2 != nil {
return fmt.Errorf("The song could not be parsed as .json (%v) nor .asm (%v)", err, err2)
}
song = *song2
}
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 = go4k.Play(synth, song) // render the song to calculate its length
if err != nil {
return fmt.Errorf("go4k.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 *headerOut {
maxSamples := 0 // 0 means it is calculated automatically
if *exactLength {
maxSamples = len(buffer) / 2
}
header := go4k.ExportCHeader(&song, maxSamples)
if err := output(".h", []byte(header)); err != nil {
return fmt.Errorf("Error outputting header file: %v", err)
}
}
if *asmOut {
asmCode, err := go4k.SerializeAsm(&song)
if err != nil {
return fmt.Errorf("Could not format the song as asm file: %v", err)
}
if err := output(".asm", []byte(asmCode)); 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 *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() {
files, err := filepath.Glob(path.Join(param, "*.asm"))
if err != nil {
fmt.Fprintf(os.Stderr, "Could not glob the path %v: %v\n", param, err)
retval = 1
continue
}
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()
}

View File

@ -1,98 +0,0 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"os"
"github.com/vsariola/sointu/go4k"
"github.com/vsariola/sointu/go4k/audio"
"github.com/vsariola/sointu/go4k/audio/oto"
"github.com/vsariola/sointu/go4k/bridge"
)
func main() {
// parse flags
quiet := flag.Bool("quiet", false, "no sound output")
out := flag.String("out", "", "write output to file")
help := flag.Bool("h", false, "show help")
flag.Usage = printUsage
flag.Parse()
if flag.NArg() == 0 || *help {
flag.Usage()
os.Exit(0)
}
// read input song
var song go4k.Song
if bytes, err := ioutil.ReadFile(flag.Arg(0)); err != nil {
fmt.Printf("Cannot read song file: %v", err)
os.Exit(1)
} else if err := json.Unmarshal(bytes, &song); err != nil {
song2, err2 := go4k.DeserializeAsm(string(bytes))
if err2 != nil {
fmt.Printf("Cannot unmarshal / parse song file: %v / %v", err, err2)
os.Exit(1)
}
song = *song2
}
bridge.Init()
// set up synth
synth, err := bridge.Synth(song.Patch)
if err != nil {
fmt.Printf("Cannot create synth: %v", err)
os.Exit(1)
}
// render the actual data for the entire song
fmt.Print("Rendering.. ")
buff, err := go4k.Play(synth, song)
if err != nil {
fmt.Printf("Error rendering with go4k: %v\n", err.Error())
os.Exit(1)
} else {
fmt.Printf("Rendered %v samples.\n", len(buff))
}
// play output if not in quiet mode
if !*quiet {
fmt.Print("Playing.. ")
player, err := oto.NewPlayer()
if err != nil {
fmt.Printf("Error creating oto player: %v\n", err.Error())
os.Exit(1)
}
defer player.Close()
if err := player.Play(buff); err != nil {
fmt.Printf("Error playing: %v\n", err.Error())
os.Exit(1)
}
fmt.Println("Played.")
}
// write output to file if output given
if out != nil && *out != "" {
fmt.Printf("Writing output to %v.. ", *out)
if bbuffer, err := audio.FloatBufferTo16BitLE(buff); err != nil {
fmt.Printf("Error converting buffer: %v\n", err.Error())
os.Exit(1)
} else if err := ioutil.WriteFile(*out, bbuffer, os.ModePerm); err != nil {
fmt.Printf("Error writing: %v\n", err.Error())
os.Exit(1)
} else {
fmt.Printf("Wrote %v bytes.\n", len(bbuffer))
}
}
fmt.Println("All done.")
os.Exit(0)
}
func printUsage() {
fmt.Fprintf(os.Stderr, "Usage: %s [OPTIONS] [SONG FILE] [OUTPUT FILE]\n", os.Args[0])
flag.PrintDefaults()
}

View File

@ -5,7 +5,7 @@ function(regression_test testname)
add_custom_command(
PRE_BUILD
OUTPUT ${headerfile}
COMMAND go run ${PROJECT_SOURCE_DIR}/go4k/cmd/asmfmt/main.go -c -d -w -o ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/${source}
COMMAND go run ${PROJECT_SOURCE_DIR}/go4k/cmd/sointu-cli/main.go -c -w -d ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/${source}
DEPENDS ${source}
)
add_executable(${testname} ${source} test_renderer.c ${headerfile})