mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
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.
180 lines
4.6 KiB
Go
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
|
|
}
|