summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--cmd/midipak/main.go68
-rw-r--r--go.mod2
-rw-r--r--go.sum4
-rw-r--r--pkg/midipak/midipak.go55
4 files changed, 129 insertions, 0 deletions
diff --git a/cmd/midipak/main.go b/cmd/midipak/main.go
new file mode 100644
index 0000000..3db1e7e
--- /dev/null
+++ b/cmd/midipak/main.go
@@ -0,0 +1,68 @@
+// Command midipak creates a compressed pack from
+// input MIDI files.
+//
+//     midipak OUTPUT INPUTS...
+package main
+
+import (
+	"context"
+	"database/sql"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"os"
+	"path"
+
+	"github.com/klauspost/compress/zstd"
+	_ "github.com/mattn/go-sqlite3"
+	"go.terinstock.com/midifi/pkg/midipak"
+)
+
+func pack(ctx context.Context, output string, files []string) error {
+	fa := make([]midipak.File, 0, len(files))
+	for _, file := range files {
+		name := path.Base(file)
+		p, err := ioutil.ReadFile(file)
+		if err != nil {
+			return err
+		}
+
+		fa = append(fa, midipak.File{Name: name, Data: p})
+	}
+
+	enc, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(zstd.SpeedBestCompression))
+	if err != nil {
+		return err
+	}
+	defer enc.Close()
+
+	db, err := sql.Open("sqlite3", fmt.Sprintf("%s", output))
+	if err != nil {
+		return err
+	}
+	defer db.Close()
+
+	if err := midipak.Create(ctx, db); err != nil {
+		return err
+	}
+
+	if err := midipak.Pack(ctx, db, enc, fa); err != nil {
+		return err
+	}
+
+	if _, err = db.Exec("VACUUM"); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func main() {
+	ctx := context.Background()
+	output := os.Args[1]
+	files := os.Args[2:]
+
+	if err := pack(ctx, output, files); err != nil {
+		log.Print(err)
+	}
+}
diff --git a/go.mod b/go.mod
index 3096343..bc93c25 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,8 @@ go 1.14
 
 require (
 	github.com/google/go-cmp v0.2.0 // indirect
+	github.com/klauspost/compress v1.11.7
+	github.com/mattn/go-sqlite3 v1.14.6
 	github.com/pkg/errors v0.8.0 // indirect
 	github.com/pkg/term v0.0.0-20180423043932-cda20d4ac917
 	github.com/tarm/serial v0.0.0-20180114052751-eaafced92e96
diff --git a/go.sum b/go.sum
index 1af2417..308030b 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,9 @@
 github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/klauspost/compress v1.11.7 h1:0hzRabrMN4tSTvMfnL3SCv1ZGeAP23ynzodBgaHeMeg=
+github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
+github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
 github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/term v0.0.0-20180423043932-cda20d4ac917 h1:BinR73QvQveJdQ8uYZK/8MOjLADpZbI2qs/2+5rnhzQ=
diff --git a/pkg/midipak/midipak.go b/pkg/midipak/midipak.go
new file mode 100644
index 0000000..19ccde3
--- /dev/null
+++ b/pkg/midipak/midipak.go
@@ -0,0 +1,55 @@
+// midipak implements compressing and appending MIDI flles
+// to a midipak file.
+package midipak
+
+import (
+	"context"
+	"database/sql"
+)
+
+// Encoder compresses each of the input files,
+// and is expected to be goroutine safe.
+type Encoder interface {
+	EncodeAll(src, dst []byte) []byte
+}
+
+// File stores the name and data of a file.
+type File struct {
+	Name string
+	Data []byte
+}
+
+// Create initializes a new midipak.
+func Create(ctx context.Context, db *sql.DB) error {
+	sqlStmt := "CREATE TABLE files (name TEXT PRIMARY KEY, sz INTEGER NOT NULL, data BLOB);"
+
+	_, err := db.ExecContext(ctx, sqlStmt)
+	return err
+}
+
+// Pack appends the files to the midipak, after being compressed
+// with the provided encoder. Attempting to pack files that already
+// exist is an error.
+func Pack(ctx context.Context, db *sql.DB, enc Encoder, files []File) error {
+	tx, err := db.BeginTx(ctx, nil)
+	if err != nil {
+		return err
+	}
+
+	stmt, err := tx.Prepare("INSERT INTO files(name, sz, data) VALUES (?, ?, ?)")
+	if err != nil {
+		return err
+	}
+	defer stmt.Close()
+
+	for _, file := range files {
+		sz := len(file.Data)
+
+		_, err = stmt.Exec(file.Name, sz, enc.EncodeAll(file.Data, make([]byte, 0, sz)))
+		if err != nil {
+			return err
+		}
+	}
+
+	return tx.Commit()
+}