Files
sointu/tracker/instrument.go
2026-01-27 22:16:14 +02:00

484 lines
16 KiB
Go

package tracker
import (
"bytes"
"encoding/json"
"fmt"
"io"
"math"
"os"
"path/filepath"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/vm"
"gopkg.in/yaml.v3"
)
// Instrument returns the Instrument view of the model, containing methods to
// manipulate the instruments.
func (m *Model) Instrument() *InstrModel { return (*InstrModel)(m) }
type InstrModel Model
// Add returns an Action to add a new instrument.
func (m *InstrModel) Add() Action { return MakeAction((*addInstrument)(m)) }
type addInstrument InstrModel
func (m *addInstrument) Enabled() bool { return (*Model)(m).d.Song.Patch.NumVoices() < vm.MAX_VOICES }
func (m *addInstrument) Do() {
defer (*Model)(m).change("AddInstrument", SongChange, MajorChange)()
voiceIndex := m.d.Song.Patch.FirstVoiceForInstrument(m.d.InstrIndex)
p := sointu.Patch{defaultInstrument.Copy()}
t := []sointu.Track{{NumVoices: 1}}
_, _, ok := (*Model)(m).addVoices(voiceIndex, p, t, true, (*Model)(m).linkInstrTrack)
m.changeCancel = !ok
}
// Delete returns an Action to delete the currently selected instrument(s).
func (m *InstrModel) Delete() Action { return MakeAction((*deleteInstrument)(m)) }
type deleteInstrument InstrModel
func (m *deleteInstrument) Enabled() bool { return len((*Model)(m).d.Song.Patch) > 0 }
func (m *deleteInstrument) Do() { (*Model)(m).Instrument().List().DeleteElements(false) }
// Split returns an Action to split the currently selected instrument, dividing
// the voices as evenly as possible.
func (m *InstrModel) Split() Action { return MakeAction((*splitInstrument)(m)) }
type splitInstrument InstrModel
func (m *splitInstrument) Enabled() bool {
return m.d.InstrIndex >= 0 && m.d.InstrIndex < len(m.d.Song.Patch) && m.d.Song.Patch[m.d.InstrIndex].NumVoices > 1
}
func (m *splitInstrument) Do() {
defer (*Model)(m).change("SplitInstrument", SongChange, MajorChange)()
voiceIndex := m.d.Song.Patch.Copy().FirstVoiceForInstrument(m.d.InstrIndex)
middle := voiceIndex + (m.d.Song.Patch[m.d.InstrIndex].NumVoices+1)/2
end := voiceIndex + m.d.Song.Patch[m.d.InstrIndex].NumVoices
left, ok := VoiceSlice(m.d.Song.Patch, Range{math.MinInt, middle})
if !ok {
m.changeCancel = true
return
}
right, ok := VoiceSlice(m.d.Song.Patch, Range{end, math.MaxInt})
if !ok {
m.changeCancel = true
return
}
newInstrument := defaultInstrument.Copy()
(*Model)(m).assignUnitIDs(newInstrument.Units)
newInstrument.NumVoices = end - middle
m.d.Song.Patch = append(left, newInstrument)
m.d.Song.Patch = append(m.d.Song.Patch, right...)
}
// Item returns information about the instrument at a given index.
func (v *InstrModel) Item(i int) (name string, maxLevel float32, mute bool, ok bool) {
if i < 0 || i >= len(v.d.Song.Patch) {
return "", 0, false, false
}
name = v.d.Song.Patch[i].Name
mute = v.d.Song.Patch[i].Mute
start := v.d.Song.Patch.FirstVoiceForInstrument(i)
end := start + v.d.Song.Patch[i].NumVoices
if end >= vm.MAX_VOICES {
end = vm.MAX_VOICES
}
if start < end {
for _, level := range v.playerStatus.VoiceLevels[start:end] {
if maxLevel < level {
maxLevel = level
}
}
}
ok = true
return
}
// Tab returns an Int representing the currently selected instrument tab.
func (m *InstrModel) Tab() Int { return MakeInt((*instrumentTab)(m)) }
type instrumentTab InstrModel
func (v *instrumentTab) Value() int { return int(v.d.InstrumentTab) }
func (v *instrumentTab) Range() RangeInclusive { return RangeInclusive{0, int(NumInstrumentTabs) - 1} }
func (v *instrumentTab) SetValue(value int) bool {
v.d.InstrumentTab = InstrumentTab(value)
return true
}
// List returns a List of all the instruments in the patch, implementing
// ListData and MutableListData interfaces.
func (m *InstrModel) List() List { return List{(*instrumentList)(m)} }
type instrumentList InstrModel
func (v *instrumentList) Count() int { return len(v.d.Song.Patch) }
func (v *instrumentList) Selected() int { return v.d.InstrIndex }
func (v *instrumentList) Selected2() int { return v.d.InstrIndex2 }
func (v *instrumentList) SetSelected2(value int) { v.d.InstrIndex2 = value }
func (v *instrumentList) SetSelected(value int) {
v.d.InstrIndex = value
v.d.UnitIndex = 0
v.d.UnitIndex2 = 0
v.d.UnitSearching = false
v.d.UnitSearchString = ""
}
func (v *instrumentList) Move(r Range, delta int) (ok bool) {
voiceDelta := 0
if delta < 0 {
voiceDelta = -VoiceRange(v.d.Song.Patch, Range{r.Start + delta, r.Start}).Len()
} else if delta > 0 {
voiceDelta = VoiceRange(v.d.Song.Patch, Range{r.End, r.End + delta}).Len()
}
if voiceDelta == 0 {
return false
}
ranges := MakeMoveRanges(VoiceRange(v.d.Song.Patch, r), voiceDelta)
return (*Model)(v).sliceInstrumentsTracks(true, v.linkInstrTrack, ranges[:]...)
}
func (v *instrumentList) Delete(r Range) (ok bool) {
ranges := Complement(VoiceRange(v.d.Song.Patch, r))
return (*Model)(v).sliceInstrumentsTracks(true, v.linkInstrTrack, ranges[:]...)
}
func (v *instrumentList) Change(n string, severity ChangeSeverity) func() {
return (*Model)(v).change("Instruments."+n, SongChange, severity)
}
func (v *instrumentList) Cancel() {
v.changeCancel = true
}
func (v *instrumentList) Marshal(r Range) ([]byte, error) {
return (*Model)(v).marshalVoices(VoiceRange(v.d.Song.Patch, r))
}
func (m *instrumentList) Unmarshal(data []byte) (r Range, err error) {
voiceIndex := m.d.Song.Patch.FirstVoiceForInstrument(m.d.InstrIndex)
r, _, ok := (*Model)(m).unmarshalVoices(voiceIndex, data, true, m.linkInstrTrack)
if !ok {
return Range{}, fmt.Errorf("unmarshal: unmarshalVoices failed")
}
return r, nil
}
// Thread methods
type (
instrumentThread1 Model
instrumentThread2 Model
instrumentThread3 Model
instrumentThread4 Model
)
func (m *InstrModel) Thread1() Bool { return MakeBool((*instrumentThread1)(m)) }
func (m *instrumentThread1) Value() bool { return (*InstrModel)(m).getThreadsBit(0) }
func (m *instrumentThread1) SetValue(val bool) { (*InstrModel)(m).setThreadsBit(0, val) }
func (m *InstrModel) Thread2() Bool { return MakeBool((*instrumentThread2)(m)) }
func (m *instrumentThread2) Value() bool { return (*InstrModel)(m).getThreadsBit(1) }
func (m *instrumentThread2) SetValue(val bool) { (*InstrModel)(m).setThreadsBit(1, val) }
func (m *InstrModel) Thread3() Bool { return MakeBool((*instrumentThread3)(m)) }
func (m *instrumentThread3) Value() bool { return (*InstrModel)(m).getThreadsBit(2) }
func (m *instrumentThread3) SetValue(val bool) { (*InstrModel)(m).setThreadsBit(2, val) }
func (m *InstrModel) Thread4() Bool { return MakeBool((*instrumentThread4)(m)) }
func (m *instrumentThread4) Value() bool { return (*InstrModel)(m).getThreadsBit(3) }
func (m *instrumentThread4) SetValue(val bool) { (*InstrModel)(m).setThreadsBit(3, val) }
func (m *InstrModel) getThreadsBit(bit int) bool {
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
return false
}
mask := m.d.Song.Patch[m.d.InstrIndex].ThreadMaskM1 + 1
return mask&(1<<bit) != 0
}
func (m *InstrModel) setThreadsBit(bit int, value bool) {
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
return
}
mask := m.d.Song.Patch[m.d.InstrIndex].ThreadMaskM1 + 1
if value {
mask |= (1 << bit)
} else {
mask &^= (1 << bit)
}
defer (*Model)(m).change("ThreadBitMask", PatchChange, MinorChange)()
m.d.Song.Patch[m.d.InstrIndex].ThreadMaskM1 = max(mask-1, -1) // -1 has all threads disabled, we warn about that
m.warnAboutCrossThreadSends()
m.warnNoMultithreadSupport()
m.warnNoThread()
}
func (m *InstrModel) warnAboutCrossThreadSends() {
for i, instr := range m.d.Song.Patch {
for _, unit := range instr.Units {
if unit.Type == "send" {
targetID, ok := unit.Parameters["target"]
if !ok {
continue
}
it, _, err := m.d.Song.Patch.FindUnit(targetID)
if err != nil {
continue
}
if instr.ThreadMaskM1 != m.d.Song.Patch[it].ThreadMaskM1 {
(*Alerts)(m).AddNamed("CrossThreadSend", fmt.Sprintf("Instrument %d '%s' has a send to instrument %d '%s' but they are not on the same threads, which may cause issues", i+1, instr.Name, it+1, m.d.Song.Patch[it].Name), Warning)
return
}
}
}
}
(*Alerts)(m).ClearNamed("CrossThreadSend")
}
func (m *InstrModel) warnNoMultithreadSupport() {
for _, instr := range m.d.Song.Patch {
if instr.ThreadMaskM1 > 0 && !m.synthers[m.syntherIndex].SupportsMultithreading() {
(*Alerts)(m).AddNamed("NoMultithreadSupport", "The current synth does not support multithreading and the patch was configured to use more than one thread", Warning)
return
}
}
(*Alerts)(m).ClearNamed("NoMultithreadSupport")
}
func (m *InstrModel) warnNoThread() {
for i, instr := range m.d.Song.Patch {
if instr.ThreadMaskM1 == -1 {
(*Alerts)(m).AddNamed("NoThread", fmt.Sprintf("Instrument %d '%s' is not rendered on any thread", i+1, instr.Name), Warning)
return
}
}
(*Alerts)(m).ClearNamed("NoThread")
}
// Mute returns a Bool for muting/unmuting the currently selected instrument(s).
func (m *InstrModel) Mute() Bool { return MakeBool((*muteInstrument)(m)) }
type muteInstrument Model
func (m *muteInstrument) Value() bool {
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
return false
}
return m.d.Song.Patch[m.d.InstrIndex].Mute
}
func (m *muteInstrument) SetValue(val bool) {
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
return
}
defer (*Model)(m).change("Mute", PatchChange, MinorChange)()
a, b := min(m.d.InstrIndex, m.d.InstrIndex2), max(m.d.InstrIndex, m.d.InstrIndex2)
for i := a; i <= b; i++ {
if i < 0 || i >= len(m.d.Song.Patch) {
continue
}
m.d.Song.Patch[i].Mute = val
}
}
func (m *muteInstrument) Enabled() bool {
return m.d.InstrIndex >= 0 && m.d.InstrIndex < len(m.d.Song.Patch)
}
// Solo returns a Bool for soloing/unsoloing the currently selected instrument(s).
func (m *InstrModel) Solo() Bool { return MakeBool((*soloInstrument)(m)) }
type soloInstrument Model
func (m *soloInstrument) Value() bool {
a, b := min(m.d.InstrIndex, m.d.InstrIndex2), max(m.d.InstrIndex, m.d.InstrIndex2)
for i := range m.d.Song.Patch {
if i < 0 || i >= len(m.d.Song.Patch) {
continue
}
if (i >= a && i <= b) == m.d.Song.Patch[i].Mute {
return false
}
}
return true
}
func (m *soloInstrument) SetValue(val bool) {
defer (*Model)(m).change("Solo", PatchChange, MinorChange)()
a, b := min(m.d.InstrIndex, m.d.InstrIndex2), max(m.d.InstrIndex, m.d.InstrIndex2)
for i := range m.d.Song.Patch {
if i < 0 || i >= len(m.d.Song.Patch) {
continue
}
m.d.Song.Patch[i].Mute = !(i >= a && i <= b) && val
}
}
func (m *soloInstrument) Enabled() bool {
return m.d.InstrIndex >= 0 && m.d.InstrIndex < len(m.d.Song.Patch)
}
// Name returns a String representing the name of the currently selected
// instrument.
func (m *InstrModel) Name() String { return MakeString((*instrumentName)(m)) }
type instrumentName InstrModel
func (v *instrumentName) Value() string {
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
return ""
}
return v.d.Song.Patch[v.d.InstrIndex].Name
}
func (v *instrumentName) SetValue(value string) bool {
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
return false
}
defer (*Model)(v).change("InstrumentNameString", PatchChange, MinorChange)()
v.d.Song.Patch[v.d.InstrIndex].Name = value
return true
}
// Comment returns a String representing the comment of the currently selected
// instrument.
func (m *InstrModel) Comment() String { return MakeString((*instrumentComment)(m)) }
type instrumentComment InstrModel
func (v *instrumentComment) Value() string {
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
return ""
}
return v.d.Song.Patch[v.d.InstrIndex].Comment
}
func (v *instrumentComment) SetValue(value string) bool {
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
return false
}
defer (*Model)(v).change("InstrumentComment", PatchChange, MinorChange)()
v.d.Song.Patch[v.d.InstrIndex].Comment = value
return true
}
// Voices returns an Int representing the number of voices for the currently
// selected instrument.
func (m *InstrModel) Voices() Int { return MakeInt((*instrumentVoices)(m)) }
type instrumentVoices InstrModel
func (v *instrumentVoices) Value() int {
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
return 1
}
return max(v.d.Song.Patch[v.d.InstrIndex].NumVoices, 1)
}
func (m *instrumentVoices) SetValue(value int) bool {
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
return false
}
defer (*Model)(m).change("InstrumentVoices", SongChange, MinorChange)()
voiceIndex := m.d.Song.Patch.FirstVoiceForInstrument(m.d.InstrIndex)
voiceRange := Range{voiceIndex, voiceIndex + m.d.Song.Patch[m.d.InstrIndex].NumVoices}
ranges := MakeSetLength(voiceRange, value)
ok := (*Model)(m).sliceInstrumentsTracks(true, m.linkInstrTrack, ranges...)
if !ok {
m.changeCancel = true
}
return ok
}
func (v *instrumentVoices) Range() RangeInclusive {
return RangeInclusive{1, (*Model)(v).remainingVoices(true, v.linkInstrTrack) + v.Value()}
}
// Write writes the currently selected instrument to the given io.WriteCloser.
// If the WriteCloser is a file, the file extension is used to determine the
// format (.json for JSON, anything else for YAML).
func (m *InstrModel) Write(w io.WriteCloser) bool {
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
(*Model)(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
instr := m.d.Song.Patch[m.d.InstrIndex]
if _, ok := w.(*os.File); ok {
instr.Name = "" // don't save the instrument name to a file; we'll replace the instruments name with the filename when loading from a file
}
if extension == ".json" {
contents, err = json.Marshal(instr)
} else {
contents, err = yaml.Marshal(instr)
}
if err != nil {
(*Model)(m).Alerts().Add(fmt.Sprintf("Error marshaling an instrument file: %v", err), Error)
return false
}
w.Write(contents)
w.Close()
return true
}
// Read reads an instrument from the given io.ReadCloser and sets it as the
// currently selected instrument. The format is determined by trying JSON first, then
// YAML, then 4klang Patch, then 4klang Instrument.
func (m *InstrModel) Read(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 (*Model)(m).change("LoadInstrument", PatchChange, MajorChange)()
m.d.Song.Patch = patch
return true
}
instrument, err4ki = sointu.Read4klangInstrument(bytes.NewReader(b))
if err4ki == nil {
goto success
}
(*Model)(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 instrument names are generally junk, replace them with the filename without extension
instrument.Name = filepath.Base(filename[:len(filename)-len(filepath.Ext(filename))])
}
defer (*Model)(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
(*Model)(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)
(*Model)(m).assignUnitIDs(instrument.Units)
m.d.Song.Patch[m.d.InstrIndex] = instrument
return true
}