From 3155973decb282da7bcfe5d3726dafa428d51313 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Wed, 15 Apr 2026 23:15:43 -0500 Subject: [PATCH] fill out majority of MUS parsing --- cmd/musparser/main.go | 52 ++++++ mus.go | 377 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 429 insertions(+) create mode 100644 cmd/musparser/main.go create mode 100644 mus.go diff --git a/cmd/musparser/main.go b/cmd/musparser/main.go new file mode 100644 index 0000000..1e5dc75 --- /dev/null +++ b/cmd/musparser/main.go @@ -0,0 +1,52 @@ +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: "mus", + Value: "", + Usage: "path to JAK MUS file", + Required: true, + }, + &cli.StringFlag{ + Name: "output", + Usage: "directory to output parsed MUS files to (if not specified, will just print names and counts)", + Value: "", + Required: false, + }, + }, + Action: func(ctx context.Context, cmd *cli.Command) error { + musPath := cmd.String("mus") + musData, err := os.ReadFile(musPath) + if err != nil { + return fmt.Errorf("failed to read WAD file: %w", err) + } + + musFile, err := jakaudiotools.ParseMUS(musData) + if err != nil { + return fmt.Errorf("failed to parse MUS file: %w", err) + } + + fmt.Printf("found MUS %+v\n", musFile) + + return nil + }, + } + + if err := cmd.Run(context.Background(), os.Args); err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } +} diff --git a/mus.go b/mus.go new file mode 100644 index 0000000..e1ec655 --- /dev/null +++ b/mus.go @@ -0,0 +1,377 @@ +package jakaudiotools + +import ( + "fmt" +) + +type MUSFile struct { + Version uint32 + SubVersion uint32 + MinorVersion uint32 + a uint32 + SoundBankStart uint32 + SoundBankSize uint32 + SequenceDataStart uint32 + b uint32 +} + +func readUint32(data []byte, offset int) uint32 { + return uint32(data[offset]) | uint32(data[offset+1])<<8 | uint32(data[offset+2])<<16 | uint32(data[offset+3])<<24 +} + +// MUS = namedtuple('MUS','version subversion minorversion something soundBankStart soundBankSize sequenceDataStart s3') +// MUSStructString= "8I" +func (mfe *MUSFile) Unmarshal(data []byte) error { + if len(data) < 32 { + return fmt.Errorf("data too short to unmarshal MUSFileEntry") + } + mfe.Version = readUint32(data, 0) + mfe.SubVersion = readUint32(data, 4) + mfe.MinorVersion = readUint32(data, 8) + mfe.a = readUint32(data, 12) + mfe.SoundBankStart = readUint32(data, 16) + mfe.SoundBankSize = readUint32(data, 20) + mfe.SequenceDataStart = readUint32(data, 24) + mfe.b = readUint32(data, 28) + return nil +} + +type SBV2 struct { + Signature string + Version uint32 + HVersion uint32 + Name string + a uint32 + b uint16 + InstrumentCount uint16 + RegionCount uint16 + NumSample uint16 + f uint32 + InstrumentOffset uint32 + RegionOffset uint32 + i uint32 + SoundBankSize uint32 + k uint32 + l uint32 + ExtName string + m uint16 + n uint16 + o uint16 + p uint16 + q uint32 +} + +// #struct type 4s I I 4s I H H H H I I I I I I I 12s H H H H I +// SBV2 = namedtuple('SBV2', 'signature version hversion name a b nMap nSMap nSample f instrumentOffset regionOffset i soundBankSize k l extname m n o p q') +// SBV2StructString = "4s2I4sI4H7I12s4HI" +func (sbv2 *SBV2) Unmarshal(data []byte) error { + if len(data) < 158 { + return fmt.Errorf("data too short to unmarshal SBV2") + } + sbv2.Signature = string(data[0:4]) + sbv2.Version = readUint32(data, 4) + sbv2.HVersion = readUint32(data, 8) + sbv2.Name = string(data[12:16]) + sbv2.a = readUint32(data, 16) + sbv2.b = uint16(data[20]) | uint16(data[21])<<8 + sbv2.InstrumentCount = uint16(data[22]) | uint16(data[23])<<8 + sbv2.RegionCount = uint16(data[24]) | uint16(data[25])<<8 + sbv2.NumSample = uint16(data[26]) | uint16(data[27])<<8 + sbv2.f = readUint32(data, 28) + sbv2.InstrumentOffset = readUint32(data, 32) + sbv2.RegionOffset = readUint32(data, 36) + sbv2.i = readUint32(data, 40) + sbv2.SoundBankSize = readUint32(data, 44) + sbv2.k = readUint32(data, 48) + sbv2.l = readUint32(data, 52) + sbv2.ExtName = string(data[56:68]) + sbv2.m = uint16(data[68]) | uint16(data[69])<<8 + sbv2.n = uint16(data[70]) | uint16(data[71])<<8 + sbv2.o = uint16(data[72]) | uint16(data[73])<<8 + sbv2.p = uint16(data[74]) | uint16(data[75])<<8 + sbv2.q = readUint32(data, 76) + return nil +} + +type Instrument struct { + RegionCount uint8 + Volume uint8 + zero uint16 + RegionOffset uint32 +} + +// #struct type B B H I +// Instrument = namedtuple('Instrument','regionCount volume zero regionOffset') +func (inst *Instrument) Unmarshal(data []byte) error { + if len(data) < 8 { + return fmt.Errorf("data too short to unmarshal Instrument") + } + inst.RegionCount = data[0] + inst.Volume = data[1] + inst.zero = uint16(data[2]) | uint16(data[3])<<8 + inst.RegionOffset = readUint32(data, 4) + return nil +} + +// #struct type B B B B h B B H B B B B H I I +// Region = namedtuple('Region','type volume1 a b pan c volume2 zero d e f g loopFlag sampleOffset sampleId') +// RegionStructString = "=BBBBhBBHBBBBHII" +type Region struct { + Type uint8 + Volume1 uint8 + a uint8 + b uint8 + Pan int16 + c uint8 + Volume2 uint8 + zero uint16 + d uint8 + e uint8 + f uint8 + g uint8 + LoopFlag uint16 + SampleOffset uint32 + SampleId uint32 +} + +func (r *Region) Unmarshal(data []byte) error { + if len(data) < 24 { + return fmt.Errorf("data too short to unmarshal Region") + } + r.Type = data[0] + r.Volume1 = data[1] + r.a = data[2] + r.b = data[3] + r.Pan = int16(data[4]) | int16(data[5])<<8 + r.c = data[6] + r.Volume2 = data[7] + r.zero = uint16(data[8]) | uint16(data[9])<<8 + r.d = data[10] + r.e = data[11] + r.f = data[12] + r.g = data[13] + r.LoopFlag = uint16(data[14]) | uint16(data[15])<<8 + r.SampleOffset = readUint32(data, 16) + r.SampleId = readUint32(data, 20) + return nil +} + +// #struct type I I I I +// SEQ = namedtuple('SEQ','version subversion minorversion sequenceDataSize') +// SEQStructString = "4I" + +// #struct type 4s H H I I 4s I I I I I H B B +// MID = namedtuple('MID','signature a b c d name e midiDataOffset g midiTempo division zero trackIndex trackCount') +// MIDStructString = "4s2H2I4s5IH2B" + +// #struct type 4s H B B 4s I +// MMID = namedtuple('MMID','signature type a trackCount name zero') +// MMIDStructString = "4sH2B4sI" + +func ParseMUS(data []byte) (*MUSFile, error) { + + // nameLength := 20 + // soundsStart := 24 + // soundsLength := 20 + + offset := 0 + musFile := &MUSFile{} + err := musFile.Unmarshal(data) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal MUS file: %w", err) + } + offset += 32 + sbv2Start := offset + sbv2Info := &SBV2{} + err = sbv2Info.Unmarshal(data[offset:]) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal MUS file: %w", err) + } + + fmt.Printf("SBV2 info: %+v\n", sbv2Info) + + instruments := []Instrument{} + offset = sbv2Start + int(sbv2Info.InstrumentOffset) + for i := 0; i < int(sbv2Info.InstrumentCount); i++ { + instrument := &Instrument{} + err := instrument.Unmarshal(data[offset:]) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal instrument at offset %d: %w", offset, err) + } + fmt.Printf("instrument %d: %+v\n", i, instrument) + instruments = append(instruments, *instrument) + offset += 8 + } + fmt.Printf("found %d instruments\n", len(instruments)) + + regions := []Region{} + + offset = sbv2Start + int(sbv2Info.RegionOffset) + for i := 0; i < int(sbv2Info.RegionCount); i++ { + region := &Region{} + err := region.Unmarshal(data[offset:]) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal region at offset %d: %w", offset, err) + } + fmt.Printf("region %d: %+v\n", i, region) + regions = append(regions, *region) + offset += 24 + } + + // TODO(jwetzell): parse samples in regions + + sequenceOffset := int(musFile.SequenceDataStart) + + sequence := &Sequence{} + err = sequence.Unmarshal(data[sequenceOffset:]) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal sequence at offset %d: %w", sequenceOffset, err) + } + fmt.Printf("sequence: %+v\n", sequence) + + offset = sequenceOffset + 16 + + midBlockType := string(data[offset : offset+4]) + fmt.Printf("mid block type: %s\n", midBlockType) + + if midBlockType == "MID " { + midBlock := &MID{} + err = midBlock.Unmarshal(data[offset:]) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal MID block at offset %d: %w", offset, err) + } + fmt.Printf("MID block: %+v\n", midBlock) + // midBlocks = append(midBlocks, *midBlock) + } else if midBlockType == "MMID" { + mmidBlockStart := offset + mmidBlock := &MMID{ + MIDBlocks: []MID{}, + } + err = mmidBlock.Unmarshal(data[offset:]) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal MMID block at offset %d: %w", offset, err) + } + fmt.Printf("MMID block: %+v\n", mmidBlock) + + offset += 16 + + for i := 0; i < int(mmidBlock.TrackCount); i++ { + blockOffset := int(readUint32(data[offset:], 0)) + mmidBlockStart + fmt.Printf("Found MID block at offset %d\n", blockOffset) + + midBlock := &MID{ + BlockOffset: blockOffset, + } + err = midBlock.Unmarshal(data[blockOffset:]) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal MID block at offset %d: %w", blockOffset, err) + } + mmidBlock.MIDBlocks = append(mmidBlock.MIDBlocks, *midBlock) + offset += 4 + } + for i, midBlock := range mmidBlock.MIDBlocks { + blockOffset := midBlock.BlockOffset + if i != len(mmidBlock.MIDBlocks)-1 { + nextBlockOffset := mmidBlock.MIDBlocks[i+1].BlockOffset + midBlock.MidiData = data[blockOffset+int(midBlock.MidiDataOffset) : nextBlockOffset] + } else { + midBlock.MidiData = data[blockOffset+int(midBlock.MidiDataOffset):] + } + } + } else { + return nil, fmt.Errorf("unknown mid block type: %s", midBlockType) + } + + return musFile, nil +} + +// #struct type I I I I +// SEQ = namedtuple('SEQ','version subversion minorversion sequenceDataSize') +// SEQStructString = "4I" +type Sequence struct { + Version uint32 + SubVersion uint32 + MinorVersion uint32 + SequenceDataSize uint32 +} + +func (s *Sequence) Unmarshal(data []byte) error { + if len(data) < 16 { + return fmt.Errorf("data too short to unmarshal Sequence") + } + s.Version = readUint32(data, 0) + s.SubVersion = readUint32(data, 4) + s.MinorVersion = readUint32(data, 8) + s.SequenceDataSize = readUint32(data, 12) + return nil +} + +// #struct type 4s H H I I 4s I I I I I H B B +// MID = namedtuple('MID','signature a b c d name e midiDataOffset g midiTempo division zero trackIndex trackCount') +// MIDStructString = "4s2H2I4s5IH2B" +type MID struct { + Signature string + a uint16 + b uint16 + c uint32 + d uint32 + Name string + e uint32 + MidiDataOffset uint32 + g uint32 + MidiTempo uint32 + Division uint32 + zero uint16 + TrackIndex uint8 + TrackCount uint8 + + BlockOffset int + MidiData []byte +} + +func (m *MID) Unmarshal(data []byte) error { + if len(data) < 44 { + return fmt.Errorf("data too short to unmarshal MID") + } + m.Signature = string(data[0:4]) + m.a = uint16(data[4]) | uint16(data[5])<<8 + m.b = uint16(data[6]) | uint16(data[7])<<8 + m.c = readUint32(data, 8) + m.d = readUint32(data, 12) + m.Name = string(data[16:20]) + m.e = readUint32(data, 20) + m.MidiDataOffset = readUint32(data, 24) + m.g = readUint32(data, 28) + m.MidiTempo = readUint32(data, 32) + m.Division = readUint32(data, 36) + m.zero = uint16(data[40]) | uint16(data[41])<<8 + m.TrackIndex = data[42] + m.TrackCount = data[43] + return nil +} + +// #struct type 4s H B B 4s I +// MMID = namedtuple('MMID','signature type a trackCount name zero') +// MMIDStructString = "4sH2B4sI" +type MMID struct { + Signature string + Type uint16 + a uint8 + TrackCount uint8 + Name string + zero uint32 + MIDBlocks []MID +} + +func (mmid *MMID) Unmarshal(data []byte) error { + if len(data) < 16 { + return fmt.Errorf("data too short to unmarshal MMID") + } + mmid.Signature = string(data[0:4]) + mmid.Type = uint16(data[4]) | uint16(data[5])<<8 + mmid.a = data[6] + mmid.TrackCount = data[7] + mmid.Name = string(data[8:12]) + mmid.zero = readUint32(data, 12) + return nil +}