sointu/tracker/files.go
5684185+vsariola@users.noreply.github.com d92426a100 feat!: rewrote the GUI and model for better testability
The Model was getting unmaintanable mess. This is an attempt to refactor/rewrite the Model so that data of certain type is exposed in standardized way, offering certain standard manipulations for that data type, and on the GUI side, certain standard widgets to tied to that data.

This rewrite closes #72, #106 and #120.
2024-02-17 18:16:06 +02:00

180 lines
4.6 KiB
Go

package tracker
import (
"bytes"
"crypto/rand"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
"github.com/vsariola/sointu"
)
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, execChan chan<- func()) {
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) {
execChan <- func() {
m.Alerts().AddNamed(name, fmt.Sprintf("Exporting song: %.0f%%", p*100), Info)
}
}) // render the song to calculate its length
if err != nil {
execChan <- func() {
m.Alerts().Add(fmt.Sprintf("Error rendering the song during export: %v", err), Error)
}
return
}
buffer, err := data.Wav(pcm16)
if err != nil {
execChan <- func() {
m.Alerts().Add(fmt.Sprintf("Error converting to .wav: %v", err), Error)
}
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
}
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] = instrument
if m.d.Song.Patch[m.d.InstrIndex].Comment != "" {
m.commentExpanded = true
}
return true
}