Start migrating go-nes/studybox stuff over here

I need a bunch of tools for studybox related stuff.  It makes more sense
to have a separate project for them instead of trying to shove all the
functionality I want into a single command in the go-nes project.
This commit is contained in:
Zorchenhimer 2024-05-15 23:47:29 -04:00
parent 0f9ab661cc
commit 928a8e2faf
Signed by: Zorchenhimer
GPG Key ID: 70A1AB767AAB9C20
7 changed files with 1246 additions and 0 deletions

224
rom/decoder.go Normal file
View File

@ -0,0 +1,224 @@
package rom
import (
"bytes"
"fmt"
"strings"
//"encoding/binary"
)
// Returns packet, next state, and error (if any)
type decodeFunction func(page *Page, data []byte, startIdx int) (Packet, int, error)
// map of states. each state is a map of types
var definedPackets = map[int]map[byte]decodeFunction{
0: map[byte]decodeFunction{
0x01: decodeHeader,
},
1: map[byte]decodeFunction{
0x00: decodeMarkDataEnd,
},
2: map[byte]decodeFunction{
0x02: decodeSetWorkRamLoad,
0x03: decodeMarkDataStart,
0x04: decodeMarkDataStart,
0x05: decodeDelay,
},
}
// Main decoding loop is here
func (page *Page) decode(data []byte) error {
var err error
page.Packets = []Packet{}
page.state = 0
for idx := 0; idx < len(data); {
if data[idx] != 0xC5 {
// Padding after the last valid packet.
dataLeft := len(data) - idx
page.Packets = append(page.Packets, &packetPadding{
Length: dataLeft,
address: page.DataOffset + idx,
raw: data[idx:len(data)]})
return nil
}
var packet Packet
if page.state == 1 && data[idx+1] != 0x00 {
// bulk data
packet, page.state, err = decodeBulkData(page, data, idx)
if err != nil {
return err
}
} else {
df, ok := definedPackets[page.state][data[idx+1]]
if !ok {
return fmt.Errorf("State %d packet with type %02X isn't implemented",
page.state, data[idx+1])
}
packet, page.state, err = df(page, data, idx)
if err != nil {
return err
}
}
page.Packets = append(page.Packets, packet)
idx += len(packet.RawBytes())
}
return nil
}
func decodeHeader(page *Page, data []byte, idx int) (Packet, int, error) {
if !bytes.Equal(data[idx+1:idx+5], []byte{0x01, 0x01, 0x01, 0x01}) {
return nil, 0, fmt.Errorf("Packet header at offset %08X has invalid payload: $%08X",
idx+page.DataOffset, data[idx+1:idx+5])
}
if data[idx+5] != data[idx+6] {
return nil, 0, fmt.Errorf("Packet header at offset %08X has missmatched page numbers at offset %08X: %02X vs %02X",
idx+page.DataOffset,
idx+page.DataOffset+5,
data[idx+5],
data[idx+6],
)
}
ph := &packetHeader{
PageNumber: uint8(data[idx+6]),
Checksum: data[idx+8],
address: page.DataOffset + idx,
}
checksum := calcChecksum(data[idx : idx+7])
if checksum != ph.Checksum {
return nil, 0, fmt.Errorf("Invalid checksum for header packet starting at offset %08X. Got %02X, expected %02X",
page.DataOffset+idx, checksum, ph.Checksum)
}
return ph, 2, nil
}
func decodeDelay(page *Page, data []byte, idx int) (Packet, int, error) {
if data[idx+1] != data[idx+2] {
return nil, 0, fmt.Errorf("State 2 packet at offset %08X has missmatched type [%08X]: %d vs %d",
idx+page.DataOffset, idx+1+page.DataOffset, data[idx+1], data[idx+2])
}
count := 0
var i int
for i = idx + 3; i < len(data) && data[i] != 0x00 && data[i] != 0xC5; i++ {
count++
}
if count%2 != 0 {
fmt.Printf("0xAA delay packet at offset %08X has odd number of 0xAA's", idx+page.FileOffset)
}
pd := &packetDelay{
Length: count,
address: page.DataOffset + idx,
}
checksum := calcChecksum(data[idx : idx+count+3])
if checksum != 0xC5 {
return nil, 0, fmt.Errorf("Invalid checksum for delay packet starting at offset %08X. Got %02X, expected %02X",
pd.address, checksum, 0xC5)
}
idx += count + 3
return pd, 1, nil
}
func decodeMarkDataStart(page *Page, data []byte, idx int) (Packet, int, error) {
packet := &packetMarkDataStart{
Type: data[idx+1],
ArgA: data[idx+3],
ArgB: data[idx+4],
checksum: data[idx+5],
address: page.DataOffset + idx,
}
checksum := calcChecksum(data[idx : idx+5])
if checksum != packet.checksum {
return nil, 0, fmt.Errorf("Invalid checksum for UnknownS2T3 packet starting at offset %08X. Got %02X, expected %02X",
packet.address, checksum, packet.checksum)
}
return packet, 1, nil
}
func decodeMarkDataEnd(page *Page, data []byte, idx int) (Packet, int, error) {
packet := &packetMarkDataEnd{
Type: data[idx+2],
Reset: (data[idx+2]&0xF0 == 0xF0),
checksum: data[idx+3],
address: page.DataOffset + idx,
}
checksum := calcChecksum(data[idx : idx+3])
if checksum != packet.checksum {
return nil, 0, fmt.Errorf("Invalid checksum for UnknownS2T3 packet starting at offset %08X. Got %02X, expected %02X",
packet.address, checksum, packet.checksum)
}
newstate := 2
//if page.Data[idx+2]&0xF0 == 0xF0 {
// // this changes to state 3, not zero!
// newstate = 0
//}
return packet, newstate, nil
}
// C5 02 02 nn mm zz
// Map 8k ram bank nn to $6000-$7FFF; set load address to $mm00; zz = checksum
func decodeSetWorkRamLoad(page *Page, data []byte, idx int) (Packet, int, error) {
if data[idx+1] != data[idx+2] {
return nil, 0, fmt.Errorf("State 1 packet at offset %08X has missmatched type [%08X]: %d vs %d",
idx+page.DataOffset, idx+1+page.DataOffset, data[idx+1], data[idx+2])
}
packet := &packetWorkRamLoad{
bankId: data[idx+3],
loadAddressHigh: data[idx+4],
checksum: data[idx+5],
address: page.DataOffset + idx,
}
checksum := calcChecksum(data[idx : idx+5])
if checksum != packet.checksum {
return nil, 0, fmt.Errorf("Invalid checksum for SetWorkRamLoad packet starting at offset %08X. Got %02X, expected %02X",
packet.address, checksum, packet.checksum)
}
return packet, 1, nil
}
func decodeBulkData(page *Page, data []byte, idx int) (Packet, int, error) {
if data[idx+1] == 0 {
return nil, 0, fmt.Errorf("Bulk data packet has a length of zero at offset %08X",
page.DataOffset+idx)
}
packet := &packetBulkData{
address: page.DataOffset + idx,
}
datalen := int(data[idx+1])
packet.Data = data[idx+2 : idx+2+datalen]
packet.checksum = data[idx+len(packet.Data)+2]
checksum := calcChecksum(data[idx : idx+int(data[idx+1])+2])
if checksum != packet.checksum {
data := []string{}
for _, b := range packet.Data {
data = append(data, fmt.Sprintf("$%02X", b))
}
fmt.Printf("checksum data: %s\n", strings.Join(data, " "))
fmt.Printf("checksum address: %08X\n", packet.address+len(packet.Data)+2)
return nil, 0, fmt.Errorf("Invalid checksum for BulkData packet starting at offset %08X. Got %02X, expected %02X",
packet.address, checksum, packet.checksum)
}
return packet, 1, nil
}

143
rom/export.go Normal file
View File

@ -0,0 +1,143 @@
package rom
import (
"encoding/json"
"fmt"
"os"
)
func (sb *StudyBox) Export(directory string) error {
sbj := StudyBoxJson{
Version: 1,
Pages: []jsonPage{},
Audio: directory + "/audio" + sb.Audio.ext(),
}
for pidx, page := range sb.Data.Pages {
jp := jsonPage{
AudioOffsetLeadIn: page.AudioOffsetLeadIn,
AudioOffsetData: page.AudioOffsetData,
Data: []jsonData{},
}
file, err := os.Create(fmt.Sprintf("%s/page%02d_0000.txt", directory, pidx))
if err != nil {
return err
}
fmt.Fprintln(file, page.InfoString())
file.Close()
var dataStartId int
jData := jsonData{}
rawData := []byte{}
for i, packet := range page.Packets {
switch p := packet.(type) {
case *packetHeader:
jData.Type = "header"
jData.Values = []int{int(p.PageNumber)}
jp.Data = append(jp.Data, jData)
jData = jsonData{}
case *packetDelay:
jData.Type = "delay"
jData.Values = []int{p.Length}
case *packetWorkRamLoad:
jData.Type = "script"
jData.Values = []int{int(p.bankId), int(p.loadAddressHigh)}
dataStartId = i
case *packetPadding:
jData.Type = "padding"
jData.Values = []int{p.Length}
jData.Reset = false
jp.Data = append(jp.Data, jData)
jData = jsonData{}
case *packetMarkDataStart:
jData.Values = []int{int(p.ArgA), int(p.ArgB)}
jData.Type = p.dataType()
dataStartId = i
case *packetMarkDataEnd:
jData.Reset = p.Reset
if jData.Values == nil || len(jData.Values) == 0 {
fmt.Printf("[WARN] No data at page %d, dataStartId: %d\n", pidx, dataStartId)
jp.Data = append(jp.Data, jData)
jData = jsonData{}
continue
}
switch jData.Type {
case "pattern":
jData.File = fmt.Sprintf("%s/page%02d_%04d_chrData.chr", directory, pidx, dataStartId)
case "nametable":
jData.File = fmt.Sprintf("%s/page%02d_%04d_ntData.dat", directory, pidx, dataStartId)
case "script":
jData.File = fmt.Sprintf("%s/page%02d_%04d_scriptData.dat", directory, pidx, dataStartId)
//script, err := DissassembleScript(scriptData)
//if err != nil {
// fmt.Println(err)
//} else {
// fmt.Printf("Script OK Page %02d @ %04d\n", pidx, dataStartId)
// err = script.WriteToFile(fmt.Sprintf("%s/script_page%02d_%04d.txt", directory, pidx, dataStartId))
// if err != nil {
// return fmt.Errorf("Unable to write data to file: %v", err)
// }
//}
case "delay":
jp.Data = append(jp.Data, jData)
jData = jsonData{}
continue
default:
return fmt.Errorf("[WARN] unknown end data type: %s\n", jData.Type)
}
err = os.WriteFile(jData.File, rawData, 0777)
if err != nil {
return fmt.Errorf("Unable to write data to file [%q]: %v", jData.File, err)
}
jp.Data = append(jp.Data, jData)
jData = jsonData{}
rawData = []byte{}
case *packetBulkData:
if rawData == nil {
rawData = []byte{}
}
rawData = append(rawData, p.Data...)
default:
return fmt.Errorf("Encountered an unknown packet: %s page: %d", p.Asm(), pidx)
}
}
sbj.Pages = append(sbj.Pages, jp)
}
if sb.Audio == nil {
return fmt.Errorf("Missing audio!")
}
err := sb.Audio.WriteToFile(directory + "/audio")
if err != nil {
return fmt.Errorf("Error writing audio file: %v", err)
}
rawJson, err := json.MarshalIndent(sbj, "", " ")
if err != nil {
return err
}
return os.WriteFile(directory+".json", rawJson, 0777)
}

140
rom/import.go Normal file
View File

@ -0,0 +1,140 @@
package rom
import (
"encoding/json"
"fmt"
"os"
"strings"
)
func Import(filename string) (*StudyBox, error) {
if !strings.HasSuffix(strings.ToLower(filename), ".json") {
return nil, fmt.Errorf("Can only import .json files")
}
raw, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
sbj := &StudyBoxJson{}
err = json.Unmarshal(raw, sbj)
if err != nil {
return nil, fmt.Errorf("Unable to unmarshal json: %v", err)
}
audio, err := readAudio(sbj.Audio)
if err != nil {
return nil, fmt.Errorf("Unable to read audio: %v", err)
}
sb := &StudyBox{
Data: &TapeData{Pages: []*Page{}},
Audio: audio,
}
for _, jpage := range sbj.Pages {
page := &Page{
AudioOffsetLeadIn: jpage.AudioOffsetLeadIn,
AudioOffsetData: jpage.AudioOffsetData,
}
packets, err := importPackets(jpage.Data)
if err != nil {
return nil, err
}
page.Packets = packets
sb.Data.Pages = append(sb.Data.Pages, page)
}
return sb, nil
}
func importPackets(jdata []jsonData) ([]Packet, error) {
packets := []Packet{}
for idx, data := range jdata {
switch data.Type {
case "header":
if len(data.Values) < 1 {
return nil, fmt.Errorf("Missing header value from script data in element %d", idx)
}
packets = append(packets, newPacketHeader(uint8(data.Values[0])))
case "delay":
if len(data.Values) < 1 {
return nil, fmt.Errorf("Missing delay value from script data in element %d", idx)
}
packets = append(packets, newPacketDelay(data.Values[0]))
packets = append(packets, newPacketMarkDataEnd(packet_Delay, data.Reset))
case "script":
if len(data.Values) < 2 {
return nil, fmt.Errorf("Missing bank id and/or load address high values from script data in element %d", idx)
}
if data.File == "" {
fmt.Println("[WARN] No script file given in data element %d\n", idx)
}
packets = append(packets, newPacketWorkRamLoad(uint8(data.Values[0]), uint8(data.Values[1])))
if data.File != "" {
raw, err := os.ReadFile(data.File)
if err != nil {
return nil, fmt.Errorf("Error reading script data file: %v", err)
}
packets = append(packets, newBulkDataPackets(raw)...)
}
packets = append(packets, newPacketMarkDataEnd(packet_Script, data.Reset))
case "nametable":
if len(data.Values) < 2 {
return nil, fmt.Errorf("Missing bank id and/or load address high values from nametable data in element %d", idx)
}
if data.File == "" {
fmt.Println("[WARN] No script file given in data element %d\n", idx)
}
packets = append(packets, newPacketMarkDataStart(packet_Nametable, uint8(data.Values[0]), uint8(data.Values[1])))
if data.File != "" {
raw, err := os.ReadFile(data.File)
if err != nil {
return nil, fmt.Errorf("Error reading nametable data file: %v", err)
}
packets = append(packets, newBulkDataPackets(raw)...)
}
packets = append(packets, newPacketMarkDataEnd(packet_Nametable, data.Reset))
case "pattern":
if len(data.Values) < 2 {
return nil, fmt.Errorf("Missing bank id and/or load address high values from pattern data in element %d", idx)
}
if data.File == "" {
fmt.Printf("[WARN] No pattern file given in data element %d\n", idx)
}
packets = append(packets, newPacketMarkDataStart(packet_Pattern, uint8(data.Values[0]), uint8(data.Values[1])))
if data.File != "" {
raw, err := os.ReadFile(data.File)
if err != nil {
return nil, fmt.Errorf("Error reading pattern data file: %v", err)
}
packets = append(packets, newBulkDataPackets(raw)...)
}
packets = append(packets, newPacketMarkDataEnd(packet_Pattern, data.Reset))
case "padding":
if len(data.Values) < 1 {
return nil, fmt.Errorf("Missing padding value from script data in element %d", idx)
}
packets = append(packets, newPacketPadding(data.Values[0]))
default:
return nil, fmt.Errorf("Unknown packet type: %s", data.Type)
}
}
return packets, nil
}

287
rom/packets.go Normal file
View File

@ -0,0 +1,287 @@
package rom
import (
"fmt"
"strings"
)
type packetHeader struct {
PageNumber uint8
Checksum uint8
address int
}
func newPacketHeader(pageNumber uint8) *packetHeader {
ph := &packetHeader{PageNumber: pageNumber}
ph.Checksum = calcChecksum(ph.RawBytes()[0:7])
return ph
}
func (p *packetHeader) Name() string { return "Header" }
func (ph *packetHeader) RawBytes() []byte {
return []byte{0xC5, 0x01, 0x01, 0x01, 0x01,
byte(ph.PageNumber), byte(ph.PageNumber), ph.Checksum}
}
func (ph *packetHeader) Asm() string {
return fmt.Sprintf("header %d ; Checksum: %02X", ph.PageNumber, ph.Checksum)
}
func (ph *packetHeader) Address() int {
return ph.address
}
type packetDelay struct {
Length int
address int
}
func (p *packetDelay) Name() string { return "delay" }
func newPacketDelay(length int) *packetDelay {
return &packetDelay{Length: length}
}
func (pd *packetDelay) RawBytes() []byte {
payload := make([]byte, pd.Length)
for i := 0; i < pd.Length; i++ {
payload[i] = 0xAA
}
return append([]byte{0xC5, 0x05, 0x05}, payload...)
}
func (pd *packetDelay) Asm() string {
checksum := calcChecksum(pd.RawBytes())
return fmt.Sprintf("delay %d ; Checksum %02X",
pd.Length, checksum)
}
func (p *packetDelay) Address() int {
return p.address
}
type packetWorkRamLoad struct {
bankId uint8
loadAddressHigh uint8
checksum uint8
address int
}
func (p *packetWorkRamLoad) Name() string { return "workRamLoad" }
func newPacketWorkRamLoad(bank, addressHigh uint8) *packetWorkRamLoad {
p := &packetWorkRamLoad{bankId: bank, loadAddressHigh: addressHigh}
p.checksum = calcChecksum(p.RawBytes()[0:5])
return p
}
func (p *packetWorkRamLoad) Asm() string {
return fmt.Sprintf("work_ram_load $%02X $%02X ; Checksum %02X",
p.bankId, p.loadAddressHigh, p.checksum)
}
func (p *packetWorkRamLoad) RawBytes() []byte {
return []byte{0xC5, 0x02, 0x02, p.bankId, p.loadAddressHigh, p.checksum}
}
func (p *packetWorkRamLoad) Address() int {
return p.address
}
type packetBulkData struct {
checksum uint8
Data []byte
address int
}
func (p *packetBulkData) Name() string { return "BulkData" }
// Returns a list of packets
func newBulkDataPackets(raw []byte) []Packet {
packets := []Packet{}
for i := 0; i < len(raw); i += 128 {
l := 128
// TODO: veryfy this is actually correct
if len(raw) < i+128 {
l = len(raw) - i
}
p := &packetBulkData{Data: raw[i : i+l]}
raw := p.RawBytes()
p.checksum = calcChecksum(raw[0 : len(raw)-1])
packets = append(packets, p)
}
return packets
}
func (p *packetBulkData) Asm() string {
// commented out code prints the full data
//data := []string{}
//for _, b := range p.Data {
// data = append(data, fmt.Sprintf("$%02X", b))
//}
//return fmt.Sprintf("[%08X] data %s ; Length %d Checksum: %02X", p.address, strings.Join(data, ", "), len(p.Data), p.checksum)
return fmt.Sprintf("data $%02X, [...], $%02X ; Length %d Checksum: %02X", p.Data[0], p.Data[len(p.Data)-1], len(p.Data), p.checksum)
}
func (p *packetBulkData) RawBytes() []byte {
data := []byte{0xC5, uint8(len(p.Data))}
data = append(data, p.Data...)
data = append(data, p.checksum)
return data
}
func (p *packetBulkData) Address() int {
return p.address
}
type packetMarkDataStart struct {
ArgA uint8
ArgB uint8
Type uint8
address int
checksum uint8
}
func (p *packetMarkDataStart) Name() string { return "DataStart" }
func newPacketMarkDataStart(dataType packetType, a, b uint8) *packetMarkDataStart {
p := &packetMarkDataStart{
Type: uint8(dataType),
ArgA: a,
ArgB: b,
}
raw := p.RawBytes()
p.checksum = calcChecksum(raw[0 : len(raw)-1])
return p
}
func (p *packetMarkDataStart) dataType() string {
tstr := "unknown"
switch p.Type {
case 2:
tstr = "script"
case 3:
tstr = "nametable"
case 4:
tstr = "pattern"
}
return tstr
}
func (p *packetMarkDataStart) Asm() string {
return fmt.Sprintf("mark_datatype_start %s $%02X $%02X ; Checksum: %02X",
p.dataType(), p.ArgA, p.ArgB, p.checksum)
}
func (p *packetMarkDataStart) RawBytes() []byte {
return []byte{0xC5, uint8(p.Type), uint8(p.Type), p.ArgA, p.ArgB, p.checksum}
}
func (p *packetMarkDataStart) Address() int {
return p.address
}
type packetMarkDataEnd struct {
//Arg uint8
Reset bool
Type uint8
address int
checksum uint8
}
func (p *packetMarkDataEnd) Name() string { return "DataEnd" }
type packetType uint8
const (
packet_Script packetType = 2
packet_Nametable packetType = 3
packet_Pattern packetType = 4
packet_Delay packetType = 5
)
func newPacketMarkDataEnd(datatype packetType, reset bool) *packetMarkDataEnd {
p := &packetMarkDataEnd{
Reset: reset,
Type: uint8(datatype),
}
raw := p.RawBytes()
p.checksum = calcChecksum(raw[0 : len(raw)-1])
return p
}
func (p *packetMarkDataEnd) RawBytes() []byte {
arg := uint8(p.Type)
if p.Reset {
arg = arg | 0xF0
}
return []byte{0xC5, 0x00, arg, p.checksum}
}
func (p *packetMarkDataEnd) Asm() string {
var tstr string
switch p.Type & 0x0F {
case 2:
tstr = "script"
case 3:
tstr = "nametable"
case 4:
tstr = "pattern"
case 5:
tstr = "delay"
default:
tstr = fmt.Sprintf("unknown $%02X", p.Type)
}
if p.Reset {
tstr += " reset_state"
}
s := []string{}
for _, b := range p.RawBytes() {
s = append(s, fmt.Sprintf("%02X", b))
}
return fmt.Sprintf("mark_datatype_end %s ; %s Checksum: %02X", tstr, strings.Join(s, " "), p.checksum)
}
func (p *packetMarkDataEnd) Address() int {
return p.address
}
type packetPadding struct {
Length int
address int
raw []byte
}
func (p *packetPadding) Name() string { return "Padding" }
func newPacketPadding(length int) *packetPadding {
return &packetPadding{Length: length}
}
func (p *packetPadding) Asm() string {
return fmt.Sprintf("page_padding %d", p.Length)
}
func (p *packetPadding) RawBytes() []byte {
b := []byte{}
for i := 0; i < p.Length; i++ {
b = append(b, 0xAA)
}
return b
}
func (p *packetPadding) Address() int {
return p.address
}

148
rom/read.go Normal file
View File

@ -0,0 +1,148 @@
package rom
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"os"
)
func Read(reader io.Reader) (*StudyBox, error) {
raw, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
sb, err := readTape(raw)
if err != nil {
return nil, err
}
return sb, nil
}
// Read opens and decodes a `.studybox` file.
func ReadFile(filename string) (*StudyBox, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
return Read(file)
}
func readTape(data []byte) (*StudyBox, error) {
// check for length and identifier
if len(data) < 16 {
return nil, fmt.Errorf("Not enough data")
}
if !bytes.Equal(data[:4], []byte("STBX")) {
return nil, fmt.Errorf("Missing STBX identifier")
}
sb := &StudyBox{
Data: &TapeData{
Identifier: "STBX",
},
}
// header data length and version
sb.Data.Length = int(binary.LittleEndian.Uint32(data[4:8]))
sb.Data.Version = int(binary.LittleEndian.Uint32(data[8:12]))
// decode page chunks
var idx = 12
if string(data[idx:idx+4]) != "PAGE" {
return nil, fmt.Errorf("Missing PAGE chunks")
}
for string(data[idx:idx+4]) == "PAGE" {
page, err := unpackPage(idx+4, data)
if err != nil {
return nil, err
}
idx += page.Length + 8
sb.Data.Pages = append(sb.Data.Pages, page)
}
// audio is a single chunk
if string(data[idx:idx+4]) != "AUDI" {
return nil, fmt.Errorf("Missing AUDI chunk")
}
audio, err := unpackAudio(idx+4, data)
if err != nil {
return nil, err
}
sb.Audio = audio
return sb, nil
}
func unpackPage(start int, data []byte) (*Page, error) {
tp := &Page{Identifier: "PAGE"}
tp.FileOffset = start - 4
tp.DataOffset = start + 12
if len(data) < start+12 {
return nil, fmt.Errorf("Not enough data in PAGE")
}
tp.Length = int(binary.LittleEndian.Uint32(data[start+0 : start+4]))
tp.AudioOffsetLeadIn = int(binary.LittleEndian.Uint32(data[start+4 : start+8]))
tp.AudioOffsetData = int(binary.LittleEndian.Uint32(data[start+8 : start+12]))
if tp.Length > len(data)-start+12 {
return nil, fmt.Errorf("PAGE Length too large: %d with %d bytes remaining.",
tp.Length, len(data)-start)
}
if tp.AudioOffsetLeadIn > len(data)-start+12 {
return nil, fmt.Errorf("PAGE Audio offest lead-in too large: %d with %d bytes remaining.",
tp.Length, len(data)-start)
}
if tp.AudioOffsetData > len(data)-start+12 {
return nil, fmt.Errorf("PAGE Audio offest data too large: %d with %d bytes remaining.",
tp.Length, len(data)-start)
}
//tp.Data = data[start+12 : start+12+tp.Length-1]
err := tp.decode(data[start+12 : start+12+tp.Length-8])
if err != nil {
return nil, fmt.Errorf("Error decoding: %v", err)
}
return tp, nil
}
func unpackAudio(start int, data []byte) (*TapeAudio, error) {
if len(data) < start+12 {
return nil, fmt.Errorf("Not enough data in AUDI")
}
ta := &TapeAudio{
Identifier: "AUDI",
Length: int(binary.LittleEndian.Uint32(data[start : start+4])),
}
format := binary.LittleEndian.Uint32(data[start+4 : start+8])
switch format {
case 0:
ta.Format = AUDIO_WAV
case 1:
ta.Format = AUDIO_FLAC
case 2:
ta.Format = AUDIO_OGG
case 3:
ta.Format = AUDIO_MP3
default:
return nil, fmt.Errorf("Unknown audio format: %d", format)
}
ta.Data = data[start+8 : start+8+ta.Length]
return ta, nil
}

174
rom/rom.go Normal file
View File

@ -0,0 +1,174 @@
package rom
import (
"fmt"
"os"
"path/filepath"
"strings"
)
type StudyBox struct {
Data *TapeData
Audio *TapeAudio
}
func (sb StudyBox) String() string {
return fmt.Sprintf("%s\n%s", sb.Data.String(), sb.Audio.String())
}
type TapeData struct {
Identifier string // MUST be "STBX"
Length int // length of everything following this field (excluding Pages)
Version int
Pages []*Page
}
func (td TapeData) String() string {
return fmt.Sprintf("%s %d %v", td.Identifier, td.Length, td.Pages)
}
type Page struct {
Identifier string // MUST be "PAGE"
Length int
AudioOffsetLeadIn int
AudioOffsetData int
FileOffset int // offset in the file
DataOffset int // offset in the file for the data
//Data []byte
Packets []Packet
state int
}
func (p *Page) Debug() string {
lines := []string{}
for _, packet := range p.Packets {
raw := packet.RawBytes()
s := []string{}
for _, b := range raw {
s = append(s, fmt.Sprintf("%02X", b))
}
lines = append(lines, fmt.Sprintf("%s: %s", packet.Name(), strings.Join(s, " ")))
}
return strings.Join(lines, "\n")
}
func (page *Page) InfoString() string {
str := []string{}
for _, p := range page.Packets {
str = append(str, fmt.Sprintf("%08X: %s", p.Address(), p.Asm()))
}
return strings.Join(str, "\n")
}
func (p Page) String() string {
return fmt.Sprintf("%s @ %08X: %d %d %d",
p.Identifier,
p.FileOffset,
p.Length,
p.AudioOffsetLeadIn,
p.AudioOffsetData,
)
}
type AudioType string
const (
AUDIO_WAV AudioType = "WAV"
AUDIO_FLAC AudioType = "FLAC"
AUDIO_OGG AudioType = "OGG"
AUDIO_MP3 AudioType = "MP3"
)
type TapeAudio struct {
Identifier string // MUST be "AUDI"
Length int
Format AudioType
Data []byte
}
func readAudio(filename string) (*TapeAudio, error) {
ta := &TapeAudio{
Identifier: "AUDI",
}
switch strings.ToLower(filepath.Ext(filename)) {
case ".wav":
ta.Format = AUDIO_WAV
case ".flac":
ta.Format = AUDIO_FLAC
case ".ogg":
ta.Format = AUDIO_OGG
case ".mp3":
ta.Format = AUDIO_MP3
default:
return nil, fmt.Errorf("Unsupported audio format: %s", filepath.Ext(filename))
}
raw, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
ta.Data = raw
return ta, nil
}
func (ta TapeAudio) String() string {
return fmt.Sprintf("%s %d %s %d", ta.Identifier, ta.Length, ta.Format, len(ta.Data))
}
func (ta *TapeAudio) WriteToFile(basename string) error {
ext := "." + strings.ToLower(string(ta.Format))
return os.WriteFile(basename+ext, ta.Data, 0777)
}
func (ta *TapeAudio) ext() string {
return "." + strings.ToLower(string(ta.Format))
}
type Packet interface {
RawBytes() []byte
Asm() string
Address() int // Address this packet starts in the .studybox file (if loaded from a .studybox file)
Name() string
}
func calcChecksum(data []byte) uint8 {
var sum uint8
for i := 0; i < len(data); i++ {
sum ^= data[i]
}
return sum
}
type StudyBoxJson struct {
Version uint
Filename string // .studybox filename. defaults to the name of the json file if empty.
Audio string // filename of the audio
Pages []jsonPage
}
type jsonPage struct {
AudioOffsetLeadIn int
AudioOffsetData int
Data []jsonData
}
type jsonData struct {
Type string
Values []int
File string `json:",omitempty"`
Reset bool `json:",omitempty"`
}
func byteString(data []byte) string {
s := []string{}
for _, b := range data {
s = append(s, fmt.Sprintf("$%02X", b))
}
return strings.Join(s, ", ")
}

130
rom/write.go Normal file
View File

@ -0,0 +1,130 @@
package rom
import (
"bytes"
"encoding/binary"
"fmt"
"os"
)
func (sb *StudyBox) Write(filename string) error {
raw, err := sb.rawBytes()
if err != nil {
return err
}
if filename == "" {
filename = "output.studybox"
}
fmt.Println("Writing to " + filename)
return os.WriteFile(filename, raw, 0777)
}
func (sb *StudyBox) rawBytes() ([]byte, error) {
buffer := &bytes.Buffer{}
_, err := buffer.WriteString("STBX")
if err != nil {
return nil, err
}
// Remaining field length
err = binary.Write(buffer, binary.LittleEndian, uint32(4))
if err != nil {
return nil, fmt.Errorf("Error writing field length: %v", err)
}
// Version number (* 0x100)
err = binary.Write(buffer, binary.LittleEndian, uint32(1*0x100))
if err != nil {
return nil, fmt.Errorf("Error writing version: %v", err)
}
for _, page := range sb.Data.Pages {
raw, err := page.rawBytes()
if err != nil {
return nil, err
}
_, err = buffer.Write(raw)
if err != nil {
return nil, err
}
}
_, err = buffer.WriteString("AUDI")
if err != nil {
return nil, err
}
err = binary.Write(buffer, binary.LittleEndian, uint32(len(sb.Audio.Data)))
if err != nil {
return nil, err
}
var format uint32
switch sb.Audio.Format {
case AUDIO_WAV:
format = 0
case AUDIO_FLAC:
format = 1
case AUDIO_OGG:
format = 2
case AUDIO_MP3:
format = 3
default:
return nil, fmt.Errorf("Unsupported audio format: %s", sb.Audio.Format)
}
err = binary.Write(buffer, binary.LittleEndian, format)
if err != nil {
return nil, err
}
// For some reason there's 4 extra bytes. no idea why. chomp them off.
_, err = buffer.Write(sb.Audio.Data[0 : uint32(len(sb.Audio.Data))-4])
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func (page *Page) rawBytes() ([]byte, error) {
fieldBuffer := &bytes.Buffer{}
err := binary.Write(fieldBuffer, binary.LittleEndian, uint32(page.AudioOffsetLeadIn))
if err != nil {
return nil, err
}
err = binary.Write(fieldBuffer, binary.LittleEndian, uint32(page.AudioOffsetData))
if err != nil {
return nil, err
}
for _, packet := range page.Packets {
_, err = fieldBuffer.Write(packet.RawBytes())
if err != nil {
return nil, err
}
}
pageBuffer := &bytes.Buffer{}
_, err = pageBuffer.WriteString("PAGE")
if err != nil {
return nil, err
}
err = binary.Write(pageBuffer, binary.LittleEndian, uint32(fieldBuffer.Len()))
if err != nil {
return nil, err
}
_, err = pageBuffer.Write(fieldBuffer.Bytes())
if err != nil {
return nil, err
}
return pageBuffer.Bytes(), nil
}