1
0

sequences.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. package project
  2. import (
  3. "fmt"
  4. "sort"
  5. "git.monoexp.com/bajdev/mpckit/internal/xpj"
  6. )
  7. // Event types in a clip's event list.
  8. const (
  9. eventAutomation = 1
  10. eventNote = 3
  11. )
  12. // MaxBars is the longest sequence MPCkit writes. 64 bars is confirmed on the
  13. // device (checkpoint A5); its own maximum is not yet known.
  14. const MaxBars = 999
  15. // Sequence is a read-only view of one sequence.
  16. type Sequence struct {
  17. Slot int
  18. Name string
  19. BPM float64
  20. Bars int
  21. HasNotes bool
  22. }
  23. // Sequences returns the project's sequences in slot order.
  24. func (p *Project) Sequences() []Sequence {
  25. var out []Sequence
  26. for _, s := range p.sequences().Items {
  27. v := s.Lookup("value")
  28. bpm, _ := v.Lookup("bpm").Float()
  29. out = append(out, Sequence{
  30. Slot: int(num(s.Lookup("key"))),
  31. Name: text(v.Lookup("name")),
  32. BPM: bpm,
  33. Bars: int(num(v.Lookup("lengthBars"))),
  34. HasNotes: hasNotes(v),
  35. })
  36. }
  37. sort.Slice(out, func(i, j int) bool { return out[i].Slot < out[j].Slot })
  38. return out
  39. }
  40. // findSequence returns the index in sequences[] of the sequence in slot, or -1.
  41. func (p *Project) findSequence(slot int) int {
  42. for i, s := range p.sequences().Items {
  43. if num(s.Lookup("key")) == int64(slot) {
  44. return i
  45. }
  46. }
  47. return -1
  48. }
  49. func checkSeqSlot(slot int) error {
  50. if slot < 0 || slot >= MaxSequences {
  51. return fmt.Errorf("sequence slot %d out of range 0-%d", slot, MaxSequences-1)
  52. }
  53. return nil
  54. }
  55. // AddSequence adds an empty sequence in slot, cloned from the blank project:
  56. // named "Sequence NN", 2 bars, with the BPM of the most recently edited
  57. // sequence (else 120). Like the device, the newest sequence goes first.
  58. func (p *Project) AddSequence(slot int) error {
  59. if err := checkSeqSlot(slot); err != nil {
  60. return err
  61. }
  62. if p.findSequence(slot) >= 0 {
  63. return fmt.Errorf("sequence %d already exists", slot+1)
  64. }
  65. bpm := p.lastBPM
  66. if bpm == 0 {
  67. bpm = DefaultBPM
  68. }
  69. s := template.sequence.Clone()
  70. var e edit
  71. e.set(s, "key", xpj.NewInt(int64(slot)))
  72. v := s.Lookup("value")
  73. e.set(v, "name", xpj.NewString(fmt.Sprintf("Sequence %02d", slot+1)))
  74. e.set(v, "bpm", xpj.NewFloat(bpm))
  75. if e.err != nil {
  76. return e.err
  77. }
  78. if err := setLength(v, 2); err != nil {
  79. return err
  80. }
  81. p.Dirty = true
  82. return p.sequences().Insert(0, s)
  83. }
  84. // RemoveSequence deletes the sequence in slot. The last sequence cannot be
  85. // removed. If it was the current sequence, the lowest remaining slot
  86. // becomes current.
  87. func (p *Project) RemoveSequence(slot int) error {
  88. i := p.findSequence(slot)
  89. if i < 0 {
  90. return fmt.Errorf("sequence %d does not exist", slot+1)
  91. }
  92. if len(p.sequences().Items) == 1 {
  93. return fmt.Errorf("cannot remove the only sequence")
  94. }
  95. if err := p.sequences().Remove(i); err != nil {
  96. return err
  97. }
  98. p.Dirty = true
  99. if cur := p.data.Lookup("currentSequence"); cur != nil && num(cur) == int64(slot) {
  100. lowest := p.Sequences()[0].Slot
  101. return p.data.SetKey("currentSequence", xpj.NewInt(int64(lowest)))
  102. }
  103. return nil
  104. }
  105. // SetSequence sets a sequence's name, BPM and length in bars. Recorded
  106. // notes are left as they are, including any past a new, shorter end.
  107. func (p *Project) SetSequence(slot int, name string, bpm float64, bars int) error {
  108. i := p.findSequence(slot)
  109. if i < 0 {
  110. return fmt.Errorf("sequence %d does not exist", slot+1)
  111. }
  112. if err := ValidName(name); err != nil {
  113. return fmt.Errorf("sequence name: %w", err)
  114. }
  115. if !(bpm >= MinBPM && bpm <= MaxBPM) {
  116. return fmt.Errorf("BPM must be %g-%g", MinBPM, MaxBPM)
  117. }
  118. if bars < 1 || bars > MaxBars {
  119. return fmt.Errorf("length must be 1-%d bars", MaxBars)
  120. }
  121. v := p.sequences().Items[i].Lookup("value")
  122. var e edit
  123. e.set(v, "name", xpj.NewString(name))
  124. e.set(v, "bpm", xpj.NewFloat(bpm))
  125. if e.err != nil {
  126. return e.err
  127. }
  128. if err := setLength(v, bars); err != nil {
  129. return err
  130. }
  131. p.lastBPM = bpm
  132. p.Dirty = true
  133. return nil
  134. }
  135. // setLength writes the 26 fields that hold a sequence's length: four on the
  136. // sequence and two on each of its 11 clips.
  137. func setLength(v *xpj.Node, bars int) error {
  138. pulses := int64(bars) * PulsesPerBar
  139. var e edit
  140. e.set(v, "lengthBars", xpj.NewInt(int64(bars)))
  141. e.set(v, "loopEndBar", xpj.NewInt(int64(bars)))
  142. e.set(v, "lengthPulses", xpj.NewInt(pulses))
  143. e.set(v, "loopEndPulses", xpj.NewInt(pulses))
  144. clips, err := v.Get("trackClipMaps[0]")
  145. if err != nil {
  146. return fmt.Errorf("project: %w", err)
  147. }
  148. for _, c := range clips.Items {
  149. e.set(c, "value.endPulses", xpj.NewInt(pulses))
  150. e.set(c, "value.loopEndPulses", xpj.NewInt(pulses))
  151. }
  152. return e.err
  153. }
  154. // hasNotes reports whether a sequence value holds any note event.
  155. func hasNotes(v *xpj.Node) bool {
  156. maps := v.Lookup("trackClipMaps")
  157. if maps == nil {
  158. return false
  159. }
  160. for _, m := range maps.Items {
  161. for _, c := range m.Items {
  162. evs, err := c.Get("value.eventList.events")
  163. if err != nil {
  164. continue
  165. }
  166. for _, ev := range evs.Items {
  167. if num(ev.Lookup("type")) == eventNote {
  168. return true
  169. }
  170. }
  171. }
  172. }
  173. return false
  174. }
  175. // eachDrumEvent calls fn for every event in every sequence's drum-track
  176. // clip (the clip keyed by tracks[0]'s name) until fn returns false.
  177. func (p *Project) eachDrumEvent(fn func(ev *xpj.Node) bool) {
  178. track, _ := p.data.Get("tracks[0].name")
  179. drum := text(track)
  180. for _, s := range p.sequences().Items {
  181. maps, err := s.Get("value.trackClipMaps")
  182. if err != nil {
  183. continue
  184. }
  185. for _, m := range maps.Items {
  186. for _, c := range m.Items {
  187. if text(c.Lookup("key")) != drum {
  188. continue
  189. }
  190. evs, err := c.Get("value.eventList.events")
  191. if err != nil {
  192. continue
  193. }
  194. for _, ev := range evs.Items {
  195. if !fn(ev) {
  196. return
  197. }
  198. }
  199. }
  200. }
  201. }
  202. }