diff --git a/rom/decoder.go b/rom/decoder.go new file mode 100644 index 0000000..13028b7 --- /dev/null +++ b/rom/decoder.go @@ -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 +} diff --git a/rom/export.go b/rom/export.go new file mode 100644 index 0000000..339ed67 --- /dev/null +++ b/rom/export.go @@ -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) +} diff --git a/rom/import.go b/rom/import.go new file mode 100644 index 0000000..a044b2a --- /dev/null +++ b/rom/import.go @@ -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 +} diff --git a/rom/packets.go b/rom/packets.go new file mode 100644 index 0000000..f518f58 --- /dev/null +++ b/rom/packets.go @@ -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 +} diff --git a/rom/read.go b/rom/read.go new file mode 100644 index 0000000..38d3797 --- /dev/null +++ b/rom/read.go @@ -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 +} diff --git a/rom/rom.go b/rom/rom.go new file mode 100644 index 0000000..f5cd484 --- /dev/null +++ b/rom/rom.go @@ -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, ", ") +} diff --git a/rom/write.go b/rom/write.go new file mode 100644 index 0000000..1fa7923 --- /dev/null +++ b/rom/write.go @@ -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 +}