pads.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. package project
  2. import (
  3. "fmt"
  4. "path/filepath"
  5. "strings"
  6. "git.monoexp.com/bajdev/mpckit/internal/wav"
  7. "git.monoexp.com/bajdev/mpckit/internal/xpj"
  8. )
  9. // Values the device writes when it assigns a sample with no tempo or key of
  10. // its own (format doc section 3, build plan 4.3).
  11. const (
  12. padTempo = 20.0
  13. newSampleKey = "C Natural Minor"
  14. newSampleRoot = 60
  15. crossfadeLength = -1
  16. )
  17. // Pad is a read-only view of one pad.
  18. type Pad struct {
  19. Slot int
  20. Label string // "A1" .. "H16"
  21. SampleName string // empty for an empty pad
  22. File string // pool file name
  23. Coarse int
  24. Fine int
  25. MuteGroup int
  26. Start, End int64 // slice start and end frame, for the preview
  27. }
  28. // Empty reports whether the pad has no sample.
  29. func (p Pad) Empty() bool { return p.File == "" }
  30. // Pad returns the pad in slot.
  31. func (p *Project) Pad(slot int) (Pad, error) {
  32. if err := checkSlot(slot); err != nil {
  33. return Pad{}, err
  34. }
  35. pad := p.pad(slot)
  36. l, err := layer(pad)
  37. if err != nil {
  38. return Pad{}, fmt.Errorf("project: pad %s: %w", SlotName(slot), err)
  39. }
  40. v := Pad{Slot: slot, Label: SlotName(slot)}
  41. v.SampleName = text(l.Lookup("sampleName"))
  42. v.File = text(l.Lookup("sampleFile"))
  43. v.Coarse = int(num(l.Lookup("coarseTune")))
  44. v.Fine = int(num(l.Lookup("fineTune")))
  45. v.MuteGroup = int(num(pad.Lookup("whichMuteGroup")))
  46. if si := l.Lookup("sliceInfo"); si != nil {
  47. v.Start = num(si.Lookup("Start"))
  48. v.End = num(si.Lookup("End"))
  49. }
  50. return v, nil
  51. }
  52. func text(n *xpj.Node) string {
  53. if n == nil || n.Kind != xpj.String {
  54. return ""
  55. }
  56. return n.Text
  57. }
  58. // num and float return a number field's value, or 0 if it is missing or
  59. // not a number of that kind.
  60. func num(n *xpj.Node) int64 {
  61. i, _ := n.Int()
  62. return i
  63. }
  64. func float(n *xpj.Node) float64 {
  65. f, _ := n.Float()
  66. return f
  67. }
  68. // padFiles returns the pool files used by any layer of pad.
  69. func padFiles(pad *xpj.Node) []string {
  70. var files []string
  71. layers := pad.Lookup("layersv")
  72. if layers == nil {
  73. return nil
  74. }
  75. for _, l := range layers.Items {
  76. if f := text(l.Lookup("sampleFile")); f != "" {
  77. files = append(files, f)
  78. }
  79. }
  80. return files
  81. }
  82. // usage counts the pad layers that use the pool file.
  83. func (p *Project) usage(file string) int {
  84. n := 0
  85. for _, pad := range p.pads().Items {
  86. for _, f := range padFiles(pad) {
  87. if f == file {
  88. n++
  89. }
  90. }
  91. }
  92. return n
  93. }
  94. // SampleCount returns the number of pool entries.
  95. func (p *Project) SampleCount() int { return len(p.pool().Items) }
  96. // AssignSample puts the WAV at src on the pad in slot, replacing what was
  97. // there. A file already in the pool from the same source is reused; a
  98. // different file with a name already in the pool gets a 001, 002 suffix.
  99. func (p *Project) AssignSample(slot int, src string) error {
  100. if err := checkSlot(slot); err != nil {
  101. return err
  102. }
  103. abs, err := filepath.Abs(src)
  104. if err != nil {
  105. return err
  106. }
  107. info, err := wav.ReadFile(abs)
  108. if err != nil {
  109. return fmt.Errorf("%s: %w", filepath.Base(abs), err)
  110. }
  111. if err := info.Validate(); err != nil {
  112. return fmt.Errorf("%s: %w", filepath.Base(abs), err)
  113. }
  114. file := p.fileForSource(abs)
  115. if file == "" {
  116. // A new pool entry is needed. Clearing the pad first may free one.
  117. freed := 0
  118. for _, f := range padFiles(p.pad(slot)) {
  119. if p.usage(f) == 1 {
  120. freed++
  121. }
  122. }
  123. if p.SampleCount()-freed >= MaxSamples {
  124. return fmt.Errorf("the project already has %d samples, the most the device allows", MaxSamples)
  125. }
  126. }
  127. // Keep the reused entry even if this pad was its only user.
  128. p.clear(slot, file)
  129. if file == "" {
  130. file = p.uniqueFile(filepath.Base(abs))
  131. if err := p.addPoolEntry(file); err != nil {
  132. return err
  133. }
  134. p.Sources[file] = abs
  135. }
  136. pad := p.pad(slot)
  137. l, err := layer(pad)
  138. if err != nil {
  139. return err
  140. }
  141. var e edit
  142. e.set(pad, "tempo", xpj.NewFloat(padTempo))
  143. e.set(l, "sampleName", xpj.NewString(stem(file)))
  144. e.set(l, "sampleFile", xpj.NewString(file))
  145. e.set(l, "sliceInfo.End", xpj.NewInt(info.Frames-1))
  146. e.set(l, "sliceInfo.LoopCrossfadeLength", xpj.NewInt(crossfadeLength))
  147. p.Dirty = true
  148. return e.err
  149. }
  150. // fileForSource returns the pool file already copied from src, if any.
  151. func (p *Project) fileForSource(src string) string {
  152. for file, s := range p.Sources {
  153. if s == src && p.inPool(file) {
  154. return file
  155. }
  156. }
  157. return ""
  158. }
  159. func (p *Project) inPool(file string) bool {
  160. for _, e := range p.pool().Items {
  161. if text(e.Lookup("path")) == file {
  162. return true
  163. }
  164. }
  165. return false
  166. }
  167. func stem(file string) string { return strings.TrimSuffix(file, filepath.Ext(file)) }
  168. // uniqueFile returns base, or base with a 001, 002 ... suffix before the
  169. // extension, so that no two pool files share a name. Names are compared
  170. // without case, as on Windows, macOS and FAT.
  171. func (p *Project) uniqueFile(base string) string {
  172. taken := map[string]bool{}
  173. for _, e := range p.pool().Items {
  174. taken[strings.ToLower(text(e.Lookup("path")))] = true
  175. taken[strings.ToLower(text(e.Lookup("name")))+"\x00"] = true
  176. }
  177. free := func(f string) bool {
  178. return !taken[strings.ToLower(f)] && !taken[strings.ToLower(stem(f))+"\x00"]
  179. }
  180. if free(base) {
  181. return base
  182. }
  183. ext := filepath.Ext(base)
  184. for i := 1; ; i++ {
  185. f := fmt.Sprintf("%s%03d%s", stem(base), i, ext)
  186. if free(f) {
  187. return f
  188. }
  189. }
  190. }
  191. // poolEntry builds a new pool entry in the device's key order.
  192. func poolEntry(file string) *xpj.Node {
  193. meta := xpj.NewObject()
  194. meta.Members = []xpj.Member{
  195. {Key: "tempo", Value: xpj.NewFloat(0)},
  196. {Key: "rootNote", Value: xpj.NewInt(newSampleRoot)},
  197. {Key: "tune", Value: xpj.NewFloat(0)},
  198. {Key: "key", Value: xpj.NewString(newSampleKey)},
  199. }
  200. e := xpj.NewObject()
  201. e.Members = []xpj.Member{
  202. {Key: "version", Value: xpj.NewInt(1)},
  203. {Key: "name", Value: xpj.NewString(stem(file))},
  204. {Key: "path", Value: xpj.NewString(file)},
  205. {Key: "loadImpl", Value: xpj.NewInt(0)},
  206. {Key: "metadata", Value: meta},
  207. }
  208. return e
  209. }
  210. func (p *Project) addPoolEntry(file string) error {
  211. if err := p.pool().Append(poolEntry(file)); err != nil {
  212. return err
  213. }
  214. return p.trackPool().Insert(0, poolEntry(file))
  215. }
  216. func (p *Project) removePoolEntry(file string) {
  217. for _, arr := range []*xpj.Node{p.pool(), p.trackPool()} {
  218. for i := len(arr.Items) - 1; i >= 0; i-- {
  219. if text(arr.Items[i].Lookup("path")) == file {
  220. _ = arr.Remove(i)
  221. }
  222. }
  223. }
  224. delete(p.Sources, file)
  225. }
  226. // ClearPad resets the pad in slot to the blank pad and drops pool entries
  227. // no other pad uses.
  228. func (p *Project) ClearPad(slot int) error {
  229. if err := checkSlot(slot); err != nil {
  230. return err
  231. }
  232. if len(padFiles(p.pad(slot))) == 0 {
  233. return nil
  234. }
  235. p.clear(slot, "")
  236. p.Dirty = true
  237. return nil
  238. }
  239. // clear resets the pad in slot and drops pool entries left unused, except keep.
  240. func (p *Project) clear(slot int, keep string) {
  241. files := padFiles(p.pad(slot))
  242. p.pads().Items[slot] = template.pad.Clone()
  243. for _, f := range files {
  244. if f != keep && p.usage(f) == 0 {
  245. p.removePoolEntry(f)
  246. }
  247. }
  248. }
  249. // CopyPad copies the whole pad in from onto to, replacing what was there.
  250. // Both pads share the pool entry.
  251. func (p *Project) CopyPad(from, to int) error {
  252. if err := checkSlot(from); err != nil {
  253. return err
  254. }
  255. if err := checkSlot(to); err != nil {
  256. return err
  257. }
  258. if from == to {
  259. return nil
  260. }
  261. src := p.pad(from).Clone()
  262. p.clear(to, "")
  263. p.pads().Items[to] = src
  264. p.Dirty = true
  265. return nil
  266. }
  267. // MovePad copies from onto to, then resets from to the blank pad. Recorded
  268. // notes stay on their slot; check PadHasNotes and warn first.
  269. func (p *Project) MovePad(from, to int) error {
  270. if from == to {
  271. return checkSlot(from)
  272. }
  273. if err := p.CopyPad(from, to); err != nil {
  274. return err
  275. }
  276. p.pads().Items[from] = template.pad.Clone()
  277. return nil
  278. }
  279. // SwapPads exchanges two pads.
  280. func (p *Project) SwapPads(a, b int) error {
  281. if err := checkSlot(a); err != nil {
  282. return err
  283. }
  284. if err := checkSlot(b); err != nil {
  285. return err
  286. }
  287. if a == b {
  288. return nil
  289. }
  290. items := p.pads().Items
  291. items[a], items[b] = items[b], items[a]
  292. p.Dirty = true
  293. return nil
  294. }
  295. // SetTune sets the pad's tuning, clamped to -24..+24 semitones and
  296. // -90..+90 cents. pitch is stored as a 32-bit float.
  297. func (p *Project) SetTune(slot, coarse, fine int) error {
  298. if err := checkSlot(slot); err != nil {
  299. return err
  300. }
  301. coarse = max(MinCoarse, min(MaxCoarse, coarse))
  302. fine = max(MinFine, min(MaxFine, fine))
  303. l, err := layer(p.pad(slot))
  304. if err != nil {
  305. return err
  306. }
  307. var e edit
  308. e.set(l, "coarseTune", xpj.NewInt(int64(coarse)))
  309. e.set(l, "fineTune", xpj.NewInt(int64(fine)))
  310. e.set(l, "pitch", xpj.NewFloat32(float64(coarse)+float64(fine)/100))
  311. p.Dirty = true
  312. return e.err
  313. }
  314. // SetMuteGroup sets the pad's mute group; 0 is off.
  315. func (p *Project) SetMuteGroup(slot, group int) error {
  316. if err := checkSlot(slot); err != nil {
  317. return err
  318. }
  319. if group < 0 || group > MaxMuteGroup {
  320. return fmt.Errorf("mute group %d out of range 0-%d", group, MaxMuteGroup)
  321. }
  322. var e edit
  323. e.set(p.pad(slot), "whichMuteGroup", xpj.NewInt(int64(group)))
  324. p.Dirty = true
  325. return e.err
  326. }
  327. // PadHasNotes reports whether any sequence has a note or automation event
  328. // for the pad in slot (note 36 + slot on the drum track).
  329. func (p *Project) PadHasNotes(slot int) bool {
  330. note := int64(FirstNote + slot)
  331. found := false
  332. p.eachDrumEvent(func(ev *xpj.Node) bool {
  333. var n *xpj.Node
  334. switch num(ev.Lookup("type")) {
  335. case eventNote:
  336. n = ev.Lookup("note")
  337. case eventAutomation:
  338. n = ev.Lookup("automation")
  339. }
  340. if n != nil && num(n.Lookup("note")) == note {
  341. found = true
  342. }
  343. return !found
  344. })
  345. return found
  346. }
  347. // PoolFiles returns the file name of every pool entry, in pool order.
  348. func (p *Project) PoolFiles() []string {
  349. var files []string
  350. for _, e := range p.pool().Items {
  351. files = append(files, text(e.Lookup("path")))
  352. }
  353. return files
  354. }