|
|
@@ -217,63 +217,99 @@ func (s *Service) About() About {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// state builds what the frontend shows about the project. Callers hold s.mu.
|
|
|
func (s *Service) state() State {
|
|
|
- p := s.p
|
|
|
+ proj := s.p
|
|
|
+ missing := missingSources(proj)
|
|
|
st := State{
|
|
|
- Name: p.Name,
|
|
|
- Dirty: p.Dirty(),
|
|
|
- Pads: make([]Pad, 0, project.Pads),
|
|
|
- Sequences: []Sequence{},
|
|
|
- SampleCount: p.SampleCount(),
|
|
|
- Limits: Limits{
|
|
|
- Pads: project.Pads,
|
|
|
- MaxSamples: project.MaxSamples,
|
|
|
- MaxSequences: project.MaxSequences,
|
|
|
- MinCoarse: project.MinCoarse,
|
|
|
- MaxCoarse: project.MaxCoarse,
|
|
|
- MinFine: project.MinFine,
|
|
|
- MaxFine: project.MaxFine,
|
|
|
- MaxMuteGroup: project.MaxMuteGroup,
|
|
|
- MinBPM: project.MinBPM,
|
|
|
- MaxBPM: project.MaxBPM,
|
|
|
- MaxBars: project.MaxBars,
|
|
|
- },
|
|
|
- }
|
|
|
- missing := map[string]bool{} // pool file -> source missing, as export sees it
|
|
|
- for _, f := range p.PoolFiles() {
|
|
|
- src, _ := p.Source(f)
|
|
|
- missing[f] = src == "" || !export.Usable(src)
|
|
|
+ Name: proj.Name,
|
|
|
+ Dirty: proj.Dirty(),
|
|
|
+ Pads: make([]Pad, 0, project.Pads),
|
|
|
+ Sequences: sequenceStates(proj),
|
|
|
+ SampleCount: proj.SampleCount(),
|
|
|
+ MissingCount: len(missing),
|
|
|
+ Limits: limits(),
|
|
|
}
|
|
|
for slot := 0; slot < project.Pads; slot++ {
|
|
|
- v, err := p.Pad(slot)
|
|
|
- if err != nil {
|
|
|
- // A loaded file with an odd pad; show it as empty.
|
|
|
- st.Pads = append(st.Pads, Pad{Slot: slot, Label: project.SlotName(slot)})
|
|
|
- continue
|
|
|
- }
|
|
|
- pad := Pad{Slot: slot, Label: project.SlotName(slot), Name: v.SampleName, File: v.File,
|
|
|
- Coarse: v.Coarse, Fine: v.Fine, MuteGroup: v.MuteGroup, Start: v.Start, End: v.End}
|
|
|
- if !v.Empty() {
|
|
|
- pad.HasNotes = p.PadHasNotes(slot)
|
|
|
- pad.Source, _ = p.Source(v.File)
|
|
|
- pad.Missing = missing[v.File]
|
|
|
- if !pad.Missing {
|
|
|
- pad.Audio = audioURL(slot, pad.Source)
|
|
|
- }
|
|
|
- }
|
|
|
- st.Pads = append(st.Pads, pad)
|
|
|
- }
|
|
|
- for _, q := range p.Sequences() {
|
|
|
- st.Sequences = append(st.Sequences, Sequence{Slot: q.Slot, Label: project.SlotName(q.Slot),
|
|
|
- Name: q.Name, BPM: q.BPM, Bars: q.Bars, HasNotes: q.HasNotes})
|
|
|
+ st.Pads = append(st.Pads, padState(proj, slot, missing))
|
|
|
}
|
|
|
st.SequenceCount = len(st.Sequences)
|
|
|
- for _, m := range missing {
|
|
|
- if m {
|
|
|
- st.MissingCount++
|
|
|
+ return st
|
|
|
+}
|
|
|
+
|
|
|
+// limits returns the device and format limits the frontend enforces.
|
|
|
+func limits() Limits {
|
|
|
+ return Limits{
|
|
|
+ Pads: project.Pads,
|
|
|
+ MaxSamples: project.MaxSamples,
|
|
|
+ MaxSequences: project.MaxSequences,
|
|
|
+ MinCoarse: project.MinCoarse,
|
|
|
+ MaxCoarse: project.MaxCoarse,
|
|
|
+ MinFine: project.MinFine,
|
|
|
+ MaxFine: project.MaxFine,
|
|
|
+ MaxMuteGroup: project.MaxMuteGroup,
|
|
|
+ MinBPM: project.MinBPM,
|
|
|
+ MaxBPM: project.MaxBPM,
|
|
|
+ MaxBars: project.MaxBars,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// missingSources returns the pool files that export could not copy: no
|
|
|
+// source recorded, or one export refuses (see export.Usable).
|
|
|
+func missingSources(proj *project.Project) map[string]bool {
|
|
|
+ missing := map[string]bool{}
|
|
|
+ for _, file := range proj.PoolFiles() {
|
|
|
+ if src, _ := proj.Source(file); src == "" || !export.Usable(src) {
|
|
|
+ missing[file] = true
|
|
|
}
|
|
|
}
|
|
|
- return st
|
|
|
+ return missing
|
|
|
+}
|
|
|
+
|
|
|
+// padState describes the pad in slot. missing is from missingSources.
|
|
|
+func padState(proj *project.Project, slot int, missing map[string]bool) Pad {
|
|
|
+ view, err := proj.Pad(slot)
|
|
|
+ if err != nil {
|
|
|
+ // A loaded file with an odd pad; show it as empty.
|
|
|
+ return Pad{Slot: slot, Label: project.SlotName(slot)}
|
|
|
+ }
|
|
|
+ pad := Pad{
|
|
|
+ Slot: slot,
|
|
|
+ Label: project.SlotName(slot),
|
|
|
+ Name: view.SampleName,
|
|
|
+ File: view.File,
|
|
|
+ Coarse: view.Coarse,
|
|
|
+ Fine: view.Fine,
|
|
|
+ MuteGroup: view.MuteGroup,
|
|
|
+ Start: view.Start,
|
|
|
+ End: view.End,
|
|
|
+ }
|
|
|
+ if view.Empty() {
|
|
|
+ return pad
|
|
|
+ }
|
|
|
+ pad.HasNotes = proj.PadHasNotes(slot)
|
|
|
+ pad.Source, _ = proj.Source(view.File)
|
|
|
+ pad.Missing = missing[view.File]
|
|
|
+ if !pad.Missing {
|
|
|
+ pad.Audio = audioURL(slot, pad.Source)
|
|
|
+ }
|
|
|
+ return pad
|
|
|
+}
|
|
|
+
|
|
|
+// sequenceStates describes the project's sequences, in slot order.
|
|
|
+func sequenceStates(proj *project.Project) []Sequence {
|
|
|
+ seqs := []Sequence{}
|
|
|
+ for _, seq := range proj.Sequences() {
|
|
|
+ seqs = append(seqs, Sequence{
|
|
|
+ Slot: seq.Slot,
|
|
|
+ Label: project.SlotName(seq.Slot),
|
|
|
+ Name: seq.Name,
|
|
|
+ BPM: seq.BPM,
|
|
|
+ Bars: seq.Bars,
|
|
|
+ HasNotes: seq.HasNotes,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return seqs
|
|
|
}
|
|
|
|
|
|
// audioURL is the preview URL for a pad. The key in the query changes with
|
|
|
@@ -283,9 +319,9 @@ func audioURL(slot int, src string) string {
|
|
|
return fmt.Sprintf("%s%d?k=%s", AudioPrefix, slot, hex.EncodeToString(sum[:6]))
|
|
|
}
|
|
|
|
|
|
-// edit runs fn on the project under the lock and returns the new state.
|
|
|
+// apply runs fn on the project under the lock and returns the new state.
|
|
|
// The state is returned even on error, so the frontend stays in step.
|
|
|
-func (s *Service) edit(fn func(p *project.Project) error) (State, error) {
|
|
|
+func (s *Service) apply(fn func(p *project.Project) error) (State, error) {
|
|
|
s.mu.Lock()
|
|
|
defer s.mu.Unlock()
|
|
|
err := fn(s.p)
|
|
|
@@ -294,7 +330,7 @@ func (s *Service) edit(fn func(p *project.Project) error) (State, error) {
|
|
|
|
|
|
// SetName renames the project.
|
|
|
func (s *Service) SetName(name string) (State, error) {
|
|
|
- return s.edit(func(p *project.Project) error { return p.SetName(strings.TrimSpace(name)) })
|
|
|
+ return s.apply(func(p *project.Project) error { return p.SetName(strings.TrimSpace(name)) })
|
|
|
}
|
|
|
|
|
|
// AssignSampleDialog asks for a WAV file and puts it on the pad in slot.
|
|
|
@@ -308,22 +344,22 @@ func (s *Service) AssignSampleDialog(slot int) (State, error) {
|
|
|
}
|
|
|
|
|
|
func (s *Service) assignSample(slot int, path string) (State, error) {
|
|
|
- return s.edit(func(p *project.Project) error { return p.AssignSample(slot, path) })
|
|
|
+ return s.apply(func(p *project.Project) error { return p.AssignSample(slot, path) })
|
|
|
}
|
|
|
|
|
|
// ClearPad empties the pad in slot.
|
|
|
func (s *Service) ClearPad(slot int) (State, error) {
|
|
|
- return s.edit(func(p *project.Project) error { return p.ClearPad(slot) })
|
|
|
+ return s.apply(func(p *project.Project) error { return p.ClearPad(slot) })
|
|
|
}
|
|
|
|
|
|
// SetTune sets a pad's tuning in semitones and cents.
|
|
|
func (s *Service) SetTune(slot, coarse, fine int) (State, error) {
|
|
|
- return s.edit(func(p *project.Project) error { return p.SetTune(slot, coarse, fine) })
|
|
|
+ return s.apply(func(p *project.Project) error { return p.SetTune(slot, coarse, fine) })
|
|
|
}
|
|
|
|
|
|
// SetMuteGroup sets a pad's mute group; 0 is off.
|
|
|
func (s *Service) SetMuteGroup(slot, group int) (State, error) {
|
|
|
- return s.edit(func(p *project.Project) error { return p.SetMuteGroup(slot, group) })
|
|
|
+ return s.apply(func(p *project.Project) error { return p.SetMuteGroup(slot, group) })
|
|
|
}
|
|
|
|
|
|
// ChooseExportFolder asks for the folder to export into and remembers it
|
|
|
@@ -412,17 +448,17 @@ func (s *Service) exported(path string, err error) (ExportResult, error) {
|
|
|
// MovePad moves the pad in from to slot to, replacing what was there.
|
|
|
// Recorded notes stay on their slots; the frontend warns first.
|
|
|
func (s *Service) MovePad(from, to int) (State, error) {
|
|
|
- return s.edit(func(p *project.Project) error { return p.MovePad(from, to) })
|
|
|
+ return s.apply(func(p *project.Project) error { return p.MovePad(from, to) })
|
|
|
}
|
|
|
|
|
|
// CopyPad copies the pad in from to slot to, replacing what was there.
|
|
|
func (s *Service) CopyPad(from, to int) (State, error) {
|
|
|
- return s.edit(func(p *project.Project) error { return p.CopyPad(from, to) })
|
|
|
+ return s.apply(func(p *project.Project) error { return p.CopyPad(from, to) })
|
|
|
}
|
|
|
|
|
|
// SwapPads exchanges two pads.
|
|
|
func (s *Service) SwapPads(a, b int) (State, error) {
|
|
|
- return s.edit(func(p *project.Project) error { return p.SwapPads(a, b) })
|
|
|
+ return s.apply(func(p *project.Project) error { return p.SwapPads(a, b) })
|
|
|
}
|
|
|
|
|
|
// Load asks for a project file and opens it in place of the current one.
|
|
|
@@ -453,19 +489,19 @@ func (s *Service) loadPath(path string) (LoadResult, error) {
|
|
|
// AddSequence adds a sequence in an empty slot: "Sequence NN", 2 bars, at
|
|
|
// the BPM of the most recently edited sequence (else 120).
|
|
|
func (s *Service) AddSequence(slot int) (State, error) {
|
|
|
- return s.edit(func(p *project.Project) error { return p.AddSequence(slot) })
|
|
|
+ return s.apply(func(p *project.Project) error { return p.AddSequence(slot) })
|
|
|
}
|
|
|
|
|
|
// RemoveSequence deletes a sequence and its recorded notes. The last
|
|
|
// sequence cannot be removed.
|
|
|
func (s *Service) RemoveSequence(slot int) (State, error) {
|
|
|
- return s.edit(func(p *project.Project) error { return p.RemoveSequence(slot) })
|
|
|
+ return s.apply(func(p *project.Project) error { return p.RemoveSequence(slot) })
|
|
|
}
|
|
|
|
|
|
// SetSequence sets a sequence's name, BPM and length in bars. Notes past a
|
|
|
// new, shorter end are left in the file.
|
|
|
func (s *Service) SetSequence(slot int, name string, bpm float64, bars int) (State, error) {
|
|
|
- return s.edit(func(p *project.Project) error {
|
|
|
+ return s.apply(func(p *project.Project) error {
|
|
|
return p.SetSequence(slot, strings.TrimSpace(name), bpm, bars)
|
|
|
})
|
|
|
}
|