From f07276b8585ef003f5d85c65d9227fda758ec589 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Wed, 15 Apr 2026 12:43:39 -0500 Subject: [PATCH] start conversion to Go --- cmd/vagparser/main.go | 99 ++++++++++++++++++ go.mod | 5 + go.sum | 10 ++ musparser.py => python/musparser.py | 0 requirements.txt => python/requirements.txt | 0 sbkparser.py => python/sbkparser.py | 0 vagparser.py => python/vagparser.py | 0 vag.go | 109 ++++++++++++++++++++ 8 files changed, 223 insertions(+) create mode 100644 cmd/vagparser/main.go create mode 100644 go.mod create mode 100644 go.sum rename musparser.py => python/musparser.py (100%) rename requirements.txt => python/requirements.txt (100%) rename sbkparser.py => python/sbkparser.py (100%) rename vagparser.py => python/vagparser.py (100%) create mode 100644 vag.go diff --git a/cmd/vagparser/main.go b/cmd/vagparser/main.go new file mode 100644 index 0000000..e780420 --- /dev/null +++ b/cmd/vagparser/main.go @@ -0,0 +1,99 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/jwetzell/jakaudiotools" + "github.com/urfave/cli/v3" +) + +func main() { + cmd := &cli.Command{ + Name: "vagparser", + Usage: "parse VAG files", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "wad", + Value: "", + Usage: "path to JAK VAGWAD file", + Required: true, + }, + &cli.StringFlag{ + Name: "dict", + Value: "", + Usage: "path to JAK VAGDIR file to lookup names", + Required: true, + }, + &cli.Int8Flag{ + Name: "game", + Value: 1, + Usage: "the JAK game the VAG file is from (1,2,3)", + Validator: func(flag int8) error { + if flag != 1 && flag != 2 && flag != 3 { + return fmt.Errorf("game must be either 1, 2, or 3") + } + return nil + }, + Required: true, + }, + &cli.StringFlag{ + Name: "output", + Usage: "directory to output parsed VAG files to (if not specified, will just print names and counts)", + Value: "", + Required: false, + }, + }, + Action: func(ctx context.Context, cmd *cli.Command) error { + wadPath := cmd.String("wad") + wadData, err := os.ReadFile(wadPath) + if err != nil { + return fmt.Errorf("failed to read WAD file: %w", err) + } + + dirPath := cmd.String("dict") + dirData, err := os.ReadFile(dirPath) + if err != nil { + return fmt.Errorf("failed to read dict file: %w", err) + } + + game := cmd.Int8("game") + + entries, err := jakaudiotools.ParseVAG(wadData, dirData, game) + if err != nil { + return fmt.Errorf("failed to parse VAG file: %w", err) + } + + outputPath := cmd.String("output") + if outputPath != "" { + if _, err := os.Stat(outputPath); os.IsNotExist(err) { + err := os.MkdirAll(outputPath, 0755) + if err != nil { + return fmt.Errorf("failed to create output directory: %w", err) + } + } + for _, entry := range entries { + entryPath := fmt.Sprintf("%s/%s.VAGp", outputPath, entry.Name) + fmt.Printf("writing VAG file to %s\n", entryPath) + err := os.WriteFile(entryPath, entry.Data, 0644) + if err != nil { + return fmt.Errorf("failed to write VAG file: %w", err) + } + } + fmt.Printf("wrote %d VAG files to %s\n", len(entries), outputPath) + } else { + for _, entry := range entries { + fmt.Printf("VAG file named %s found\n", entry.Name) + } + fmt.Printf("found %d VAG entries\n", len(entries)) + } + return nil + }, + } + + if err := cmd.Run(context.Background(), os.Args); err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9784964 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/jwetzell/jakaudiotools + +go 1.26.2 + +require github.com/urfave/cli/v3 v3.8.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e8ab57f --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI= +github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/musparser.py b/python/musparser.py similarity index 100% rename from musparser.py rename to python/musparser.py diff --git a/requirements.txt b/python/requirements.txt similarity index 100% rename from requirements.txt rename to python/requirements.txt diff --git a/sbkparser.py b/python/sbkparser.py similarity index 100% rename from sbkparser.py rename to python/sbkparser.py diff --git a/vagparser.py b/python/vagparser.py similarity index 100% rename from vagparser.py rename to python/vagparser.py diff --git a/vag.go b/vag.go new file mode 100644 index 0000000..6d0bb80 --- /dev/null +++ b/vag.go @@ -0,0 +1,109 @@ +package jakaudiotools + +import ( + "bytes" + "fmt" +) + +type VAGEntry struct { + Name string + Data []byte +} + +func ParseVAG(vagWAD []byte, vagDict []byte, game int8) ([]VAGEntry, error) { + entries := []VAGEntry{} + separator := "VAGp" + dictionaryEntrySize := 12 + + if game != 1 { + separator = "pGAV" + dictionaryEntrySize = 16 + } + + magic := []byte(separator) + stereo := []byte("Stereo") + mono := []byte("Mono") + + stereoInterleaveSize := 8192 + + fileStart := 0 + firstTime := true + previousFileStart := 0 + fileCount := 0 + skipInterleave := false + + vagDictLookup := processVAGDict(vagDict, dictionaryEntrySize) + + for fileStart != -1 { + previousFileStart = fileStart + if firstTime { + location := bytes.Index(vagWAD[fileStart:], magic) + if location != -1 { + fileStart += location + } else { + fileStart = -1 + } + firstTime = false + } else { + //if last time the start of a stereo file was found we can start looking for the next file starting after the interleave + if skipInterleave { + location := bytes.Index(vagWAD[fileStart+len(magic)+stereoInterleaveSize:], magic) + if location != -1 { + fileStart += len(magic) + stereoInterleaveSize + location + } else { + fileStart = -1 + } + skipInterleave = false + } else { + location := bytes.Index(vagWAD[fileStart+len(magic):], magic) + if location != -1 { + fileStart += len(magic) + location + } else { + fileStart = -1 + } + } + } + stereoLocation := bytes.Index(vagWAD, stereo) + monoLocation := bytes.Index(vagWAD, mono) + + audioType := "mono" + if stereoLocation != -1 && (monoLocation == -1 || stereoLocation < monoLocation) { + audioType = "stereo" + } + + if audioType == "stereo" && !skipInterleave { + skipInterleave = true + } + + if fileStart >= 0 && fileStart != previousFileStart { + fileCount += 1 + outname := fmt.Sprintf("%d", fileCount) + if vagDict != nil && game <= 2 { + if fileCount-1 >= len(vagDictLookup) { + return nil, fmt.Errorf("file count exceeds number of entries in VAG dict") + } + outname = vagDictLookup[fileCount-1] + } + fmt.Printf("%s file named %s found\n", audioType, outname) + entry := VAGEntry{ + Name: outname, + Data: vagWAD[previousFileStart:fileStart], + } + entries = append(entries, entry) + } + + } + return entries, nil +} + +func processVAGDict(vagDict []byte, entrySize int) []string { + names := []string{} + for i := 0; i < len(vagDict); i += entrySize { + if i+entrySize > len(vagDict) { + break + } + name := string(vagDict[i+4 : i+entrySize]) + names = append(names, name) + } + return names +}