| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272 |
- // 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()
- }
|