1
0

pads.go 9.4 KB

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