sointu/cmd/sointu-play/main.go
vsariola 99dbdfe223 feat: add the ability to use Sointu as a sync-tracker
There is a new "sync" opcode that saves the top-most signal every 256 samples to the new "syncBuffer" output. Additionally, you can enable saving the current fractional row as sync[0], avoiding calculating the beat in the shader, but also calculating the beat correctly when the beat is modulated.
2021-03-09 23:52:33 +02:00

234 lines
7.6 KiB
Go

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/oto"
"github.com/vsariola/sointu/vm/compiler/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).")
unreleased := flag.Bool("u", false, "Start song with all oscillators unreleased.")
//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
}
var audioContext sointu.AudioContext
if *play {
var err error
audioContext, err = oto.NewContext()
if err != nil {
fmt.Fprintf(os.Stderr, "could not acquire oto AudioContext: %v\n", err)
os.Exit(1)
}
defer audioContext.Close()
}
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)
}
if !*unreleased {
for i := 0; i < 32; i++ {
synth.Release(i)
}
}
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 {
output := audioContext.Output()
defer output.Close()
if err := output.WriteAudio(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
}