1
0

sequences.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. package project
  2. import (
  3. "errors"
  4. "fmt"
  5. "sort"
  6. "git.monoexp.com/bajdev/mpckit/internal/xpj"
  7. )
  8. // Event types in a clip's event list.
  9. const (
  10. eventAutomation = 1
  11. eventNote = 3
  12. )
  13. // MaxBars is the longest sequence MPCkit writes. 64 bars is confirmed on the
  14. // device (checkpoint A5); its own maximum is not yet known.
  15. const MaxBars = 999
  16. // Sequence is a read-only view of one sequence.
  17. type Sequence struct {
  18. Slot int
  19. Name string
  20. BPM float64
  21. Bars int
  22. HasNotes bool
  23. }
  24. // Sequences returns the project's sequences in slot order.
  25. func (p *Project) Sequences() []Sequence {
  26. var out []Sequence
  27. for _, s := range p.sequences().Items {
  28. v := s.Lookup("value")
  29. out = append(out, Sequence{
  30. Slot: int(num(s.Lookup("key"))),
  31. Name: text(v.Lookup("name")),
  32. BPM: float(v.Lookup("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. // The edits above were on a copy; inserting into a list cannot fail.
  82. _ = p.sequences().Insert(0, s)
  83. p.dirty = true
  84. return nil
  85. }
  86. // RemoveSequence deletes the sequence in slot. The last sequence cannot be
  87. // removed. If it was the current sequence, the lowest remaining slot
  88. // becomes current.
  89. func (p *Project) RemoveSequence(slot int) error {
  90. i := p.findSequence(slot)
  91. if i < 0 {
  92. return fmt.Errorf("sequence %d does not exist", slot+1)
  93. }
  94. if len(p.sequences().Items) == 1 {
  95. return fmt.Errorf("cannot remove the only sequence")
  96. }
  97. // If it is the current sequence, the lowest other slot becomes current.
  98. next := -1
  99. if cur := p.data.Lookup("currentSequence"); cur != nil && num(cur) == int64(slot) {
  100. for _, q := range p.Sequences() {
  101. if q.Slot != slot {
  102. next = q.Slot
  103. break
  104. }
  105. }
  106. }
  107. // Neither step can fail: i is in range and data is an object.
  108. _ = p.sequences().Remove(i)
  109. if next >= 0 {
  110. _ = p.data.SetKey("currentSequence", xpj.NewInt(int64(next)))
  111. }
  112. p.dirty = true
  113. return nil
  114. }
  115. // SetSequence sets a sequence's name, BPM and length in bars. Recorded
  116. // notes are left as they are, including any past a new, shorter end.
  117. func (p *Project) SetSequence(slot int, name string, bpm float64, bars int) error {
  118. i := p.findSequence(slot)
  119. if i < 0 {
  120. return fmt.Errorf("sequence %d does not exist", slot+1)
  121. }
  122. if err := ValidName(name); err != nil {
  123. return fmt.Errorf("sequence name: %w", err)
  124. }
  125. if !(bpm >= MinBPM && bpm <= MaxBPM) {
  126. return fmt.Errorf("BPM must be %g-%g", MinBPM, MaxBPM)
  127. }
  128. if bars < 1 || bars > MaxBars {
  129. return fmt.Errorf("length must be 1-%d bars", MaxBars)
  130. }
  131. v := p.sequences().Items[i].Lookup("value")
  132. clips, err := lengthClips(v)
  133. if err != nil {
  134. return err
  135. }
  136. // wrap checked that value is an object, and lengthClips that each clip
  137. // has a value object, so none of these writes can fail.
  138. _ = v.SetKey("name", xpj.NewString(name))
  139. _ = v.SetKey("bpm", xpj.NewFloat(bpm))
  140. writeLength(v, clips, bars)
  141. p.lastBPM = bpm
  142. p.dirty = true
  143. return nil
  144. }
  145. // setLength writes the 26 fields that hold a sequence's length: four on the
  146. // sequence and two on each of its 11 clips. It writes nothing if the
  147. // sequence does not have that shape.
  148. func setLength(v *xpj.Node, bars int) error {
  149. clips, err := lengthClips(v)
  150. if err != nil {
  151. return err
  152. }
  153. writeLength(v, clips, bars)
  154. return nil
  155. }
  156. // lengthClips returns the value object of each of the sequence's clips,
  157. // checking that every object setLength writes to is there.
  158. func lengthClips(v *xpj.Node) ([]*xpj.Node, error) {
  159. if v == nil || v.Kind != xpj.Object {
  160. return nil, errors.New("project: sequence value is not an object")
  161. }
  162. clips, err := v.Get("trackClipMaps[0]")
  163. if err != nil {
  164. return nil, fmt.Errorf("project: %w", err)
  165. }
  166. var values []*xpj.Node
  167. for i, c := range clips.Items {
  168. cv := c.Lookup("value")
  169. if cv == nil || cv.Kind != xpj.Object {
  170. return nil, fmt.Errorf("project: clip %d has no value object", i)
  171. }
  172. values = append(values, cv)
  173. }
  174. return values, nil
  175. }
  176. // writeLength writes a length lengthClips has checked; it cannot fail.
  177. func writeLength(v *xpj.Node, clips []*xpj.Node, bars int) {
  178. pulses := int64(bars) * PulsesPerBar
  179. _ = v.SetKey("lengthBars", xpj.NewInt(int64(bars)))
  180. _ = v.SetKey("loopEndBar", xpj.NewInt(int64(bars)))
  181. _ = v.SetKey("lengthPulses", xpj.NewInt(pulses))
  182. _ = v.SetKey("loopEndPulses", xpj.NewInt(pulses))
  183. for _, cv := range clips {
  184. _ = cv.SetKey("endPulses", xpj.NewInt(pulses))
  185. _ = cv.SetKey("loopEndPulses", xpj.NewInt(pulses))
  186. }
  187. }
  188. // hasNotes reports whether a sequence value holds any note event.
  189. func hasNotes(v *xpj.Node) bool {
  190. maps := v.Lookup("trackClipMaps")
  191. if maps == nil {
  192. return false
  193. }
  194. for _, m := range maps.Items {
  195. for _, c := range m.Items {
  196. evs, err := c.Get("value.eventList.events")
  197. if err != nil {
  198. continue
  199. }
  200. for _, ev := range evs.Items {
  201. if num(ev.Lookup("type")) == eventNote {
  202. return true
  203. }
  204. }
  205. }
  206. }
  207. return false
  208. }
  209. // eachDrumEvent calls fn for every event in every sequence's drum-track
  210. // clip (the clip keyed by tracks[0]'s name) until fn returns false.
  211. func (p *Project) eachDrumEvent(fn func(ev *xpj.Node) bool) {
  212. track, _ := p.data.Get("tracks[0].name")
  213. drum := text(track)
  214. for _, s := range p.sequences().Items {
  215. maps, err := s.Get("value.trackClipMaps")
  216. if err != nil {
  217. continue
  218. }
  219. for _, m := range maps.Items {
  220. for _, c := range m.Items {
  221. if text(c.Lookup("key")) != drum {
  222. continue
  223. }
  224. evs, err := c.Get("value.eventList.events")
  225. if err != nil {
  226. continue
  227. }
  228. for _, ev := range evs.Items {
  229. if !fn(ev) {
  230. return
  231. }
  232. }
  233. }
  234. }
  235. }
  236. }