| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215 |
- package project
- import (
- "fmt"
- "sort"
- "git.monoexp.com/bajdev/mpckit/internal/xpj"
- )
- // Event types in a clip's event list.
- const (
- eventAutomation = 1
- eventNote = 3
- )
- // MaxBars is the longest sequence MPCkit writes. 64 bars is confirmed on the
- // device (checkpoint A5); its own maximum is not yet known.
- const MaxBars = 999
- // Sequence is a read-only view of one sequence.
- type Sequence struct {
- Slot int
- Name string
- BPM float64
- Bars int
- HasNotes bool
- }
- // Sequences returns the project's sequences in slot order.
- func (p *Project) Sequences() []Sequence {
- var out []Sequence
- for _, s := range p.sequences().Items {
- v := s.Lookup("value")
- out = append(out, Sequence{
- Slot: int(num(s.Lookup("key"))),
- Name: text(v.Lookup("name")),
- BPM: float(v.Lookup("bpm")),
- Bars: int(num(v.Lookup("lengthBars"))),
- HasNotes: hasNotes(v),
- })
- }
- sort.Slice(out, func(i, j int) bool { return out[i].Slot < out[j].Slot })
- return out
- }
- // findSequence returns the index in sequences[] of the sequence in slot, or -1.
- func (p *Project) findSequence(slot int) int {
- for i, s := range p.sequences().Items {
- if num(s.Lookup("key")) == int64(slot) {
- return i
- }
- }
- return -1
- }
- func checkSeqSlot(slot int) error {
- if slot < 0 || slot >= MaxSequences {
- return fmt.Errorf("sequence slot %d out of range 0-%d", slot, MaxSequences-1)
- }
- return nil
- }
- // AddSequence adds an empty sequence in slot, cloned from the blank project:
- // named "Sequence NN", 2 bars, with the BPM of the most recently edited
- // sequence (else 120). Like the device, the newest sequence goes first.
- func (p *Project) AddSequence(slot int) error {
- if err := checkSeqSlot(slot); err != nil {
- return err
- }
- if p.findSequence(slot) >= 0 {
- return fmt.Errorf("sequence %d already exists", slot+1)
- }
- bpm := p.lastBPM
- if bpm == 0 {
- bpm = DefaultBPM
- }
- s := template.sequence.Clone()
- var e edit
- e.set(s, "key", xpj.NewInt(int64(slot)))
- v := s.Lookup("value")
- e.set(v, "name", xpj.NewString(fmt.Sprintf("Sequence %02d", slot+1)))
- e.set(v, "bpm", xpj.NewFloat(bpm))
- if e.err != nil {
- return e.err
- }
- if err := setLength(v, 2); err != nil {
- return err
- }
- p.Dirty = true
- return p.sequences().Insert(0, s)
- }
- // RemoveSequence deletes the sequence in slot. The last sequence cannot be
- // removed. If it was the current sequence, the lowest remaining slot
- // becomes current.
- func (p *Project) RemoveSequence(slot int) error {
- i := p.findSequence(slot)
- if i < 0 {
- return fmt.Errorf("sequence %d does not exist", slot+1)
- }
- if len(p.sequences().Items) == 1 {
- return fmt.Errorf("cannot remove the only sequence")
- }
- if err := p.sequences().Remove(i); err != nil {
- return err
- }
- p.Dirty = true
- if cur := p.data.Lookup("currentSequence"); cur != nil && num(cur) == int64(slot) {
- lowest := p.Sequences()[0].Slot
- return p.data.SetKey("currentSequence", xpj.NewInt(int64(lowest)))
- }
- return nil
- }
- // SetSequence sets a sequence's name, BPM and length in bars. Recorded
- // notes are left as they are, including any past a new, shorter end.
- func (p *Project) SetSequence(slot int, name string, bpm float64, bars int) error {
- i := p.findSequence(slot)
- if i < 0 {
- return fmt.Errorf("sequence %d does not exist", slot+1)
- }
- if err := ValidName(name); err != nil {
- return fmt.Errorf("sequence name: %w", err)
- }
- if !(bpm >= MinBPM && bpm <= MaxBPM) {
- return fmt.Errorf("BPM must be %g-%g", MinBPM, MaxBPM)
- }
- if bars < 1 || bars > MaxBars {
- return fmt.Errorf("length must be 1-%d bars", MaxBars)
- }
- v := p.sequences().Items[i].Lookup("value")
- var e edit
- e.set(v, "name", xpj.NewString(name))
- e.set(v, "bpm", xpj.NewFloat(bpm))
- if e.err != nil {
- return e.err
- }
- if err := setLength(v, bars); err != nil {
- return err
- }
- p.lastBPM = bpm
- p.Dirty = true
- return nil
- }
- // setLength writes the 26 fields that hold a sequence's length: four on the
- // sequence and two on each of its 11 clips.
- func setLength(v *xpj.Node, bars int) error {
- pulses := int64(bars) * PulsesPerBar
- var e edit
- e.set(v, "lengthBars", xpj.NewInt(int64(bars)))
- e.set(v, "loopEndBar", xpj.NewInt(int64(bars)))
- e.set(v, "lengthPulses", xpj.NewInt(pulses))
- e.set(v, "loopEndPulses", xpj.NewInt(pulses))
- clips, err := v.Get("trackClipMaps[0]")
- if err != nil {
- return fmt.Errorf("project: %w", err)
- }
- for _, c := range clips.Items {
- e.set(c, "value.endPulses", xpj.NewInt(pulses))
- e.set(c, "value.loopEndPulses", xpj.NewInt(pulses))
- }
- return e.err
- }
- // hasNotes reports whether a sequence value holds any note event.
- func hasNotes(v *xpj.Node) bool {
- maps := v.Lookup("trackClipMaps")
- if maps == nil {
- return false
- }
- for _, m := range maps.Items {
- for _, c := range m.Items {
- evs, err := c.Get("value.eventList.events")
- if err != nil {
- continue
- }
- for _, ev := range evs.Items {
- if num(ev.Lookup("type")) == eventNote {
- return true
- }
- }
- }
- }
- return false
- }
- // eachDrumEvent calls fn for every event in every sequence's drum-track
- // clip (the clip keyed by tracks[0]'s name) until fn returns false.
- func (p *Project) eachDrumEvent(fn func(ev *xpj.Node) bool) {
- track, _ := p.data.Get("tracks[0].name")
- drum := text(track)
- for _, s := range p.sequences().Items {
- maps, err := s.Get("value.trackClipMaps")
- if err != nil {
- continue
- }
- for _, m := range maps.Items {
- for _, c := range m.Items {
- if text(c.Lookup("key")) != drum {
- continue
- }
- evs, err := c.Get("value.eventList.events")
- if err != nil {
- continue
- }
- for _, ev := range evs.Items {
- if !fn(ev) {
- return
- }
- }
- }
- }
- }
- }
|