1
0

project_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. package project
  2. import (
  3. "bytes"
  4. "compress/gzip"
  5. "encoding/binary"
  6. "fmt"
  7. "io"
  8. "os"
  9. "path/filepath"
  10. "strconv"
  11. "strings"
  12. "testing"
  13. "git.monoexp.com/bajdev/mpckit/internal/xpj"
  14. )
  15. const testdata = "../../testdata"
  16. func decompress(t *testing.T, path string) []byte {
  17. t.Helper()
  18. fh, err := os.Open(path)
  19. if err != nil {
  20. t.Fatal(err)
  21. }
  22. defer fh.Close()
  23. zr, err := gzip.NewReader(fh)
  24. if err != nil {
  25. t.Fatal(err)
  26. }
  27. b, err := io.ReadAll(zr)
  28. if err != nil {
  29. t.Fatal(err)
  30. }
  31. return b
  32. }
  33. func encode(t *testing.T, p *Project) []byte {
  34. t.Helper()
  35. b, err := p.File().Encode()
  36. if err != nil {
  37. t.Fatal(err)
  38. }
  39. return b
  40. }
  41. func must(t *testing.T, err error) {
  42. t.Helper()
  43. if err != nil {
  44. t.Fatal(err)
  45. }
  46. }
  47. func newProject(t *testing.T) *Project {
  48. t.Helper()
  49. p, err := New("Test")
  50. must(t, err)
  51. return p
  52. }
  53. // leaves flattens a tree into path -> literal text, for comparing files.
  54. func leaves(n *xpj.Node, path string, out map[string]string) map[string]string {
  55. if out == nil {
  56. out = map[string]string{}
  57. }
  58. switch n.Kind {
  59. case xpj.Object:
  60. for _, m := range n.Members {
  61. leaves(m.Value, path+"."+m.Key, out)
  62. }
  63. case xpj.Array:
  64. for i, it := range n.Items {
  65. leaves(it, fmt.Sprintf("%s[%d]", path, i), out)
  66. }
  67. if len(n.Items) == 0 {
  68. out[path] = "[]"
  69. }
  70. default:
  71. b, _ := n.MarshalJSON()
  72. out[path] = string(b)
  73. }
  74. return out
  75. }
  76. func diffPaths(a, b *xpj.Node) []string {
  77. la, lb := leaves(a, "", nil), leaves(b, "", nil)
  78. var diff []string
  79. for k, v := range la {
  80. if lb[k] != v {
  81. diff = append(diff, k)
  82. }
  83. }
  84. for k := range lb {
  85. if _, ok := la[k]; !ok {
  86. diff = append(diff, k)
  87. }
  88. }
  89. return diff
  90. }
  91. // The prototype's edits for MpckitT1 and T3 (build plan appendix), made
  92. // through the model. The output must equal the files the device accepted.
  93. func buildAccepted(t *testing.T, name string, shared bool) *Project {
  94. t.Helper()
  95. p, err := New(name)
  96. must(t, err)
  97. dir := filepath.Join(testdata, "accepted", name+DataSuffix)
  98. must(t, p.AssignSample(0, filepath.Join(dir, "Linn 9000 kick.wav")))
  99. second := "Linn 9000 kick001.wav"
  100. if shared {
  101. second = "Linn 9000 kick.wav"
  102. }
  103. must(t, p.AssignSample(16, filepath.Join(dir, second)))
  104. must(t, p.SetTune(16, 5, 0))
  105. must(t, p.SetSequence(0, "Verse", 96, 8))
  106. must(t, p.AddSequence(1))
  107. must(t, p.SetSequence(1, "Chorus", 96, 4))
  108. return p
  109. }
  110. func TestGoldenMpckitT1(t *testing.T) {
  111. p := buildAccepted(t, "MpckitT1", false)
  112. want := decompress(t, filepath.Join(testdata, "accepted", "MpckitT1.xpj"))
  113. if got := encode(t, p); !bytes.Equal(got, want) {
  114. f, _ := xpj.Decode(want)
  115. t.Fatalf("differs from MpckitT1.xpj at %v", diffPaths(p.File().Root, f.Root))
  116. }
  117. if p.SampleCount() != 2 {
  118. t.Errorf("%d samples, want 2", p.SampleCount())
  119. }
  120. }
  121. func TestGoldenMpckitT3(t *testing.T) {
  122. p := buildAccepted(t, "MpckitT3", true)
  123. want := decompress(t, filepath.Join(testdata, "accepted", "MpckitT3.xpj"))
  124. if got := encode(t, p); !bytes.Equal(got, want) {
  125. f, _ := xpj.Decode(want)
  126. t.Fatalf("differs from MpckitT3.xpj at %v", diffPaths(p.File().Root, f.Root))
  127. }
  128. if p.SampleCount() != 1 {
  129. t.Errorf("%d samples, want 1 shared", p.SampleCount())
  130. }
  131. }
  132. // Rebuild Demo02 from the blank template and compare with the device's file.
  133. func TestRebuildDemo02(t *testing.T) {
  134. dir := filepath.Join(testdata, "device", "Demo02"+DataSuffix)
  135. p := newProject(t)
  136. must(t, p.AssignSample(0, filepath.Join(dir, "Linn 9000 kick.wav")))
  137. must(t, p.AssignSample(4, filepath.Join(dir, "Linn 9000 kick001.wav")))
  138. must(t, p.SetTune(4, 12, 40))
  139. // Start/end trim is a 2.0 feature; its fields are verified, so set them
  140. // directly to complete the rebuild.
  141. l, err := layer(p.pad(4))
  142. must(t, err)
  143. must(t, l.Set("sliceInfo.Start", xpj.NewInt(328)))
  144. must(t, l.Set("sliceInfo.LoopStart", xpj.NewInt(328)))
  145. must(t, l.Set("sliceInfo.End", xpj.NewInt(3777)))
  146. must(t, p.SetSequence(0, "Sequence 01", 105, 2))
  147. must(t, p.AddSequence(1))
  148. must(t, p.SetSequence(1, "Sequence 02", 105, 4))
  149. want, err := xpj.ReadFile(filepath.Join(testdata, "device", "Demo02.xpj"))
  150. must(t, err)
  151. got, wantLeaves := leaves(p.File().Root, "", nil), leaves(want.Root, "", nil)
  152. var unexpected []string
  153. for _, path := range diffPaths(p.File().Root, want.Root) {
  154. switch {
  155. case sameFloat(got[path], wantLeaves[path]):
  156. // The device prints some floats with 17 digits where we print
  157. // the shortest form: pitch 12.399999618530274 vs ...273 is the
  158. // same float64.
  159. case path == ".data.tracks[0].colour",
  160. path == ".data.assignableEnvelopeFollowerAssignments.currentControlIndex",
  161. strings.Contains(path, ".eventList.events"):
  162. // Known noise and recorded notes (build plan M2).
  163. case path == ".data.tracks[0].program.drum.instruments[4].layersv[0].sliceInfo.LoopCrossfadeLength":
  164. // The device wrote a different value on the pad it trimmed; we
  165. // write -1 as for every assigned sample (format doc section 3).
  166. default:
  167. unexpected = append(unexpected, path)
  168. }
  169. }
  170. if len(unexpected) > 0 {
  171. t.Errorf("unexpected differences from Demo02: %v", unexpected)
  172. }
  173. }
  174. func sameFloat(a, b string) bool {
  175. fa, errA := strconv.ParseFloat(a, 64)
  176. fb, errB := strconv.ParseFloat(b, 64)
  177. return errA == nil && errB == nil && fa == fb
  178. }
  179. // writeWAV writes a short 16-bit mono WAV of frames frames.
  180. func writeWAV(t *testing.T, path string, frames int) string {
  181. t.Helper()
  182. data := make([]byte, frames*2)
  183. var b bytes.Buffer
  184. le := binary.LittleEndian
  185. b.WriteString("RIFF")
  186. binary.Write(&b, le, uint32(4+8+16+8+len(data)))
  187. b.WriteString("WAVEfmt ")
  188. for _, v := range []any{uint32(16), uint16(1), uint16(1), uint32(44100), uint32(88200), uint16(2), uint16(16)} {
  189. binary.Write(&b, le, v)
  190. }
  191. b.WriteString("data")
  192. binary.Write(&b, le, uint32(len(data)))
  193. b.Write(data)
  194. must(t, os.MkdirAll(filepath.Dir(path), 0o755))
  195. must(t, os.WriteFile(path, b.Bytes(), 0o644))
  196. return path
  197. }
  198. func padOf(t *testing.T, p *Project, slot int) Pad {
  199. t.Helper()
  200. v, err := p.Pad(slot)
  201. must(t, err)
  202. return v
  203. }
  204. func TestAssignAndPoolReuse(t *testing.T) {
  205. dir := t.TempDir()
  206. kick := writeWAV(t, filepath.Join(dir, "kick.wav"), 100)
  207. p := newProject(t)
  208. must(t, p.AssignSample(0, kick))
  209. must(t, p.AssignSample(1, kick))
  210. if p.SampleCount() != 1 {
  211. t.Fatalf("same source on two pads: %d pool entries, want 1", p.SampleCount())
  212. }
  213. a := padOf(t, p, 1)
  214. if a.File != "kick.wav" || a.SampleName != "kick" || a.End != 99 {
  215. t.Errorf("pad A2 = %+v", a)
  216. }
  217. if !p.Dirty {
  218. t.Error("not dirty after assign")
  219. }
  220. // Reassigning the same source to its only pad keeps the entry.
  221. must(t, p.ClearPad(1))
  222. must(t, p.AssignSample(0, kick))
  223. if p.SampleCount() != 1 || padOf(t, p, 0).File != "kick.wav" {
  224. t.Errorf("reassign same source: %d entries, pad %+v", p.SampleCount(), padOf(t, p, 0))
  225. }
  226. if got := p.Sources["kick.wav"]; got != kick {
  227. t.Errorf("source = %q, want %q", got, kick)
  228. }
  229. // Replacing drops the old entry when no pad uses it.
  230. snare := writeWAV(t, filepath.Join(dir, "snare.wav"), 50)
  231. must(t, p.AssignSample(0, snare))
  232. if p.SampleCount() != 1 || p.inPool("kick.wav") || p.Sources["kick.wav"] != "" {
  233. t.Errorf("replace did not drop kick: %d entries", p.SampleCount())
  234. }
  235. }
  236. func TestNameCollision(t *testing.T) {
  237. dir := t.TempDir()
  238. p := newProject(t)
  239. must(t, p.AssignSample(0, writeWAV(t, filepath.Join(dir, "a", "kick.wav"), 10)))
  240. must(t, p.AssignSample(1, writeWAV(t, filepath.Join(dir, "b", "kick.wav"), 20)))
  241. must(t, p.AssignSample(2, writeWAV(t, filepath.Join(dir, "c", "Kick.WAV"), 30)))
  242. want := []string{"kick.wav", "kick001.wav", "Kick002.WAV"}
  243. for i, w := range want {
  244. if got := padOf(t, p, i); got.File != w || got.SampleName != strings.TrimSuffix(w, filepath.Ext(w)) {
  245. t.Errorf("pad %d = %q/%q, want %q", i, got.SampleName, got.File, w)
  246. }
  247. }
  248. // Both pools hold the same entries; the track pool in reverse order.
  249. var pool, track []string
  250. for _, e := range p.pool().Items {
  251. pool = append(pool, e.Lookup("path").Text)
  252. }
  253. for _, e := range p.trackPool().Items {
  254. track = append(track, e.Lookup("path").Text)
  255. }
  256. if strings.Join(pool, ",") != "kick.wav,kick001.wav,Kick002.WAV" || strings.Join(track, ",") != "Kick002.WAV,kick001.wav,kick.wav" {
  257. t.Errorf("pool %v, track pool %v", pool, track)
  258. }
  259. }
  260. func TestSampleLimit(t *testing.T) {
  261. dir := t.TempDir()
  262. p := newProject(t)
  263. // With one sample per pad the pool can only fill up when it also holds
  264. // entries no pad uses, as a loaded project may. Simulate one.
  265. must(t, p.addPoolEntry("unused.wav"))
  266. for i := 0; i < MaxSamples-1; i++ {
  267. must(t, p.AssignSample(i, writeWAV(t, filepath.Join(dir, fmt.Sprintf("s%03d.wav", i)), 4)))
  268. }
  269. if p.SampleCount() != MaxSamples {
  270. t.Fatalf("%d samples, want %d", p.SampleCount(), MaxSamples)
  271. }
  272. err := p.AssignSample(127, writeWAV(t, filepath.Join(dir, "x.wav"), 4))
  273. if err == nil || !strings.Contains(err.Error(), "128") {
  274. t.Fatalf("129th sample: %v", err)
  275. }
  276. if !padOf(t, p, 127).Empty() || p.SampleCount() != MaxSamples {
  277. t.Error("refused assign changed the project")
  278. }
  279. // Reusing a file already in the pool needs no new entry.
  280. must(t, p.AssignSample(127, filepath.Join(dir, "s000.wav")))
  281. // Replacing a pad's only sample frees its entry first.
  282. must(t, p.AssignSample(1, filepath.Join(dir, "x.wav")))
  283. if p.SampleCount() != MaxSamples || padOf(t, p, 1).File != "x.wav" {
  284. t.Errorf("replace at the limit: %d samples, pad %+v", p.SampleCount(), padOf(t, p, 1))
  285. }
  286. }
  287. func TestAssignRejectsBadFiles(t *testing.T) {
  288. dir := t.TempDir()
  289. p := newProject(t)
  290. bad := filepath.Join(dir, "bad.wav")
  291. must(t, os.WriteFile(bad, []byte("not a wav"), 0o644))
  292. if err := p.AssignSample(0, bad); err == nil {
  293. t.Error("non-WAV accepted")
  294. }
  295. if err := p.AssignSample(0, filepath.Join(dir, "missing.wav")); err == nil {
  296. t.Error("missing file accepted")
  297. }
  298. if err := p.AssignSample(128, writeWAV(t, filepath.Join(dir, "ok.wav"), 4)); err == nil {
  299. t.Error("slot 128 accepted")
  300. }
  301. if p.Dirty || p.SampleCount() != 0 {
  302. t.Error("failed assigns changed the project")
  303. }
  304. }
  305. func TestClearCopyMoveSwap(t *testing.T) {
  306. dir := t.TempDir()
  307. p := newProject(t)
  308. kick := writeWAV(t, filepath.Join(dir, "kick.wav"), 10)
  309. snare := writeWAV(t, filepath.Join(dir, "snare.wav"), 20)
  310. must(t, p.AssignSample(0, kick))
  311. must(t, p.AssignSample(1, snare))
  312. must(t, p.SetTune(0, 3, -20))
  313. must(t, p.SetMuteGroup(0, 2))
  314. // Copy across banks keeps tuning and mute group and shares the entry.
  315. must(t, p.CopyPad(0, 40))
  316. c := padOf(t, p, 40)
  317. if c.File != "kick.wav" || c.Coarse != 3 || c.Fine != -20 || c.MuteGroup != 2 || p.SampleCount() != 2 {
  318. t.Errorf("copy: %+v, %d samples", c, p.SampleCount())
  319. }
  320. // The copy is independent of its source.
  321. must(t, p.SetTune(40, 0, 0))
  322. if padOf(t, p, 0).Coarse != 3 {
  323. t.Error("copy shares data with its source")
  324. }
  325. // Move onto a filled pad replaces it; the snare's entry goes.
  326. must(t, p.MovePad(0, 1))
  327. if !padOf(t, p, 0).Empty() || padOf(t, p, 1).File != "kick.wav" || p.inPool("snare.wav") {
  328. t.Errorf("move: A1 %+v, A2 %+v", padOf(t, p, 0), padOf(t, p, 1))
  329. }
  330. blank, _ := template.pad.MarshalJSON()
  331. if got, _ := p.pad(0).MarshalJSON(); !bytes.Equal(got, blank) {
  332. t.Error("moved-from pad is not the blank pad")
  333. }
  334. // Swap.
  335. must(t, p.AssignSample(5, snare))
  336. must(t, p.SwapPads(1, 5))
  337. if padOf(t, p, 1).File != "snare.wav" || padOf(t, p, 5).File != "kick.wav" {
  338. t.Error("swap")
  339. }
  340. // Clearing the last user drops the entry; clearing a shared one keeps it.
  341. must(t, p.ClearPad(5))
  342. if !p.inPool("kick.wav") {
  343. t.Error("cleared a shared entry (kick is still on pad 40)")
  344. }
  345. must(t, p.ClearPad(40))
  346. if p.inPool("kick.wav") || p.SampleCount() != 1 {
  347. t.Errorf("kick entry kept after its last pad was cleared (%d samples)", p.SampleCount())
  348. }
  349. // No-ops and range checks.
  350. p.Dirty = false
  351. must(t, p.ClearPad(100))
  352. must(t, p.CopyPad(1, 1))
  353. must(t, p.MovePad(1, 1))
  354. must(t, p.SwapPads(1, 1))
  355. if p.Dirty {
  356. t.Error("no-op marked the project dirty")
  357. }
  358. for _, err := range []error{p.CopyPad(-1, 0), p.MovePad(0, 128), p.SwapPads(200, 0), p.ClearPad(128), p.SetTune(128, 0, 0), p.SetMuteGroup(-1, 0)} {
  359. if err == nil {
  360. t.Error("out-of-range slot accepted")
  361. }
  362. }
  363. }
  364. func TestTuneAndMuteGroup(t *testing.T) {
  365. p := newProject(t)
  366. must(t, p.SetTune(0, 12, 40))
  367. l, _ := layer(p.pad(0))
  368. if got := l.Lookup("pitch").Text; got != "12.399999618530273" {
  369. t.Errorf("pitch = %s", got)
  370. }
  371. must(t, p.SetTune(0, 99, -500))
  372. if v := padOf(t, p, 0); v.Coarse != 24 || v.Fine != -90 {
  373. t.Errorf("clamped tune = %d %d", v.Coarse, v.Fine)
  374. }
  375. if got := l.Lookup("pitch").Text; got != "23.100000381469727" {
  376. t.Errorf("pitch = %s", got)
  377. }
  378. must(t, p.SetMuteGroup(3, 1))
  379. if padOf(t, p, 3).MuteGroup != 1 {
  380. t.Error("mute group")
  381. }
  382. if err := p.SetMuteGroup(3, MaxMuteGroup+1); err == nil {
  383. t.Error("mute group out of range accepted")
  384. }
  385. }
  386. func TestSequences(t *testing.T) {
  387. p := newProject(t)
  388. must(t, p.AddSequence(4))
  389. s := p.Sequences()
  390. if len(s) != 2 || s[1] != (Sequence{Slot: 4, Name: "Sequence 05", BPM: DefaultBPM, Bars: 2}) {
  391. t.Fatalf("after add: %+v", s)
  392. }
  393. if err := p.AddSequence(4); err == nil {
  394. t.Error("duplicate slot accepted")
  395. }
  396. if err := p.AddSequence(MaxSequences); err == nil {
  397. t.Error("slot 128 accepted")
  398. }
  399. must(t, p.SetSequence(4, "Drop", 140.5, 16))
  400. must(t, p.AddSequence(127))
  401. s = p.Sequences()
  402. if s[1].Name != "Drop" || s[1].BPM != 140.5 || s[1].Bars != 16 || s[2].Name != "Sequence 128" || s[2].BPM != 140.5 {
  403. t.Errorf("after edit: %+v", s)
  404. }
  405. // Newest first in the file, as the device writes.
  406. if num(p.sequences().Items[0].Lookup("key")) != 127 {
  407. t.Error("new sequence not inserted first")
  408. }
  409. // All 26 length fields.
  410. v := p.sequences().Items[p.findSequence(4)].Lookup("value")
  411. n := 0
  412. for path, val := range leaves(v, "", nil) {
  413. if strings.HasSuffix(path, "lengthBars") || strings.HasSuffix(path, "loopEndBar") {
  414. n++
  415. if val != "16" {
  416. t.Errorf("%s = %s", path, val)
  417. }
  418. }
  419. if strings.HasSuffix(path, ".lengthPulses") || strings.HasSuffix(path, "loopEndPulses") || strings.HasSuffix(path, ".endPulses") {
  420. n++
  421. if val != "61440" {
  422. t.Errorf("%s = %s", path, val)
  423. }
  424. }
  425. }
  426. if n != 26 {
  427. t.Errorf("%d length fields, want 26", n)
  428. }
  429. for _, bad := range []struct {
  430. name string
  431. bpm float64
  432. bars int
  433. }{{"", 120, 2}, {"a/b", 120, 2}, {"ok", 0, 2}, {"ok", -1, 2}, {"ok", 120, 0}, {"ok", 120, MaxBars + 1}} {
  434. if err := p.SetSequence(4, bad.name, bad.bpm, bad.bars); err == nil {
  435. t.Errorf("SetSequence(%q, %v, %d) accepted", bad.name, bad.bpm, bad.bars)
  436. }
  437. }
  438. if err := p.SetSequence(9, "x", 120, 2); err == nil {
  439. t.Error("missing sequence accepted")
  440. }
  441. // Remove: current sequence moves to the lowest remaining slot.
  442. must(t, p.data.SetKey("currentSequence", xpj.NewInt(4)))
  443. must(t, p.RemoveSequence(4))
  444. if cur := num(p.data.Lookup("currentSequence")); cur != 0 {
  445. t.Errorf("currentSequence = %d, want 0", cur)
  446. }
  447. must(t, p.RemoveSequence(0))
  448. if cur := num(p.data.Lookup("currentSequence")); cur != 127 {
  449. t.Errorf("currentSequence = %d, want 127", cur)
  450. }
  451. if err := p.RemoveSequence(127); err == nil {
  452. t.Error("removed the last sequence")
  453. }
  454. if err := p.RemoveSequence(3); err == nil {
  455. t.Error("removed a missing sequence")
  456. }
  457. }
  458. func TestLoadDemoFile(t *testing.T) {
  459. p, warnings, err := Load(filepath.Join(testdata, "device", "DemoFile.xpj"))
  460. must(t, err)
  461. if len(warnings) != 0 {
  462. t.Errorf("warnings: %v", warnings)
  463. }
  464. if p.Name != "DemoFile" || p.Dirty || p.SampleCount() != 6 {
  465. t.Errorf("name %q dirty %v samples %d", p.Name, p.Dirty, p.SampleCount())
  466. }
  467. for file, src := range p.Sources {
  468. if _, err := os.Stat(src); err != nil {
  469. t.Errorf("source for %s: %v", file, err)
  470. }
  471. }
  472. // The sequences have notes on A1-A4 (36-39); the other pads have none.
  473. for slot := 0; slot < Pads; slot++ {
  474. if got, want := p.PadHasNotes(slot), slot < 4; got != want {
  475. t.Errorf("PadHasNotes(%s) = %v", SlotName(slot), got)
  476. }
  477. }
  478. s := p.Sequences()
  479. if len(s) != 2 || s[0].Name != "Intro" || !s[0].HasNotes || s[1].Name != "Sequence 02" || !s[1].HasNotes {
  480. t.Errorf("sequences %+v", s)
  481. }
  482. if v := padOf(t, p, 2); v.MuteGroup != 1 || v.File != "80s-Hat-Dreamer1 SA.wav" {
  483. t.Errorf("A3 = %+v", v)
  484. }
  485. // Loaded and unchanged: writes back byte-identical.
  486. want := decompress(t, filepath.Join(testdata, "device", "DemoFile.xpj"))
  487. if !bytes.Equal(encode(t, p), want) {
  488. t.Error("loaded project does not write back unchanged")
  489. }
  490. // Assigning a file already in the project folder reuses its entry.
  491. must(t, p.AssignSample(50, filepath.Join(testdata, "device", "DemoFile"+DataSuffix, "Bass-Pure C.wav")))
  492. if p.SampleCount() != 6 {
  493. t.Errorf("%d samples after reusing a loaded file", p.SampleCount())
  494. }
  495. }
  496. func TestBlankPadsAndTemplate(t *testing.T) {
  497. p := newProject(t)
  498. if !bytes.Equal(encode(t, p), decompress(t, filepath.Join(testdata, "device", "DemoBlank.xpj"))) {
  499. t.Error("New() is not DemoBlank (is internal/project/template out of date?)")
  500. }
  501. for slot := 0; slot < Pads; slot++ {
  502. if !padOf(t, p, slot).Empty() || p.PadHasNotes(slot) {
  503. t.Fatalf("pad %d not empty", slot)
  504. }
  505. }
  506. }
  507. func TestNames(t *testing.T) {
  508. p := newProject(t)
  509. must(t, p.SetName("My Kit 2"))
  510. if p.Name != "My Kit 2" || !p.Dirty {
  511. t.Error("SetName")
  512. }
  513. for _, bad := range []string{"", " ", "a:b", "a?", "end.", "end ", "CON", "com1.txt", "tab\there"} {
  514. if err := ValidName(bad); err == nil {
  515. t.Errorf("ValidName(%q) accepted", bad)
  516. }
  517. }
  518. if _, err := New("bad|name"); err == nil {
  519. t.Error("New with a bad name accepted")
  520. }
  521. for slot, want := range map[int]string{0: "A1", 4: "A5", 16: "B1", 17: "B2", 127: "H16"} {
  522. if got := SlotName(slot); got != want {
  523. t.Errorf("SlotName(%d) = %s, want %s", slot, got, want)
  524. }
  525. }
  526. }