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:
parent
0f9ab661cc
commit
928a8e2faf
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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, ", ")
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue