diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fee39f1 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index ed36735..2a669fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,15 +35,16 @@ enable_language(ASM_NASM) set(CMAKE_ASM_NASM_COMPILE_OBJECT " -f ${CMAKE_ASM_NASM_OBJECT_FORMAT} -o ") 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}) diff --git a/cmd/sointu-cli/main.go b/cmd/sointu-cli/main.go deleted file mode 100644 index 495c19d..0000000 --- a/cmd/sointu-cli/main.go +++ /dev/null @@ -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() -} diff --git a/cmd/sointu-compile/main.go b/cmd/sointu-compile/main.go new file mode 100644 index 0000000..42cca8b --- /dev/null +++ b/cmd/sointu-compile/main.go @@ -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() +} diff --git a/cmd/sointu-play/main.go b/cmd/sointu-play/main.go new file mode 100644 index 0000000..4f19779 --- /dev/null +++ b/cmd/sointu-play/main.go @@ -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 +} diff --git a/cmd/sointu-tracker/main.go b/cmd/sointu-track/main.go similarity index 100% rename from cmd/sointu-tracker/main.go rename to cmd/sointu-track/main.go diff --git a/compiler/compiler.go b/compiler/compiler.go index 2ecc701..0fe06ed 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -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 } diff --git a/compiler/generate.go b/compiler/generate.go deleted file mode 100644 index 4c34cc3..0000000 --- a/compiler/generate.go +++ /dev/null @@ -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() -} diff --git a/compiler/macros.go b/compiler/macros.go index a7e572e..068a671 100644 --- a/compiler/macros.go +++ b/compiler/macros.go @@ -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} diff --git a/sointu.go b/sointu.go index 92fef70..fcfda7e 100644 --- a/sointu.go +++ b/sointu.go @@ -10,7 +10,7 @@ import ( type Unit struct { Type string Parameters map[string]int `yaml:",flow"` - VarArgs []int + VarArgs []int `,omitempty` } const ( diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 468f1f8..7800f90 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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)