sequences.go 5.5 KB

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