sequences.go 5.5 KB

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