|
@@ -0,0 +1,272 @@
|
|
|
|
|
+// Command checkpoint-a builds the projects for device checkpoint A (build
|
|
|
|
|
+// plan section 7) with the Go writer, and zips them. Run it from the repo
|
|
|
|
|
+// root; it works the same in cmd, PowerShell and sh:
|
|
|
|
|
+//
|
|
|
|
|
+// go run ./tools/checkpoint-a
|
|
|
|
|
+// go run ./tools/checkpoint-a -zip pack.zip -folder E:\
|
|
|
|
|
+//
|
|
|
|
|
+// -folder also writes the projects unzipped, e.g. straight onto the card.
|
|
|
|
|
+package main
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "archive/zip"
|
|
|
|
|
+ "errors"
|
|
|
|
|
+ "flag"
|
|
|
|
|
+ "fmt"
|
|
|
|
|
+ "io"
|
|
|
|
|
+ "io/fs"
|
|
|
|
|
+ "os"
|
|
|
|
|
+ "path/filepath"
|
|
|
|
|
+ "sort"
|
|
|
|
|
+ "time"
|
|
|
|
|
+
|
|
|
|
|
+ "git.monoexp.com/bajdev/mpckit/internal/export"
|
|
|
|
|
+ "git.monoexp.com/bajdev/mpckit/internal/project"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+func main() {
|
|
|
|
|
+ zipPath := flag.String("zip", "mpckit-checkpoint-a.zip", "write the pack to this zip `file`")
|
|
|
|
|
+ folder := flag.String("folder", "", "also write the projects into this `folder` (e.g. the device's card)")
|
|
|
|
|
+ force := flag.Bool("force", false, "replace projects already in -folder")
|
|
|
|
|
+ flag.Parse()
|
|
|
|
|
+ if err := run(*zipPath, *folder, *force); err != nil {
|
|
|
|
|
+ fmt.Fprintln(os.Stderr, "checkpoint-a:", err)
|
|
|
|
|
+ os.Exit(1)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func run(zipPath, folder string, force bool) error {
|
|
|
|
|
+ src := filepath.Join("testdata", "device", "DemoFile"+project.DataSuffix)
|
|
|
|
|
+ if _, err := os.Stat(src); err != nil {
|
|
|
|
|
+ return errors.New("run this from the mpckit repo root (testdata/device not found)")
|
|
|
|
|
+ }
|
|
|
|
|
+ work, err := os.MkdirTemp("", "mpckit-checkpoint-a-")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+ defer os.RemoveAll(work)
|
|
|
|
|
+
|
|
|
|
|
+ projects, err := build(src, work)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+ pack := filepath.Join(work, "pack")
|
|
|
|
|
+ if err := os.Mkdir(pack, 0o755); err != nil {
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, p := range projects {
|
|
|
|
|
+ if _, err := export.Folder(p, pack, false); err != nil {
|
|
|
|
|
+ return fmt.Errorf("%s: %w", p.Name, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if folder != "" {
|
|
|
|
|
+ path, err := export.Folder(p, folder, force)
|
|
|
|
|
+ var exists *export.ExistsError
|
|
|
|
|
+ if errors.As(err, &exists) {
|
|
|
|
|
+ return fmt.Errorf("%w (use -force to replace)", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return fmt.Errorf("%s: %w", p.Name, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ fmt.Println("wrote", path)
|
|
|
|
|
+ }
|
|
|
|
|
+ fmt.Printf("%s: %d samples, %d sequences\n", p.Name, p.SampleCount(), len(p.Sequences()))
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := zipDir(pack, zipPath); err != nil {
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+ fmt.Println("wrote", zipPath)
|
|
|
|
|
+ return nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type pad struct {
|
|
|
|
|
+ label string
|
|
|
|
|
+ file string
|
|
|
|
|
+ coarse int
|
|
|
|
|
+ fine int
|
|
|
|
|
+ mute int
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type seq struct {
|
|
|
|
|
+ name string
|
|
|
|
|
+ bpm float64
|
|
|
|
|
+ bars int
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// spec is one test project: pads (applied in order) and sequences 1, 2, ...
|
|
|
|
|
+type spec struct {
|
|
|
|
|
+ name string
|
|
|
|
|
+ pads []pad
|
|
|
|
|
+ seqs []seq
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func build(src, work string) ([]*project.Project, error) {
|
|
|
|
|
+ f := func(name string) string { return filepath.Join(src, name) }
|
|
|
|
|
+ kick, snare := f("80s-Kik-Dreamer2 SA.wav"), f("80s-Snr-Dreamer SA.wav")
|
|
|
|
|
+ hat1, hat2 := f("80s-Hat-Dreamer1 SA.wav"), f("80s-Hat-Dreamer2 SA.wav")
|
|
|
|
|
+ bass := f("Bass-Pure C.wav")
|
|
|
|
|
+
|
|
|
|
|
+ tones := filepath.Join(work, "tones")
|
|
|
|
|
+ if err := os.Mkdir(tones, 0o755); err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ t := func(name string, rate, bits, channels int, ext bool) (string, error) {
|
|
|
|
|
+ path := filepath.Join(tones, name)
|
|
|
|
|
+ return path, os.WriteFile(path, tone(rate, bits, channels, ext), 0o644)
|
|
|
|
|
+ }
|
|
|
|
|
+ plain, err1 := t("44k 16bit mono.wav", 44100, 16, 1, false)
|
|
|
|
|
+ ext48, err2 := t("48k 24bit stereo extensible.wav", 48000, 24, 2, true)
|
|
|
|
|
+ ext44s, err3 := t("44k 16bit stereo extensible.wav", 44100, 16, 2, true)
|
|
|
|
|
+ ext44m, err4 := t("44k 16bit mono extensible.wav", 44100, 16, 1, true)
|
|
|
|
|
+ if err := errors.Join(err1, err2, err3, err4); err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ specs := []spec{
|
|
|
|
|
+ {
|
|
|
|
|
+ // A1 kit, A2 tuning extremes, A3 group-1 choke, A5 1 and 64 bars.
|
|
|
|
|
+ name: "MpckitKit",
|
|
|
|
|
+ pads: []pad{
|
|
|
|
|
+ {label: "A1", file: kick}, {label: "A2", file: snare},
|
|
|
|
|
+ {label: "A3", file: hat1, mute: 1}, {label: "A4", file: hat2, mute: 1},
|
|
|
|
|
+ {label: "B1", file: bass},
|
|
|
|
|
+ {label: "B2", file: bass, coarse: -24}, {label: "B3", file: bass, coarse: 24},
|
|
|
|
|
+ {label: "B4", file: bass, fine: -90}, {label: "B5", file: bass, fine: 90},
|
|
|
|
|
+ {label: "B6", file: bass, coarse: -24, fine: -90}, {label: "B7", file: bass, coarse: 24, fine: 90},
|
|
|
|
|
+ },
|
|
|
|
|
+ seqs: []seq{{"One Bar", 120, 1}, {"Long", 120, 64}},
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ // A3: does the device accept mute groups 16 and 32 in a file?
|
|
|
|
|
+ name: "MpckitMute",
|
|
|
|
|
+ pads: []pad{
|
|
|
|
|
+ {label: "A1", file: hat1, mute: 16}, {label: "A2", file: hat2, mute: 16},
|
|
|
|
|
+ {label: "A3", file: hat1, mute: 32}, {label: "A4", file: hat2, mute: 32},
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ // A5: BPM extremes.
|
|
|
|
|
+ name: "MpckitTempo",
|
|
|
|
|
+ pads: []pad{{label: "A1", file: kick}},
|
|
|
|
|
+ seqs: []seq{{"Slow", 30, 2}, {"Fast", 300, 2}, {"Very Slow", 20, 2}, {"Very Fast", 999, 2}},
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ // Extensible WAV headers (section 7, "Not yet tested").
|
|
|
|
|
+ name: "MpckitWav",
|
|
|
|
|
+ pads: []pad{{label: "A1", file: plain}, {label: "A2", file: ext48}, {label: "A3", file: ext44s}, {label: "A4", file: ext44m}},
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var out []*project.Project
|
|
|
|
|
+ for _, s := range specs {
|
|
|
|
|
+ p, err := s.build()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, fmt.Errorf("%s: %w", s.name, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ out = append(out, p)
|
|
|
|
|
+ }
|
|
|
|
|
+ return out, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (s spec) build() (*project.Project, error) {
|
|
|
|
|
+ p, err := project.New(s.name)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, pd := range s.pads {
|
|
|
|
|
+ slot := int(pd.label[0]-'A')*16 + atoi(pd.label[1:]) - 1
|
|
|
|
|
+ if err := p.AssignSample(slot, pd.file); err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ if pd.coarse != 0 || pd.fine != 0 {
|
|
|
|
|
+ if err := p.SetTune(slot, pd.coarse, pd.fine); err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if pd.mute != 0 {
|
|
|
|
|
+ if err := p.SetMuteGroup(slot, pd.mute); err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ for i, sq := range s.seqs {
|
|
|
|
|
+ if i > 0 {
|
|
|
|
|
+ if err := p.AddSequence(i); err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := p.SetSequence(i, sq.name, sq.bpm, sq.bars); err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return p, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func atoi(s string) int {
|
|
|
|
|
+ n := 0
|
|
|
|
|
+ for _, c := range s {
|
|
|
|
|
+ n = n*10 + int(c-'0')
|
|
|
|
|
+ }
|
|
|
|
|
+ return n
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// zipDir zips the contents of dir with forward-slash entry names, folders
|
|
|
|
|
+// first, in name order.
|
|
|
|
|
+func zipDir(dir, path string) error {
|
|
|
|
|
+ var entries []string
|
|
|
|
|
+ err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error {
|
|
|
|
|
+ if err != nil || p == dir {
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+ rel, err := filepath.Rel(dir, p)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+ rel = filepath.ToSlash(rel)
|
|
|
|
|
+ if d.IsDir() {
|
|
|
|
|
+ rel += "/"
|
|
|
|
|
+ }
|
|
|
|
|
+ entries = append(entries, rel)
|
|
|
|
|
+ return nil
|
|
|
|
|
+ })
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+ sort.Strings(entries)
|
|
|
|
|
+
|
|
|
|
|
+ out, err := os.Create(path)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+ zw := zip.NewWriter(out)
|
|
|
|
|
+ now := time.Now()
|
|
|
|
|
+ for _, name := range entries {
|
|
|
|
|
+ h := &zip.FileHeader{Name: name, Modified: now, Method: zip.Deflate}
|
|
|
|
|
+ if name[len(name)-1] == '/' {
|
|
|
|
|
+ h.Method = zip.Store
|
|
|
|
|
+ }
|
|
|
|
|
+ w, err := zw.CreateHeader(h)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ out.Close()
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+ if h.Method == zip.Store {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ in, err := os.Open(filepath.Join(dir, filepath.FromSlash(name)))
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ out.Close()
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+ _, err = io.Copy(w, in)
|
|
|
|
|
+ in.Close()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ out.Close()
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := zw.Close(); err != nil {
|
|
|
|
|
+ out.Close()
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+ return out.Close()
|
|
|
|
|
+}
|