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") bpm, _ := v.Lookup("bpm").Float() out = append(out, Sequence{ Slot: int(num(s.Lookup("key"))), Name: text(v.Lookup("name")), BPM: 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 } } } } } }