From d19d513ea83eebe2b17651002ce0f5cf1be3df4c Mon Sep 17 00:00:00 2001 From: Veikko Sariola Date: Sat, 5 Dec 2020 00:01:24 +0200 Subject: [PATCH] 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. --- go4k/cmd/asmfmt/main.go | 129 ----------------------- go4k/cmd/sointu-cli/main.go | 180 +++++++++++++++++++++++++++++++++ go4k/cmd/sointu-player/main.go | 98 ------------------ tests/CMakeLists.txt | 2 +- 4 files changed, 181 insertions(+), 228 deletions(-) delete mode 100644 go4k/cmd/asmfmt/main.go create mode 100644 go4k/cmd/sointu-cli/main.go delete mode 100644 go4k/cmd/sointu-player/main.go diff --git a/go4k/cmd/asmfmt/main.go b/go4k/cmd/asmfmt/main.go deleted file mode 100644 index 9d8481e..0000000 --- a/go4k/cmd/asmfmt/main.go +++ /dev/null @@ -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() -} diff --git a/go4k/cmd/sointu-cli/main.go b/go4k/cmd/sointu-cli/main.go new file mode 100644 index 0000000..3c3f781 --- /dev/null +++ b/go4k/cmd/sointu-cli/main.go @@ -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() +} diff --git a/go4k/cmd/sointu-player/main.go b/go4k/cmd/sointu-player/main.go deleted file mode 100644 index 3527569..0000000 --- a/go4k/cmd/sointu-player/main.go +++ /dev/null @@ -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() -} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index cb98271..9fc7670 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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})