mirror of
https://github.com/vsariola/sointu.git
synced 2025-06-04 01:28:45 -04:00
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:
parent
726e79809d
commit
d19d513ea8
@ -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
180
go4k/cmd/sointu-cli/main.go
Normal 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()
|
||||||
|
}
|
@ -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()
|
|
||||||
}
|
|
@ -5,7 +5,7 @@ function(regression_test testname)
|
|||||||
add_custom_command(
|
add_custom_command(
|
||||||
PRE_BUILD
|
PRE_BUILD
|
||||||
OUTPUT ${headerfile}
|
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}
|
DEPENDS ${source}
|
||||||
)
|
)
|
||||||
add_executable(${testname} ${source} test_renderer.c ${headerfile})
|
add_executable(${testname} ${source} test_renderer.c ${headerfile})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user