|
@@ -18,9 +18,15 @@ import (
|
|
|
"git.monoexp.com/bajdev/mpckit/internal/project"
|
|
"git.monoexp.com/bajdev/mpckit/internal/project"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+// opFunc is one change to the project, made from a flag's value.
|
|
|
|
|
+type opFunc func(p *project.Project) error
|
|
|
|
|
+
|
|
|
|
|
+// parseFunc turns a flag's value into an opFunc, or explains what is wrong.
|
|
|
|
|
+type parseFunc func(arg string) (opFunc, error)
|
|
|
|
|
+
|
|
|
type op struct {
|
|
type op struct {
|
|
|
flag, arg string
|
|
flag, arg string
|
|
|
- run func(p *project.Project) error
|
|
|
|
|
|
|
+ run opFunc
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
var ops []op
|
|
var ops []op
|
|
@@ -28,7 +34,7 @@ var ops []op
|
|
|
// opFlag is a repeatable flag whose values become ops.
|
|
// opFlag is a repeatable flag whose values become ops.
|
|
|
type opFlag struct {
|
|
type opFlag struct {
|
|
|
name string
|
|
name string
|
|
|
- parse func(arg string) (func(p *project.Project) error, error)
|
|
|
|
|
|
|
+ parse parseFunc
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (f opFlag) String() string { return "" }
|
|
func (f opFlag) String() string { return "" }
|
|
@@ -41,7 +47,7 @@ func (f opFlag) Set(arg string) error {
|
|
|
return nil
|
|
return nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-func addOp(name, usage string, parse func(arg string) (func(p *project.Project) error, error)) {
|
|
|
|
|
|
|
+func addOp(name, usage string, parse parseFunc) {
|
|
|
flag.Var(opFlag{name: name, parse: parse}, name, usage)
|
|
flag.Var(opFlag{name: name, parse: parse}, name, usage)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -80,108 +86,158 @@ func parseSeqNum(s string) (int, error) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
|
func init() {
|
|
|
- addOp("pad", "assign a WAV: `PAD=FILE` (e.g. A1=kick.wav)", func(arg string) (func(*project.Project) error, error) {
|
|
|
|
|
- label, file, ok := strings.Cut(arg, "=")
|
|
|
|
|
- if !ok {
|
|
|
|
|
- return nil, errors.New("want PAD=FILE")
|
|
|
|
|
- }
|
|
|
|
|
- slot, err := parseSlot(label)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- return nil, err
|
|
|
|
|
- }
|
|
|
|
|
- return func(p *project.Project) error { return p.AssignSample(slot, file) }, nil
|
|
|
|
|
- })
|
|
|
|
|
- addOp("tune", "tune a pad: `PAD=SEMITONES,CENTS` (e.g. B1=-24,0)", func(arg string) (func(*project.Project) error, error) {
|
|
|
|
|
- label, val, ok := strings.Cut(arg, "=")
|
|
|
|
|
- c, f, ok2 := strings.Cut(val, ",")
|
|
|
|
|
- if !ok || !ok2 {
|
|
|
|
|
- return nil, errors.New("want PAD=SEMITONES,CENTS")
|
|
|
|
|
- }
|
|
|
|
|
- slot, err := parseSlot(label)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- return nil, err
|
|
|
|
|
- }
|
|
|
|
|
- coarse, err1 := strconv.Atoi(strings.TrimSpace(c))
|
|
|
|
|
- fine, err2 := strconv.Atoi(strings.TrimSpace(f))
|
|
|
|
|
- if err1 != nil || err2 != nil {
|
|
|
|
|
- return nil, errors.New("semitones and cents must be integers")
|
|
|
|
|
- }
|
|
|
|
|
- if coarse < project.MinCoarse || coarse > project.MaxCoarse || fine < project.MinFine || fine > project.MaxFine {
|
|
|
|
|
- return nil, fmt.Errorf("tuning out of range (%d..%d semitones, %d..%d cents)", project.MinCoarse, project.MaxCoarse, project.MinFine, project.MaxFine)
|
|
|
|
|
- }
|
|
|
|
|
- return func(p *project.Project) error { return p.SetTune(slot, coarse, fine) }, nil
|
|
|
|
|
- })
|
|
|
|
|
- addOp("mute", "set a pad's mute group: `PAD=GROUP` (0 is off)", func(arg string) (func(*project.Project) error, error) {
|
|
|
|
|
- label, val, ok := strings.Cut(arg, "=")
|
|
|
|
|
- if !ok {
|
|
|
|
|
- return nil, errors.New("want PAD=GROUP")
|
|
|
|
|
- }
|
|
|
|
|
- slot, err := parseSlot(label)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- return nil, err
|
|
|
|
|
|
|
+ for _, f := range []struct {
|
|
|
|
|
+ name, usage string
|
|
|
|
|
+ parse parseFunc
|
|
|
|
|
+ }{
|
|
|
|
|
+ {"pad", "assign a WAV: `PAD=FILE` (e.g. A1=kick.wav)", parsePadOp},
|
|
|
|
|
+ {"tune", "tune a pad: `PAD=SEMITONES,CENTS` (e.g. B1=-24,0)", parseTuneOp},
|
|
|
|
|
+ {"mute", "set a pad's mute group: `PAD=GROUP` (0 is off)", parseMuteOp},
|
|
|
|
|
+ {"clear", "clear a pad: `PAD`", parseClearOp},
|
|
|
|
|
+ {"copy", "copy pads: `FROM:TO`", parseCopyOp},
|
|
|
|
|
+ {"move", "move pads: `FROM:TO`", parseMoveOp},
|
|
|
|
|
+ {"swap", "swap pads: `FROM:TO`", parseSwapOp},
|
|
|
|
|
+ {"seq", "set a sequence, adding it if needed: `N=NAME,BPM,BARS` (N is 1-128)", parseSeqOp},
|
|
|
|
|
+ {"rmseq", "remove a sequence: `N`", parseRmseqOp},
|
|
|
|
|
+ } {
|
|
|
|
|
+ addOp(f.name, f.usage, f.parse)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// parsePadOp reads -pad PAD=FILE.
|
|
|
|
|
+func parsePadOp(arg string) (opFunc, error) {
|
|
|
|
|
+ label, file, ok := strings.Cut(arg, "=")
|
|
|
|
|
+ if !ok {
|
|
|
|
|
+ return nil, errors.New("want PAD=FILE")
|
|
|
|
|
+ }
|
|
|
|
|
+ slot, err := parseSlot(label)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ return func(p *project.Project) error { return p.AssignSample(slot, file) }, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// parseTuneOp reads -tune PAD=SEMITONES,CENTS.
|
|
|
|
|
+func parseTuneOp(arg string) (opFunc, error) {
|
|
|
|
|
+ label, val, ok := strings.Cut(arg, "=")
|
|
|
|
|
+ c, f, ok2 := strings.Cut(val, ",")
|
|
|
|
|
+ if !ok || !ok2 {
|
|
|
|
|
+ return nil, errors.New("want PAD=SEMITONES,CENTS")
|
|
|
|
|
+ }
|
|
|
|
|
+ slot, err := parseSlot(label)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ coarse, err1 := strconv.Atoi(strings.TrimSpace(c))
|
|
|
|
|
+ fine, err2 := strconv.Atoi(strings.TrimSpace(f))
|
|
|
|
|
+ if err1 != nil || err2 != nil {
|
|
|
|
|
+ return nil, errors.New("semitones and cents must be integers")
|
|
|
|
|
+ }
|
|
|
|
|
+ if coarse < project.MinCoarse || coarse > project.MaxCoarse || fine < project.MinFine || fine > project.MaxFine {
|
|
|
|
|
+ return nil, fmt.Errorf("tuning out of range (%d..%d semitones, %d..%d cents)",
|
|
|
|
|
+ project.MinCoarse, project.MaxCoarse, project.MinFine, project.MaxFine)
|
|
|
|
|
+ }
|
|
|
|
|
+ return func(p *project.Project) error { return p.SetTune(slot, coarse, fine) }, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// parseMuteOp reads -mute PAD=GROUP.
|
|
|
|
|
+func parseMuteOp(arg string) (opFunc, error) {
|
|
|
|
|
+ label, val, ok := strings.Cut(arg, "=")
|
|
|
|
|
+ if !ok {
|
|
|
|
|
+ return nil, errors.New("want PAD=GROUP")
|
|
|
|
|
+ }
|
|
|
|
|
+ slot, err := parseSlot(label)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ group, err := strconv.Atoi(val)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, errors.New("group must be an integer")
|
|
|
|
|
+ }
|
|
|
|
|
+ return func(p *project.Project) error { return p.SetMuteGroup(slot, group) }, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// parseClearOp reads -clear PAD.
|
|
|
|
|
+func parseClearOp(arg string) (opFunc, error) {
|
|
|
|
|
+ slot, err := parseSlot(arg)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ return func(p *project.Project) error { return p.ClearPad(slot) }, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// parseCopyOp reads -copy FROM:TO.
|
|
|
|
|
+func parseCopyOp(arg string) (opFunc, error) {
|
|
|
|
|
+ from, to, err := parsePair(arg)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ return func(p *project.Project) error { return p.CopyPad(from, to) }, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// parseMoveOp reads -move FROM:TO. Recorded notes stay on FROM, as in the
|
|
|
|
|
+// app, so it warns when there are any.
|
|
|
|
|
+func parseMoveOp(arg string) (opFunc, error) {
|
|
|
|
|
+ from, to, err := parsePair(arg)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ return func(p *project.Project) error {
|
|
|
|
|
+ if p.PadHasNotes(from) {
|
|
|
|
|
+ fmt.Fprintf(os.Stderr, "warning: %s has recorded notes; they stay on %s\n",
|
|
|
|
|
+ project.SlotName(from), project.SlotName(from))
|
|
|
}
|
|
}
|
|
|
- g, err := strconv.Atoi(val)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- return nil, errors.New("group must be an integer")
|
|
|
|
|
|
|
+ return p.MovePad(from, to)
|
|
|
|
|
+ }, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// parseSwapOp reads -swap FROM:TO.
|
|
|
|
|
+func parseSwapOp(arg string) (opFunc, error) {
|
|
|
|
|
+ from, to, err := parsePair(arg)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ return func(p *project.Project) error { return p.SwapPads(from, to) }, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// parseSeqOp reads -seq N=NAME,BPM,BARS. The sequence is added first if
|
|
|
|
|
+// slot N is empty.
|
|
|
|
|
+func parseSeqOp(arg string) (opFunc, error) {
|
|
|
|
|
+ num, val, ok := strings.Cut(arg, "=")
|
|
|
|
|
+ parts := strings.Split(val, ",")
|
|
|
|
|
+ if !ok || len(parts) != 3 {
|
|
|
|
|
+ return nil, errors.New("want N=NAME,BPM,BARS")
|
|
|
|
|
+ }
|
|
|
|
|
+ slot, err := parseSeqNum(num)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ bpm, err1 := strconv.ParseFloat(strings.TrimSpace(parts[1]), 64)
|
|
|
|
|
+ bars, err2 := strconv.Atoi(strings.TrimSpace(parts[2]))
|
|
|
|
|
+ if err1 != nil || err2 != nil {
|
|
|
|
|
+ return nil, errors.New("BPM must be a number and BARS an integer")
|
|
|
|
|
+ }
|
|
|
|
|
+ return func(p *project.Project) error {
|
|
|
|
|
+ exists := false
|
|
|
|
|
+ for _, seq := range p.Sequences() {
|
|
|
|
|
+ exists = exists || seq.Slot == slot
|
|
|
}
|
|
}
|
|
|
- return func(p *project.Project) error { return p.SetMuteGroup(slot, g) }, nil
|
|
|
|
|
- })
|
|
|
|
|
- addOp("clear", "clear a pad: `PAD`", func(arg string) (func(*project.Project) error, error) {
|
|
|
|
|
- slot, err := parseSlot(arg)
|
|
|
|
|
- return func(p *project.Project) error { return p.ClearPad(slot) }, err
|
|
|
|
|
- })
|
|
|
|
|
- for _, name := range []string{"copy", "move", "swap"} {
|
|
|
|
|
- addOp(name, name+" pads: `FROM:TO`", func(arg string) (func(*project.Project) error, error) {
|
|
|
|
|
- from, to, err := parsePair(arg)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- return nil, err
|
|
|
|
|
|
|
+ if !exists {
|
|
|
|
|
+ if err := p.AddSequence(slot); err != nil {
|
|
|
|
|
+ return err
|
|
|
}
|
|
}
|
|
|
- return func(p *project.Project) error {
|
|
|
|
|
- switch name {
|
|
|
|
|
- case "copy":
|
|
|
|
|
- return p.CopyPad(from, to)
|
|
|
|
|
- case "move":
|
|
|
|
|
- if p.PadHasNotes(from) {
|
|
|
|
|
- fmt.Fprintf(os.Stderr, "warning: %s has recorded notes; they stay on %s\n", project.SlotName(from), project.SlotName(from))
|
|
|
|
|
- }
|
|
|
|
|
- return p.MovePad(from, to)
|
|
|
|
|
- }
|
|
|
|
|
- return p.SwapPads(from, to)
|
|
|
|
|
- }, nil
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
- addOp("seq", "set a sequence, adding it if needed: `N=NAME,BPM,BARS` (N is 1-128)", func(arg string) (func(*project.Project) error, error) {
|
|
|
|
|
- num, val, ok := strings.Cut(arg, "=")
|
|
|
|
|
- parts := strings.Split(val, ",")
|
|
|
|
|
- if !ok || len(parts) != 3 {
|
|
|
|
|
- return nil, errors.New("want N=NAME,BPM,BARS")
|
|
|
|
|
- }
|
|
|
|
|
- slot, err := parseSeqNum(num)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- return nil, err
|
|
|
|
|
- }
|
|
|
|
|
- bpm, err1 := strconv.ParseFloat(strings.TrimSpace(parts[1]), 64)
|
|
|
|
|
- bars, err2 := strconv.Atoi(strings.TrimSpace(parts[2]))
|
|
|
|
|
- if err1 != nil || err2 != nil {
|
|
|
|
|
- return nil, errors.New("BPM must be a number and BARS an integer")
|
|
|
|
|
}
|
|
}
|
|
|
- return func(p *project.Project) error {
|
|
|
|
|
- exists := false
|
|
|
|
|
- for _, s := range p.Sequences() {
|
|
|
|
|
- exists = exists || s.Slot == slot
|
|
|
|
|
- }
|
|
|
|
|
- if !exists {
|
|
|
|
|
- if err := p.AddSequence(slot); err != nil {
|
|
|
|
|
- return err
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- return p.SetSequence(slot, parts[0], bpm, bars)
|
|
|
|
|
- }, nil
|
|
|
|
|
- })
|
|
|
|
|
- addOp("rmseq", "remove a sequence: `N`", func(arg string) (func(*project.Project) error, error) {
|
|
|
|
|
- slot, err := parseSeqNum(arg)
|
|
|
|
|
- return func(p *project.Project) error { return p.RemoveSequence(slot) }, err
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ return p.SetSequence(slot, parts[0], bpm, bars)
|
|
|
|
|
+ }, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// parseRmseqOp reads -rmseq N.
|
|
|
|
|
+func parseRmseqOp(arg string) (opFunc, error) {
|
|
|
|
|
+ slot, err := parseSeqNum(arg)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ return func(p *project.Project) error { return p.RemoveSequence(slot) }, nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
func main() {
|
|
@@ -202,39 +258,54 @@ func main() {
|
|
|
fail(errors.New("give exactly one of -out or -zip"))
|
|
fail(errors.New("give exactly one of -out or -zip"))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ p := openProject(*load, *name)
|
|
|
|
|
+ for _, o := range ops {
|
|
|
|
|
+ if err := o.run(p); err != nil {
|
|
|
|
|
+ fail(fmt.Errorf("-%s %s: %w", o.flag, o.arg, err))
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ dest := write(p, *out, *zipPath, *force)
|
|
|
|
|
+ summary(p)
|
|
|
|
|
+ fmt.Println("wrote", dest)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// openProject loads the project at load, or starts a blank one, and names
|
|
|
|
|
+// it. It exits on error.
|
|
|
|
|
+func openProject(load, name string) *project.Project {
|
|
|
var p *project.Project
|
|
var p *project.Project
|
|
|
var err error
|
|
var err error
|
|
|
- if *load != "" {
|
|
|
|
|
|
|
+ if load != "" {
|
|
|
var warnings []string
|
|
var warnings []string
|
|
|
- p, warnings, err = project.Load(*load)
|
|
|
|
|
|
|
+ p, warnings, err = project.Load(load)
|
|
|
for _, w := range warnings {
|
|
for _, w := range warnings {
|
|
|
fmt.Fprintln(os.Stderr, "warning:", w)
|
|
fmt.Fprintln(os.Stderr, "warning:", w)
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
- if *name == "" {
|
|
|
|
|
|
|
+ if name == "" {
|
|
|
fail(errors.New("-name is required for a new project"))
|
|
fail(errors.New("-name is required for a new project"))
|
|
|
}
|
|
}
|
|
|
- p, err = project.New(*name)
|
|
|
|
|
|
|
+ p, err = project.New(name)
|
|
|
}
|
|
}
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
fail(err)
|
|
fail(err)
|
|
|
}
|
|
}
|
|
|
- if *name != "" {
|
|
|
|
|
- if err := p.SetName(*name); err != nil {
|
|
|
|
|
|
|
+ if name != "" {
|
|
|
|
|
+ if err := p.SetName(name); err != nil {
|
|
|
fail(err)
|
|
fail(err)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- for _, o := range ops {
|
|
|
|
|
- if err := o.run(p); err != nil {
|
|
|
|
|
- fail(fmt.Errorf("-%s %s: %w", o.flag, o.arg, err))
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return p
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
|
|
+// write exports p to the folder out or the zip file zipPath, whichever is
|
|
|
|
|
+// set, and returns where it went. It exits on error.
|
|
|
|
|
+func write(p *project.Project, out, zipPath string, force bool) string {
|
|
|
var dest string
|
|
var dest string
|
|
|
- if *out != "" {
|
|
|
|
|
- dest, err = export.Folder(p, *out, *force)
|
|
|
|
|
|
|
+ var err error
|
|
|
|
|
+ if out != "" {
|
|
|
|
|
+ dest, err = export.Folder(p, out, force)
|
|
|
} else {
|
|
} else {
|
|
|
- dest, err = *zipPath, export.Zip(p, *zipPath, *force)
|
|
|
|
|
|
|
+ dest, err = zipPath, export.Zip(p, zipPath, force)
|
|
|
}
|
|
}
|
|
|
var exists *export.ExistsError
|
|
var exists *export.ExistsError
|
|
|
if errors.As(err, &exists) {
|
|
if errors.As(err, &exists) {
|
|
@@ -243,8 +314,7 @@ func main() {
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
fail(err)
|
|
fail(err)
|
|
|
}
|
|
}
|
|
|
- summary(p)
|
|
|
|
|
- fmt.Println("wrote", dest)
|
|
|
|
|
|
|
+ return dest
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func summary(p *project.Project) {
|
|
func summary(p *project.Project) {
|