main.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. // Command checkpoint-a builds the projects for device checkpoint A (build
  2. // plan section 7) with the Go writer, and zips them. Run it from the repo
  3. // root; it works the same in cmd, PowerShell and sh:
  4. //
  5. // go run ./tools/checkpoint-a
  6. // go run ./tools/checkpoint-a -zip pack.zip -folder E:\
  7. //
  8. // -folder also writes the projects unzipped, e.g. straight onto the card.
  9. package main
  10. import (
  11. "archive/zip"
  12. "errors"
  13. "flag"
  14. "fmt"
  15. "io"
  16. "io/fs"
  17. "os"
  18. "path/filepath"
  19. "sort"
  20. "time"
  21. "git.monoexp.com/bajdev/mpckit/internal/export"
  22. "git.monoexp.com/bajdev/mpckit/internal/project"
  23. )
  24. func main() {
  25. zipPath := flag.String("zip", "mpckit-checkpoint-a.zip", "write the pack to this zip `file`")
  26. folder := flag.String("folder", "", "also write the projects into this `folder` (e.g. the device's card)")
  27. force := flag.Bool("force", false, "replace projects already in -folder")
  28. flag.Parse()
  29. if err := run(*zipPath, *folder, *force); err != nil {
  30. fmt.Fprintln(os.Stderr, "checkpoint-a:", err)
  31. os.Exit(1)
  32. }
  33. }
  34. func run(zipPath, folder string, force bool) error {
  35. src := filepath.Join("testdata", "device", "DemoFile"+project.DataSuffix)
  36. if _, err := os.Stat(src); err != nil {
  37. return errors.New("run this from the mpckit repo root (testdata/device not found)")
  38. }
  39. work, err := os.MkdirTemp("", "mpckit-checkpoint-a-")
  40. if err != nil {
  41. return err
  42. }
  43. defer os.RemoveAll(work)
  44. projects, err := build(src, work)
  45. if err != nil {
  46. return err
  47. }
  48. pack := filepath.Join(work, "pack")
  49. if err := os.Mkdir(pack, 0o755); err != nil {
  50. return err
  51. }
  52. for _, p := range projects {
  53. if _, err := export.Folder(p, pack, false); err != nil {
  54. return fmt.Errorf("%s: %w", p.Name, err)
  55. }
  56. if folder != "" {
  57. path, err := export.Folder(p, folder, force)
  58. var exists *export.ExistsError
  59. if errors.As(err, &exists) {
  60. return fmt.Errorf("%w (use -force to replace)", err)
  61. }
  62. if err != nil {
  63. return fmt.Errorf("%s: %w", p.Name, err)
  64. }
  65. fmt.Println("wrote", path)
  66. }
  67. fmt.Printf("%s: %d samples, %d sequences\n", p.Name, p.SampleCount(), len(p.Sequences()))
  68. }
  69. if err := zipDir(pack, zipPath); err != nil {
  70. return err
  71. }
  72. fmt.Println("wrote", zipPath)
  73. return nil
  74. }
  75. type pad struct {
  76. label string
  77. file string
  78. coarse int
  79. fine int
  80. mute int
  81. }
  82. type seq struct {
  83. name string
  84. bpm float64
  85. bars int
  86. }
  87. // spec is one test project: pads (applied in order) and sequences 1, 2, ...
  88. type spec struct {
  89. name string
  90. pads []pad
  91. seqs []seq
  92. }
  93. func build(src, work string) ([]*project.Project, error) {
  94. f := func(name string) string { return filepath.Join(src, name) }
  95. kick, snare := f("80s-Kik-Dreamer2 SA.wav"), f("80s-Snr-Dreamer SA.wav")
  96. hat1, hat2 := f("80s-Hat-Dreamer1 SA.wav"), f("80s-Hat-Dreamer2 SA.wav")
  97. bass := f("Bass-Pure C.wav")
  98. tones := filepath.Join(work, "tones")
  99. if err := os.Mkdir(tones, 0o755); err != nil {
  100. return nil, err
  101. }
  102. t := func(name string, rate, bits, channels int, ext bool) (string, error) {
  103. path := filepath.Join(tones, name)
  104. return path, os.WriteFile(path, tone(rate, bits, channels, ext), 0o644)
  105. }
  106. plain, err1 := t("44k 16bit mono.wav", 44100, 16, 1, false)
  107. ext48, err2 := t("48k 24bit stereo extensible.wav", 48000, 24, 2, true)
  108. ext44s, err3 := t("44k 16bit stereo extensible.wav", 44100, 16, 2, true)
  109. ext44m, err4 := t("44k 16bit mono extensible.wav", 44100, 16, 1, true)
  110. if err := errors.Join(err1, err2, err3, err4); err != nil {
  111. return nil, err
  112. }
  113. specs := []spec{
  114. {
  115. // A1 kit, A2 tuning extremes, A3 group-1 choke, A5 1 and 64 bars.
  116. name: "MpckitKit",
  117. pads: []pad{
  118. {label: "A1", file: kick}, {label: "A2", file: snare},
  119. {label: "A3", file: hat1, mute: 1}, {label: "A4", file: hat2, mute: 1},
  120. {label: "B1", file: bass},
  121. {label: "B2", file: bass, coarse: -24}, {label: "B3", file: bass, coarse: 24},
  122. {label: "B4", file: bass, fine: -90}, {label: "B5", file: bass, fine: 90},
  123. {label: "B6", file: bass, coarse: -24, fine: -90}, {label: "B7", file: bass, coarse: 24, fine: 90},
  124. },
  125. seqs: []seq{{"One Bar", 120, 1}, {"Long", 120, 64}},
  126. },
  127. {
  128. // A3: the lowest and highest mute groups. The 2026-11-10 run
  129. // used 16 and 32 (see README); MPCkit now refuses 32.
  130. name: "MpckitMute",
  131. pads: []pad{
  132. {label: "A1", file: hat1, mute: 16}, {label: "A2", file: hat2, mute: 16},
  133. {label: "A3", file: hat1, mute: 1}, {label: "A4", file: hat2, mute: 1},
  134. },
  135. },
  136. {
  137. // A5: BPM extremes.
  138. name: "MpckitTempo",
  139. pads: []pad{{label: "A1", file: kick}},
  140. seqs: []seq{{"Slow", 30, 2}, {"Fast", 300, 2}, {"Very Slow", 20, 2}, {"Very Fast", 999, 2}},
  141. },
  142. {
  143. // Extensible WAV headers (section 7, "Not yet tested").
  144. name: "MpckitWav",
  145. pads: []pad{{label: "A1", file: plain}, {label: "A2", file: ext48}, {label: "A3", file: ext44s}, {label: "A4", file: ext44m}},
  146. },
  147. }
  148. var out []*project.Project
  149. for _, s := range specs {
  150. p, err := s.build()
  151. if err != nil {
  152. return nil, fmt.Errorf("%s: %w", s.name, err)
  153. }
  154. out = append(out, p)
  155. }
  156. return out, nil
  157. }
  158. func (s spec) build() (*project.Project, error) {
  159. p, err := project.New(s.name)
  160. if err != nil {
  161. return nil, err
  162. }
  163. for _, pd := range s.pads {
  164. slot := int(pd.label[0]-'A')*16 + atoi(pd.label[1:]) - 1
  165. if err := p.AssignSample(slot, pd.file); err != nil {
  166. return nil, err
  167. }
  168. if pd.coarse != 0 || pd.fine != 0 {
  169. if err := p.SetTune(slot, pd.coarse, pd.fine); err != nil {
  170. return nil, err
  171. }
  172. }
  173. if pd.mute != 0 {
  174. if err := p.SetMuteGroup(slot, pd.mute); err != nil {
  175. return nil, err
  176. }
  177. }
  178. }
  179. for i, sq := range s.seqs {
  180. if i > 0 {
  181. if err := p.AddSequence(i); err != nil {
  182. return nil, err
  183. }
  184. }
  185. if err := p.SetSequence(i, sq.name, sq.bpm, sq.bars); err != nil {
  186. return nil, err
  187. }
  188. }
  189. return p, nil
  190. }
  191. func atoi(s string) int {
  192. n := 0
  193. for _, c := range s {
  194. n = n*10 + int(c-'0')
  195. }
  196. return n
  197. }
  198. // zipDir zips the contents of dir with forward-slash entry names, folders
  199. // first, in name order.
  200. func zipDir(dir, path string) error {
  201. var entries []string
  202. err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error {
  203. if err != nil || p == dir {
  204. return err
  205. }
  206. rel, err := filepath.Rel(dir, p)
  207. if err != nil {
  208. return err
  209. }
  210. rel = filepath.ToSlash(rel)
  211. if d.IsDir() {
  212. rel += "/"
  213. }
  214. entries = append(entries, rel)
  215. return nil
  216. })
  217. if err != nil {
  218. return err
  219. }
  220. sort.Strings(entries)
  221. out, err := os.Create(path)
  222. if err != nil {
  223. return err
  224. }
  225. zw := zip.NewWriter(out)
  226. now := time.Now()
  227. for _, name := range entries {
  228. h := &zip.FileHeader{Name: name, Modified: now, Method: zip.Deflate}
  229. if name[len(name)-1] == '/' {
  230. h.Method = zip.Store
  231. }
  232. w, err := zw.CreateHeader(h)
  233. if err != nil {
  234. out.Close()
  235. return err
  236. }
  237. if h.Method == zip.Store {
  238. continue
  239. }
  240. in, err := os.Open(filepath.Join(dir, filepath.FromSlash(name)))
  241. if err != nil {
  242. out.Close()
  243. return err
  244. }
  245. _, err = io.Copy(w, in)
  246. in.Close()
  247. if err != nil {
  248. out.Close()
  249. return err
  250. }
  251. }
  252. if err := zw.Close(); err != nil {
  253. out.Close()
  254. return err
  255. }
  256. return out.Close()
  257. }