|
|
|
package qmc
|
|
|
|
|
|
|
|
import (
|
|
|
|
bytes "bytes"
|
|
|
|
"encoding/binary"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
|
|
|
type MusicExTagV1 struct {
|
|
|
|
SongID uint32 // Song ID
|
|
|
|
Unknown1 uint32 // unused & unknown
|
|
|
|
Unknown2 uint32 // unused & unknown
|
|
|
|
MediaID string // Media ID
|
|
|
|
MediaFileName string // real file name
|
|
|
|
Unknown3 uint32 // unused; uninitialized memory?
|
|
|
|
|
|
|
|
// 16 byte at the end of tag.
|
|
|
|
// TagSize should be respected when parsing.
|
|
|
|
TagSize uint32 // 19.57: fixed value: 0xC0
|
|
|
|
TagVersion uint32 // 19.57: fixed value: 0x01
|
|
|
|
TagMagic []byte // fixed value "musicex\0" (8 bytes)
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewMusicExTag(f io.ReadSeeker) (*MusicExTagV1, error) {
|
|
|
|
_, err := f.Seek(-16, io.SeekEnd)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("musicex seek error: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
buffer := make([]byte, 16)
|
|
|
|
bytesRead, err := f.Read(buffer)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("get musicex error: %w", err)
|
|
|
|
}
|
|
|
|
if bytesRead != 16 {
|
|
|
|
return nil, fmt.Errorf("MusicExV1: read %d bytes (expected %d)", bytesRead, 16)
|
|
|
|
}
|
|
|
|
|
|
|
|
tag := &MusicExTagV1{
|
|
|
|
TagSize: binary.LittleEndian.Uint32(buffer[0x00:0x04]),
|
|
|
|
TagVersion: binary.LittleEndian.Uint32(buffer[0x04:0x08]),
|
|
|
|
TagMagic: buffer[0x04:0x0C],
|
|
|
|
}
|
|
|
|
|
|
|
|
if !bytes.Equal(tag.TagMagic, []byte("musicex\x00")) {
|
|
|
|
return nil, errors.New("MusicEx magic mismatch")
|
|
|
|
}
|
|
|
|
if tag.TagVersion != 1 {
|
|
|
|
return nil, errors.New(fmt.Sprintf("unsupported musicex tag version. expecting 1, got %d", tag.TagVersion))
|
|
|
|
}
|
|
|
|
|
|
|
|
if tag.TagSize < 0xC0 {
|
|
|
|
return nil, errors.New(fmt.Sprintf("unsupported musicex tag size. expecting at least 0xC0, got 0x%02x", tag.TagSize))
|
|
|
|
}
|
|
|
|
|
|
|
|
buffer = make([]byte, tag.TagSize)
|
|
|
|
bytesRead, err = f.Read(buffer)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if uint32(bytesRead) != tag.TagSize {
|
|
|
|
return nil, fmt.Errorf("MusicExV1: read %d bytes (expected %d)", bytesRead, tag.TagSize)
|
|
|
|
}
|
|
|
|
|
|
|
|
tag.SongID = binary.LittleEndian.Uint32(buffer[0x00:0x04])
|
|
|
|
tag.Unknown1 = binary.LittleEndian.Uint32(buffer[0x04:0x08])
|
|
|
|
tag.Unknown2 = binary.LittleEndian.Uint32(buffer[0x08:0x0C])
|
|
|
|
tag.MediaID = readUnicodeTagName(buffer[0x0C:], 30*2)
|
|
|
|
tag.MediaFileName = readUnicodeTagName(buffer[0x48:], 50*2)
|
|
|
|
tag.Unknown3 = binary.LittleEndian.Uint32(buffer[0xAC:0xB0])
|
|
|
|
return tag, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// readUnicodeTagName reads a buffer to maxLen.
|
|
|
|
// reconstruct text by skipping alternate char (ascii chars encoded in UTF-16-LE),
|
|
|
|
// until finding a zero or reaching maxLen.
|
|
|
|
func readUnicodeTagName(buffer []byte, maxLen int) string {
|
|
|
|
builder := strings.Builder{}
|
|
|
|
|
|
|
|
for i := 0; i < maxLen; i += 2 {
|
|
|
|
chr := buffer[i]
|
|
|
|
if chr != 0 {
|
|
|
|
builder.WriteByte(chr)
|
|
|
|
} else {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return builder.String()
|
|
|
|
}
|