1
0

sequences.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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 setter
  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. // Written as a negated range so that NaN, for which every comparison is
  126. // false, is refused too.
  127. if !(bpm >= MinBPM && bpm <= MaxBPM) {
  128. return fmt.Errorf("BPM must be %g-%g", MinBPM, MaxBPM)
  129. }
  130. if bars < 1 || bars > MaxBars {
  131. return fmt.Errorf("length must be 1-%d bars", MaxBars)
  132. }
  133. v := p.sequences().Items[i].Lookup("value")
  134. clips, err := lengthClips(v)
  135. if err != nil {
  136. return err
  137. }
  138. // fromFile checked that value is an object, and lengthClips that each clip
  139. // has a value object, so none of these writes can fail.
  140. _ = v.SetKey("name", xpj.NewString(name))
  141. _ = v.SetKey("bpm", xpj.NewFloat(bpm))
  142. writeLength(v, clips, bars)
  143. p.lastBPM = bpm
  144. p.dirty = true
  145. return nil
  146. }
  147. // setLength writes the 26 fields that hold a sequence's length: four on the
  148. // sequence and two on each of its 11 clips. It writes nothing if the
  149. // sequence does not have that shape.
  150. func setLength(v *xpj.Node, bars int) error {
  151. clips, err := lengthClips(v)
  152. if err != nil {
  153. return err
  154. }
  155. writeLength(v, clips, bars)
  156. return nil
  157. }
  158. // lengthClips returns the value object of each of the sequence's clips,
  159. // checking that every object setLength writes to is there.
  160. func lengthClips(v *xpj.Node) ([]*xpj.Node, error) {
  161. if v == nil || v.Kind != xpj.Object {
  162. return nil, errors.New("project: sequence value is not an object")
  163. }
  164. clips, err := v.Get("trackClipMaps[0]")
  165. if err != nil {
  166. return nil, fmt.Errorf("project: %w", err)
  167. }
  168. var values []*xpj.Node
  169. for i, c := range clips.Items {
  170. cv := c.Lookup("value")
  171. if cv == nil || cv.Kind != xpj.Object {
  172. return nil, fmt.Errorf("project: clip %d has no value object", i)
  173. }
  174. values = append(values, cv)
  175. }
  176. return values, nil
  177. }
  178. // writeLength writes a length lengthClips has checked; it cannot fail.
  179. func writeLength(v *xpj.Node, clips []*xpj.Node, bars int) {
  180. pulses := int64(bars) * PulsesPerBar
  181. _ = v.SetKey("lengthBars", xpj.NewInt(int64(bars)))
  182. _ = v.SetKey("loopEndBar", xpj.NewInt(int64(bars)))
  183. _ = v.SetKey("lengthPulses", xpj.NewInt(pulses))
  184. _ = v.SetKey("loopEndPulses", xpj.NewInt(pulses))
  185. for _, cv := range clips {
  186. _ = cv.SetKey("endPulses", xpj.NewInt(pulses))
  187. _ = cv.SetKey("loopEndPulses", xpj.NewInt(pulses))
  188. }
  189. }
  190. // hasNotes reports whether a sequence value holds any note event.
  191. func hasNotes(v *xpj.Node) bool {
  192. maps := v.Lookup("trackClipMaps")
  193. if maps == nil {
  194. return false
  195. }
  196. for _, m := range maps.Items {
  197. for _, c := range m.Items {
  198. evs, err := c.Get("value.eventList.events")
  199. if err != nil {
  200. continue
  201. }
  202. for _, ev := range evs.Items {
  203. if num(ev.Lookup("type")) == eventNote {
  204. return true
  205. }
  206. }
  207. }
  208. }
  209. return false
  210. }
  211. // eachDrumEvent calls fn for every event in every sequence's drum-track
  212. // clip (the clip keyed by tracks[0]'s name) until fn returns false.
  213. func (p *Project) eachDrumEvent(fn func(ev *xpj.Node) bool) {
  214. track, _ := p.data.Get("tracks[0].name")
  215. drum := text(track)
  216. for _, s := range p.sequences().Items {
  217. maps, err := s.Get("value.trackClipMaps")
  218. if err != nil {
  219. continue
  220. }
  221. for _, m := range maps.Items {
  222. for _, c := range m.Items {
  223. if text(c.Lookup("key")) != drum {
  224. continue
  225. }
  226. evs, err := c.Get("value.eventList.events")
  227. if err != nil {
  228. continue
  229. }
  230. for _, ev := range evs.Items {
  231. if !fn(ev) {
  232. return
  233. }
  234. }
  235. }
  236. }
  237. }
  238. }