mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-27 10:50:23 -04:00
190 lines
5.4 KiB
Go
190 lines
5.4 KiB
Go
package tracker
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"github.com/vsariola/sointu"
|
|
"github.com/vsariola/sointu/vm"
|
|
)
|
|
|
|
func (m *Model) ReadSong(r io.ReadCloser) {
|
|
b, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = r.Close()
|
|
if err != nil {
|
|
return
|
|
}
|
|
var song sointu.Song
|
|
if errJSON := json.Unmarshal(b, &song); errJSON != nil {
|
|
if errYaml := yaml.Unmarshal(b, &song); errYaml != nil {
|
|
m.Alerts().Add(fmt.Sprintf("Error unmarshaling a song file: %v / %v", errYaml, errJSON), Error)
|
|
return
|
|
}
|
|
}
|
|
f := m.change("LoadSong", SongChange, MajorChange)
|
|
m.d.Song = song
|
|
if f, ok := r.(*os.File); ok {
|
|
m.d.FilePath = f.Name()
|
|
// when the song is loaded from a file, we are quite confident that the file is persisted and thus
|
|
// we can close sointu without worrying about losing changes
|
|
m.d.ChangedSinceSave = false
|
|
}
|
|
f()
|
|
m.completeAction(false)
|
|
}
|
|
|
|
func (m *Model) WriteSong(w io.WriteCloser) {
|
|
path := ""
|
|
var extension = filepath.Ext(path)
|
|
var contents []byte
|
|
var err error
|
|
if extension == ".json" {
|
|
contents, err = json.Marshal(m.d.Song)
|
|
} else {
|
|
contents, err = yaml.Marshal(m.d.Song)
|
|
}
|
|
if err != nil {
|
|
m.Alerts().Add(fmt.Sprintf("Error marshaling a song file: %v", err), Error)
|
|
return
|
|
}
|
|
if _, err := w.Write(contents); err != nil {
|
|
m.Alerts().Add(fmt.Sprintf("Error writing to file: %v", err), Error)
|
|
return
|
|
}
|
|
if f, ok := w.(*os.File); ok {
|
|
path = f.Name()
|
|
// when the song is saved to a file, we are quite confident that the file is persisted and thus
|
|
// we can close sointu without worrying about losing changes
|
|
m.d.ChangedSinceSave = false
|
|
}
|
|
if err := w.Close(); err != nil {
|
|
m.Alerts().Add(fmt.Sprintf("Error rendering the song during export: %v", err), Error)
|
|
return
|
|
}
|
|
m.d.FilePath = path
|
|
m.completeAction(false)
|
|
}
|
|
|
|
func (m *Model) WriteWav(w io.WriteCloser, pcm16 bool) {
|
|
m.dialog = NoDialog
|
|
song := m.d.Song.Copy()
|
|
go func() {
|
|
b := make([]byte, 32+2)
|
|
rand.Read(b)
|
|
name := fmt.Sprintf("%x", b)[2 : 32+2]
|
|
data, err := sointu.Play(m.synther, song, func(p float32) {
|
|
txt := fmt.Sprintf("Exporting song: %.0f%%", p*100)
|
|
TrySend(m.broker.ToModel, MsgToModel{Data: Alert{Message: txt, Priority: Info, Name: name, Duration: defaultAlertDuration}})
|
|
}) // render the song to calculate its length
|
|
if err != nil {
|
|
txt := fmt.Sprintf("Error rendering the song during export: %v", err)
|
|
TrySend(m.broker.ToModel, MsgToModel{Data: Alert{Message: txt, Priority: Error, Name: name, Duration: defaultAlertDuration}})
|
|
return
|
|
}
|
|
buffer, err := data.Wav(pcm16)
|
|
if err != nil {
|
|
txt := fmt.Sprintf("Error converting to .wav: %v", err)
|
|
TrySend(m.broker.ToModel, MsgToModel{Data: Alert{Message: txt, Priority: Error, Name: name, Duration: defaultAlertDuration}})
|
|
return
|
|
}
|
|
w.Write(buffer)
|
|
w.Close()
|
|
}()
|
|
}
|
|
|
|
func (m *Model) SaveInstrument(w io.WriteCloser) bool {
|
|
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
|
m.Alerts().Add("No instrument selected", Error)
|
|
return false
|
|
}
|
|
path := ""
|
|
if f, ok := w.(*os.File); ok {
|
|
path = f.Name()
|
|
}
|
|
var extension = filepath.Ext(path)
|
|
var contents []byte
|
|
var err error
|
|
if extension == ".json" {
|
|
contents, err = json.Marshal(m.d.Song.Patch[m.d.InstrIndex])
|
|
} else {
|
|
contents, err = yaml.Marshal(m.d.Song.Patch[m.d.InstrIndex])
|
|
}
|
|
if err != nil {
|
|
m.Alerts().Add(fmt.Sprintf("Error marshaling a ínstrument file: %v", err), Error)
|
|
return false
|
|
}
|
|
w.Write(contents)
|
|
w.Close()
|
|
return true
|
|
}
|
|
|
|
func (m *Model) LoadInstrument(r io.ReadCloser) bool {
|
|
if m.d.InstrIndex < 0 {
|
|
return false
|
|
}
|
|
b, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
r.Close() // if we can't close the file, it's not a big deal, so ignore the error
|
|
var instrument sointu.Instrument
|
|
var errJSON, errYaml, err4ki, err4kp error
|
|
var patch sointu.Patch
|
|
errJSON = json.Unmarshal(b, &instrument)
|
|
if errJSON == nil {
|
|
goto success
|
|
}
|
|
errYaml = yaml.Unmarshal(b, &instrument)
|
|
if errYaml == nil {
|
|
goto success
|
|
}
|
|
patch, err4kp = sointu.Read4klangPatch(bytes.NewReader(b))
|
|
if err4kp == nil {
|
|
defer m.change("LoadInstrument", PatchChange, MajorChange)()
|
|
m.d.Song.Patch = patch
|
|
return true
|
|
}
|
|
instrument, err4ki = sointu.Read4klangInstrument(bytes.NewReader(b))
|
|
if err4ki == nil {
|
|
goto success
|
|
}
|
|
m.Alerts().Add(fmt.Sprintf("Error unmarshaling an instrument file: %v / %v / %v / %v", errYaml, errJSON, err4ki, err4kp), Error)
|
|
return false
|
|
success:
|
|
if f, ok := r.(*os.File); ok {
|
|
filename := f.Name()
|
|
// the 4klang instrument names are junk, replace them with the filename without extension
|
|
instrument.Name = filepath.Base(filename[:len(filename)-len(filepath.Ext(filename))])
|
|
}
|
|
defer m.change("LoadInstrument", PatchChange, MajorChange)()
|
|
for len(m.d.Song.Patch) <= m.d.InstrIndex {
|
|
m.d.Song.Patch = append(m.d.Song.Patch, defaultInstrument.Copy())
|
|
}
|
|
m.d.Song.Patch[m.d.InstrIndex] = sointu.Instrument{}
|
|
numVoices := m.d.Song.Patch.NumVoices()
|
|
if numVoices >= vm.MAX_VOICES {
|
|
// this really shouldn't happen, as we have already cleared the
|
|
// instrument and assuming each instrument has at least 1 voice, it
|
|
// should have freed up some voices
|
|
m.Alerts().Add(fmt.Sprintf("The patch has already %d voices", vm.MAX_VOICES), Error)
|
|
return false
|
|
}
|
|
instrument.NumVoices = clamp(instrument.NumVoices, 1, 32-numVoices)
|
|
m.assignUnitIDs(instrument.Units)
|
|
m.d.Song.Patch[m.d.InstrIndex] = instrument
|
|
if m.d.Song.Patch[m.d.InstrIndex].Comment != "" {
|
|
m.commentExpanded = true
|
|
}
|
|
return true
|
|
}
|