Compare commits

...

5 Commits

Author SHA1 Message Date
Zorchenhimer ac5f29a696
[build-script] Start adding a new build script format
This format is meant to replace the current .json file when describing
.studybox ROM files, as well as handle compiling/assembling new ROMs
from scratch.

In theory, this will handle both ROMs that were decoded from tape as
well as instructions to build a new ROM from various files.  This
will include options to compile scripts, encode images, and handle some
audio timing by allowing individual audio files for each page.
2025-11-29 18:15:51 -05:00
Zorchenhimer b1d8d8335a
[audio] Tweak audio encoding stuff 2025-11-29 18:14:11 -05:00
Zorchenhimer 5189f69583
Add tmp/ to .gitignore 2025-11-25 22:37:07 -05:00
Zorchenhimer 1515b4113f
[sbx2wav] Start adding a tool to generate tape audio
This tool will take a .studybox ROM and encode the data in an MFM audio
wave.  Currently, this just handles the data channel and not the
recorded audio.  Output is a mono .wav file.

Also, some timing and settings are probably wrong. (Generated audio
doesn't decode with Sour's decoder)
2025-11-23 18:24:25 -05:00
Zorchenhimer 0d019ec696
[sbutil] Remove commented out main_old()
This hasn't been used in a while and it was just sitting there commented
out.  Didn't need to be there anymore.
2025-11-22 19:46:51 -05:00
11 changed files with 903 additions and 90 deletions

3
.gitignore vendored
View File

@ -1 +1,2 @@
bin/*
bin/
tmp/

View File

@ -1,10 +1,11 @@
.PHONY: all
all: bin/script-decode bin/sbutil bin/just-stats bin/extract-imgs
all: bin/script-decode bin/sbutil bin/just-stats bin/extract-imgs bin/sbx2wav
bin/script-decode: script/*.go
bin/sbutil: rom/*.go
bin/just-stats: script/*.go
bin/sbx2wav: rom/*.go audio/*.go
bin/%: cmd/%.go
go build -o $@ $<

55
audio/bitdata.go Normal file
View File

@ -0,0 +1,55 @@
package audio
type BitData struct {
data []byte
next int
current byte
left int // bits left in current
}
func NewBitData(data []byte) *BitData {
if len(data) == 0 {
panic("no data")
}
return &BitData{
data: data,
next: 1,
current: data[0],
left: 7,
}
}
// Returns the bit in the lowest position, and end of data. false if nothing left.
func (b *BitData) Next() (byte, bool) {
if b.left < 0 {
if len(b.data) <= b.next {
return 0, false
}
b.current = b.data[b.next]
b.next++
b.left = 7
return 0, true
}
ret := (b.current >> b.left) & 0x01
b.left--
return ret, true
}
func (b *BitData) Peek() byte {
left := b.left
current := b.current
if left < 0 {
if len(b.data) <= b.next {
return 0
}
current = b.data[b.next]
left = 7
}
return (current >> left) & 0x01
}

247
audio/encode.go Normal file
View File

@ -0,0 +1,247 @@
package audio
// TODO:
// - Stereo with the recorded audio, not just data.
// - Configurable lead-in silence (from start of audio)
// - Configurable segment gap lengths (silence between segments)
import (
"math"
"slices"
"io"
"bytes"
"fmt"
"github.com/go-audio/audio"
"github.com/go-audio/wav"
"git.zorchenhimer.com/Zorchenhimer/go-studybox/rom"
)
const (
SampleRate uint32 = 44_100 // TODO: verify sample rate with SBX audio
Amplitude int = 16_000
)
var (
BitRate int = 4890
)
func EncodeRom(w io.WriteSeeker, sbx *rom.StudyBox) error {
if sbx == nil {
return fmt.Errorf("nil rom")
}
if sbx.Audio == nil {
return fmt.Errorf("Missing audio")
}
if sbx.Audio.Format != rom.AUDIO_WAV {
return fmt.Errorf("unsupported audio format: %s", sbx.Audio)
}
if len(sbx.Data.Pages) == 0 {
return fmt.Errorf("no pages")
}
if len(sbx.Data.Pages[0].Packets) == 0 {
return fmt.Errorf("no packets")
}
wavreader := bytes.NewReader(sbx.Audio.Data)
decoder := wav.NewDecoder(wavreader)
if !decoder.IsValidFile() {
return fmt.Errorf(".studybox file does not contain a valid wav file")
}
decoder.ReadInfo()
if decoder.SampleRate != SampleRate {
return fmt.Errorf("SampleRate mismatch. Expected %d; found %d", SampleRate, decoder.SampleRate)
}
afmt := &audio.Format{
NumChannels: 1,
SampleRate: int(decoder.SampleRate),
}
writer := wav.NewEncoder(
w,
int(decoder.SampleRate),
int(decoder.BitDepth),
int(decoder.NumChans),
1)
defer writer.Close()
var err error
runningSamples := int64(0)
prevPageLeadIn := 0
for _, page := range sbx.Data.Pages {
if prevPageLeadIn > page.AudioOffsetLeadIn {
return fmt.Errorf("out of order pages (AudioOffsetLeadIn)")
}
prevPageLeadIn = page.AudioOffsetLeadIn
padLen := int64(page.AudioOffsetLeadIn)-runningSamples
fmt.Printf("padLen: %d = %d - %d\n", padLen, int64(page.AudioOffsetLeadIn), runningSamples)
if padLen < 0 {
padLen = 100_000
}
err = generatePadding(writer, afmt, padLen)
if err != nil {
return fmt.Errorf("generatePadding() error: %w", err)
}
runningSamples += padLen
sampleCount, err := encodePage(writer, afmt, page)
if err != nil {
return fmt.Errorf("encodePage() error: %w", err)
}
runningSamples += sampleCount
}
writer.Close()
return nil
}
func generatePadding(writer *wav.Encoder, afmt *audio.Format, length int64) error {
fmt.Println("generatePadding() length:", length)
buf := &audio.IntBuffer{
Format: afmt,
SourceBitDepth: writer.BitDepth,
Data: make([]int, length),
}
return writer.Write(buf)
}
func encodePage(writer *wav.Encoder, afmt *audio.Format, page *rom.Page) (int64, error) {
runningFract := float64(0)
//dataLeadLen := int(float64(page.AudioOffsetData - page.AudioOffsetLeadIn) / float64(83))
samplesPerFlux := float64(writer.SampleRate) / float64(BitRate) / 2
fmt.Println("samplesPerFlux:", samplesPerFlux)
dataLeadLen := (page.AudioOffsetData - page.AudioOffsetLeadIn) / int((samplesPerFlux * 9)) / 2
fmt.Println("dataLeadLen:", dataLeadLen-9)
lead := &rawData{
data: slices.Repeat([]byte{0}, dataLeadLen-9),
pageOffset: int64(page.AudioOffsetData),
}
fmt.Println("lead length:", len(lead.data))
runningFract = lead.encode(samplesPerFlux, 0)
data := []*rawData{}
data = append(data, lead)
for _, packet := range page.Packets {
d := &rawData{
data: packet.RawBytes(),
pageOffset: int64(page.AudioOffsetData),
}
runningFract = d.encode(samplesPerFlux, runningFract)
data = append(data, d)
}
sampleCount := int64(0)
for _, d := range data {
buf := &audio.IntBuffer{
Format: afmt,
SourceBitDepth: writer.BitDepth,
Data: d.samples,
}
err := writer.Write(buf)
if err != nil {
return sampleCount, err
}
sampleCount += int64(len(d.samples))
}
return sampleCount, nil
}
type rawData struct {
pageOffset int64
realOffset int64
data []byte
samples []int
}
func (d *rawData) encode(samplesPerFlux, runningFract float64) float64 {
bits := NewBitData(d.data)
flux := []byte{}
prev := 0
for {
bit, more := bits.Next()
if !more {
break
}
peek := bits.Peek()
if bit == 1 {
flux = append(flux, 1)
} else {
flux = append(flux, 0)
}
if bit == peek && bit == 0 {
// clock flux change
flux = append(flux, 1)
} else {
// no clock flux change
flux = append(flux, 0)
}
}
//first := true
for _, f := range flux {
//amp := Amplitude
if f == 1 {
if prev <= 0 {
prev = 1
} else {
prev = -1
}
//amp = int(float64(Amplitude) * 1.5)
//first = true
}
//if i == 0 {
// amp = Amplitude + 1000
//} else {
// amp = Amplitude
//}
spf, fract := math.Modf(samplesPerFlux)
runningFract += fract
if runningFract >= 1 {
runningFract -= 1
spf++
}
d.samples = append(d.samples, slices.Repeat([]int{prev*Amplitude}, int(spf))...)
//for i := 0; i < int(spf); i++ {
// if first {
// d.samples = append(d.samples, prev*int(float64(amp)*1.5))
// }
// d.samples = append(d.samples, prev*amp)
// first = false
// //amp -= 500
//}
//d.samples = append(d.samples, slices.Repeat([]int{prev*Amplitude}, int(samplesPerFlux))...)
//first = false
}
return runningFract
}

180
build-script/parser.go Normal file
View File

@ -0,0 +1,180 @@
package build
import (
"fmt"
"bufio"
"os"
"io"
"testing"
"strings"
"unicode"
)
type parseFunc func(key, values string) (Token, error)
var parseFuncs = map[string]parseFunc{
"rom": parseStrValue,
"fullaudio": parseStrValue,
"audiooffsets": parseAudioOffsets,
"page": parseNumValue,
"padding": parseNumValue,
"version": parseNumValue,
"delay": parseDelay,
"script": parseData,
"tiles": parseData,
"pattern": parseData,
}
func ParseFile(filename string) ([]Token, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
return Parse(file)
}
func Parse(r io.Reader) ([]Token, error) {
items := []Token{}
scanner := bufio.NewScanner(r)
prev := ""
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
// blanks and comments
if line == "" || strings.HasPrefix(line, "#") {
continue
}
splitln := strings.SplitN(line, " ", 2)
if len(splitln) != 2 {
return nil, fmt.Errorf("invalid line: %q", line)
}
var itm Token
var err error
// TODO: Some of these will need to be unique in the file
// (rom, version, fullaudio), and probably exclusive/ordered.
// IE, rom, version, & fullaudio must come before the first
// page ##, and must only appear once. if a page has an
// audio line, all pages must have one and there can be no
// fullaudio.
if fn, ok := parseFuncs[splitln[0]]; ok {
itm, err = fn(splitln[0], splitln[1])
} else {
return nil, fmt.Errorf("unknown line: %s\n", splitln[0])
}
if err != nil {
return nil, err
}
if !itm.ValidAfter(prev) {
if prev == "" {
prev = "[empty]"
}
return nil, fmt.Errorf("%s not valid after %s", itm.Type(), prev)
}
prev = itm.Type()
items = append(items, itm)
}
return items, nil
}
var t *testing.T
func pKeyVals(values string) (map[string]string, error) {
m := map[string]string{}
start := 0
runes := []rune(values)
currentKey := ""
quote := false
tlog("[pKeyVals] start")
var i int
for i = 0; i < len(runes); i++ {
tlogf("[pKeyVals] rune: %c\n", runes[i])
if !quote && runes[i] == '"' {
quote = true
tlog("[pKeyVals] start quote")
continue
}
if !quote && unicode.IsSpace(runes[i]) {
tlog("[pKeyVals] !quote && IsSpace()")
if currentKey == "" {
tlog("[pKeyVals] currentKey empty")
start = i+1
} else {
tlog("[pKeyVals] currentKey not empty")
m[currentKey] = string(runes[start:i])
currentKey = ""
start = i+1
}
continue
}
if quote && runes[i] == '"' {
tlog("[pKeyVals] quote && rune == \"")
quote = false
if currentKey == "" {
currentKey = string(runes[start+1:i])
i++
start = i+1
tlogf("[pKeyVals] currentKey empty; set to %s\n", currentKey)
} else {
m[currentKey] = string(runes[start+1:i])
currentKey = ""
tlog("[pKeyVals] currentKey not empty")
}
continue
}
if runes[i] == ':' {
tlog("[pKeyVals] rune == :")
if i == start {
return nil, fmt.Errorf("missing key")
}
currentKey = string(runes[start:i])
start = i+1
continue
}
}
if quote {
return nil, fmt.Errorf("missmatched quote")
}
if currentKey != "" {
//return nil, fmt.Errorf("missing value for %q", currentKey)
tlogf("[pKeyVals] outside loop assign m[%s] = %s\n", currentKey, string(runes[start:i]))
m[currentKey] = string(runes[start:i])
}
return m, nil
}
func tlog(args ...any) {
if t != nil {
t.Log(args...)
}
}
func tlogf(format string, args ...any) {
if t != nil {
t.Logf(format, args...)
}
}

264
build-script/tokens.go Normal file
View File

@ -0,0 +1,264 @@
package build
import (
"strings"
"strconv"
"fmt"
)
type Token interface {
Type() string
String() string
ValidAfter(t string) bool
Text() string
}
type TokenDelay struct {
Value int
Reset bool
}
func (itm *TokenDelay) Type() string { return "delay" }
func (itm *TokenDelay) String() string { return fmt.Sprintf("{TokenDelay Value:%d Reset:%t}", itm.Value, itm.Reset) }
func (itm *TokenDelay) ValidAfter(t string) bool {
switch t {
case "audiooffsets", "pattern", "delay", "page", "script", "tiles":
return true
}
return false
}
func (itm *TokenDelay) Text() string {
return fmt.Sprintf("delay %d reset:%t", itm.Value, itm.Reset)
}
func parseDelay(key, line string) (Token, error) {
vals := strings.Split(line, " ")
itm := &TokenDelay{}
for _, val := range vals {
if strings.Contains(val, ":") {
keyval := strings.SplitN(val, ":", 2)
if len(keyval) != 2 {
return nil, fmt.Errorf("Invalid key/value for delay: %s", val)
}
if keyval[0] != "reset" {
return nil, fmt.Errorf("Invalid key/value for delay: %s", val)
}
switch strings.ToLower(keyval[1]) {
case "true", "yes", "1":
itm.Reset = true
case "false", "no", "0":
itm.Reset = false
default:
return nil, fmt.Errorf("Invalid reset value: %s", keyval[1])
}
} else {
num, err := strconv.ParseUint(val, 0, 32)
if err != nil {
return nil, fmt.Errorf("Invalid delay vaule: %s: %w", val, err)
}
itm.Value = int(num)
}
}
return itm, nil
}
type TokenNumValue struct {
ValType string // delay, padding, page
Value int
}
func (itm *TokenNumValue) Type() string { return itm.ValType }
func (itm *TokenNumValue) String() string { return fmt.Sprintf("{TokenNumValue ValType:%s Value:%d}", itm.ValType, itm.Value) }
func (itm *TokenNumValue) ValidAfter(t string) bool {
switch itm.ValType {
case "page":
if t == "" {
return false
}
return true
case "padding":
switch t {
case "delay", "pattern", "tiles", "script", "page":
return true
default:
return false
}
case "version":
switch t {
case "", "rom", "fullaudio":
return true
default:
return false
}
}
return false
}
func (itm *TokenNumValue) Text() string {
return fmt.Sprintf("%s %d", itm.ValType, itm.Value)
}
func parseNumValue(key, val string) (Token, error) {
v, err := strconv.Atoi(val)
if err != nil {
return nil, fmt.Errorf("Invalid %s value: %q", key, val)
}
return &TokenNumValue{
ValType: key,
Value: int(v),
}, nil
}
type TokenStrValue struct {
ValType string
Value string
}
func (itm *TokenStrValue) Type() string { return itm.ValType }
func (itm *TokenStrValue) String() string { return fmt.Sprintf("{TokenStrVal ValType:%s Value:%q}", itm.ValType, itm.Value) }
func (itm *TokenStrValue) ValidAfter(t string) bool {
switch t {
case "", "rom", "fullaudio", "version":
return true
}
return false
}
func (itm *TokenStrValue) Text() string {
return itm.ValType+" "+itm.Value
}
func parseStrValue(key, value string) (Token, error) {
return &TokenStrValue{
ValType: key,
Value: value,
}, nil
}
type TokenAudioOffsets struct {
LeadIn uint64
Data uint64
}
func (itm *TokenAudioOffsets) Type() string { return "audiooffsets" }
func (itm *TokenAudioOffsets) String() string { return fmt.Sprintf("{ItemAudioOffsets LeadIn:%d Data:%d}", itm.LeadIn, itm.Data) }
func (itm *TokenAudioOffsets) ValidAfter(t string) bool {
switch t {
case "page", "delay", "script", "tiles", "pattern":
return true
}
return false
}
func (itm *TokenAudioOffsets) Text() string {
return fmt.Sprintf("audiooffsets leadin:%d data:%d", itm.LeadIn, itm.Data)
}
func parseAudioOffsets(key, line string) (Token, error) {
vals := strings.Split(line, " ")
itm := &TokenAudioOffsets{}
for _, keyval := range vals {
pair := strings.Split(keyval, ":")
if len(pair) != 2 {
return nil, fmt.Errorf("invalid syntax: %q", keyval)
}
num, err := strconv.ParseUint(pair[1], 0, 64)
if err != nil {
return nil, err
}
switch pair[0] {
case "leadin":
itm.LeadIn = num
case "data":
itm.Data = num
default:
return nil, fmt.Errorf("unknown key: %s", pair[0])
}
}
return itm, nil
}
type TokenData struct {
ValType string
Bank int
Addr int
File string
}
func (itm *TokenData) Type() string { return itm.ValType }
func (itm *TokenData) String() string {
return fmt.Sprintf("{TokenData ValType:%s Bank:0x%02X Addr:0x%02X File:%q}",
itm.ValType,
itm.Bank,
itm.Addr,
itm.File,
)
}
func (itm *TokenData) ValidAfter(t string) bool {
switch t {
case "page", "delay", "script", "tiles", "pattern":
return true
}
return false
}
func (itm *TokenData) Text() string {
return fmt.Sprintf("%s bank:0x%02X addr:0x%02X file:%q",
itm.ValType,
itm.Bank,
itm.Addr,
itm.File,
)
}
func parseData(tokType, vals string) (Token, error) {
args, err := pKeyVals(vals)
if err != nil {
return nil, err
}
itm := &TokenData{ValType: tokType}
for key, value := range args {
switch key {
case "bank":
val, err := strconv.ParseUint(value, 0, 8)
if err != nil {
return nil, fmt.Errorf("%s bank value error: %w", key, err)
}
itm.Bank = int(val)
case "addr":
val, err := strconv.ParseUint(value, 0, 8)
if err != nil {
return nil, fmt.Errorf("%s addr value error: %w", key, err)
}
itm.Addr = int(val)
case "file":
itm.File = value
default:
return nil, fmt.Errorf("%s unknown key: %q", tokType, key)
}
}
return itm, nil
}

View File

@ -141,83 +141,3 @@ func exists(filename string) bool {
return true
}
//func main_old() {
// if len(os.Args) < 3 {
// fmt.Println("Missing command")
// os.Exit(1)
// }
//
// matches := []string{}
// for _, glob := range os.Args[2:len(os.Args)] {
// m, err := filepath.Glob(glob)
// if err != nil {
// fmt.Println(err)
// os.Exit(1)
// }
// matches = append(matches, m...)
// }
//
// if len(matches) == 0 {
// fmt.Println("No files found!")
// }
//
// switch strings.ToLower(os.Args[1]) {
// case "unpack":
// for _, file := range matches {
// fmt.Println("-- Processing " + file)
// outDir := filepath.Base(file)
// outDir = strings.ReplaceAll(outDir, ".studybox", "")
//
// err := os.MkdirAll(outDir, 0777)
// if err != nil {
// fmt.Println(err)
// continue
// }
//
// sb, err := rom.ReadFile(file)
// if err != nil {
// fmt.Println(err)
// continue
// }
//
// err = sb.Export(outDir)
// if err != nil {
// fmt.Println(err)
// }
// }
// case "pack":
// for _, file := range matches {
// fmt.Println("-- Processing " + file)
// sb, err := rom.Import(file)
// if err != nil {
// fmt.Println(err)
// continue
// }
//
// outDir := filepath.Base(file)
// outDir = strings.ReplaceAll(outDir, ".json", "_output")
//
// err = os.MkdirAll(outDir, 0777)
// if err != nil {
// fmt.Println(err)
// continue
// }
//
// err = sb.Export(outDir)
// if err != nil {
// fmt.Println(err)
// continue
// }
//
// // TODO: put this in the json file?
//
// err = sb.Write(outDir + ".studybox")
// if err != nil {
// fmt.Println(err)
// continue
// }
//
// }
// }
//}

51
cmd/sbx2wav.go Normal file
View File

@ -0,0 +1,51 @@
package main
import (
"fmt"
"os"
"github.com/alexflint/go-arg"
"git.zorchenhimer.com/Zorchenhimer/go-studybox/audio"
"git.zorchenhimer.com/Zorchenhimer/go-studybox/rom"
)
type Arguments struct {
Input string `arg:"positional,required"`
Output string `arg:"positional,required"`
BitRate int `arg:"--bit-rate", default:"4790"` // value found by trial and error
}
func run(args *Arguments) error {
sbx, err := rom.ReadFile(args.Input)
if err != nil {
return err
}
output, err := os.Create(args.Output)
if err != nil {
return err
}
defer output.Close()
audio.BitRate = args.BitRate
err = audio.EncodeRom(output, sbx)
if err != nil {
return fmt.Errorf("Encode error: %w", err)
}
return nil
}
func main() {
args := &Arguments{}
arg.MustParse(args)
err := run(args)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

11
go.mod
View File

@ -3,8 +3,13 @@ module git.zorchenhimer.com/Zorchenhimer/go-studybox
go 1.24.1
require (
github.com/alexflint/go-arg v1.5.1
github.com/zorchenhimer/go-retroimg v0.0.0-20251108020316-705ea2ebacd6
github.com/alexflint/go-arg v1.6.0
github.com/go-audio/audio v1.0.0
github.com/go-audio/wav v1.1.0
github.com/zorchenhimer/go-retroimg v0.0.0-20251111010417-7299e86df5a9
)
require github.com/alexflint/go-scalar v1.2.0 // indirect
require (
github.com/alexflint/go-scalar v1.2.0 // indirect
github.com/go-audio/riff v1.0.0 // indirect
)

14
go.sum
View File

@ -1,15 +1,21 @@
github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y=
github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8=
github.com/alexflint/go-arg v1.6.0 h1:wPP9TwTPO54fUVQl4nZoxbFfKCcy5E6HBCumj1XVRSo=
github.com/alexflint/go-arg v1.6.0/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8=
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-audio/audio v1.0.0 h1:zS9vebldgbQqktK4H0lUqWrG8P0NxCJVqcj7ZpNnwd4=
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA=
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
github.com/go-audio/wav v1.1.0 h1:jQgLtbqBzY7G+BM8fXF7AHUk1uHUviWS4X39d5rsL2g=
github.com/go-audio/wav v1.1.0/go.mod h1:mpe9qfwbScEbkd8uybLuIpTgHyrISw/OTuvjUW2iGtE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/zorchenhimer/go-retroimg v0.0.0-20251108020316-705ea2ebacd6 h1:94b43etKen/R0Ga+lxgfSMlYQicwsMvlFBizMOQ3loc=
github.com/zorchenhimer/go-retroimg v0.0.0-20251108020316-705ea2ebacd6/go.mod h1:iQUJQkvvbgycl7TS2OWdSC0+kHYypOASX129xmnv+SE=
github.com/zorchenhimer/go-retroimg v0.0.0-20251111010417-7299e86df5a9 h1:tDALzDZa+sEzKDNDtPkbWIIxcfR3lB5X6ghUieaPLnE=
github.com/zorchenhimer/go-retroimg v0.0.0-20251111010417-7299e86df5a9/go.mod h1:iQUJQkvvbgycl7TS2OWdSC0+kHYypOASX129xmnv+SE=
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -4,6 +4,9 @@ import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"git.zorchenhimer.com/Zorchenhimer/go-studybox/build-script"
)
func (sb *StudyBox) Export(directory string, includeAudio bool) error {
@ -13,6 +16,18 @@ func (sb *StudyBox) Export(directory string, includeAudio bool) error {
Audio: directory + "/audio" + sb.Audio.ext(),
}
bscript := []build.Token{
&build.TokenStrValue{ValType: "rom", Value: filepath.Base(directory+".studybox")},
&build.TokenNumValue{ValType: "version", Value: 1},
&build.TokenStrValue{
ValType: "fullaudio",
Value: "audio"+sb.Audio.ext(),
},
}
// for delay resets and data file names
var prevTok build.Token
// A "Page" here does not correspond to the entered "Page" number on the
// title screen. These are really segments. The "Page" that is entered on
// the title screen is stored in the header of a segment. Multiple
@ -44,15 +59,31 @@ func (sb *StudyBox) Export(directory string, includeAudio bool) error {
jp.Data = append(jp.Data, jData)
jData = jsonData{}
bscript = append(bscript, &build.TokenNumValue{ValType: "page", Value: int(p.PageNumber)})
bscript = append(bscript, &build.TokenAudioOffsets{
LeadIn: uint64(page.AudioOffsetLeadIn),
Data: uint64(page.AudioOffsetData),
})
prevTok = nil
case *packetDelay:
jData.Type = "delay"
jData.Values = []int{p.Length}
prevTok = &build.TokenDelay{Value: int(p.Length)}
bscript = append(bscript, prevTok)
case *packetWorkRamLoad:
jData.Type = "script"
jData.Values = []int{int(p.bankId), int(p.loadAddressHigh)}
dataStartId = i
prevTok = &build.TokenData{
Bank: int(p.bankId),
Addr: int(p.loadAddressHigh),
}
bscript = append(bscript, prevTok)
case *packetPadding:
jData.Type = "padding"
jData.Values = []int{p.Length}
@ -61,11 +92,23 @@ func (sb *StudyBox) Export(directory string, includeAudio bool) error {
jp.Data = append(jp.Data, jData)
jData = jsonData{}
prevTok = nil
bscript = append(bscript, &build.TokenNumValue{
ValType: "padding",
Value: int(p.Length),
})
case *packetMarkDataStart:
jData.Values = []int{int(p.ArgA), int(p.ArgB)}
jData.Type = p.dataType()
dataStartId = i
prevTok = &build.TokenData{
Bank: int(p.ArgA),
Addr: int(p.ArgB),
}
bscript = append(bscript, prevTok)
case *packetMarkDataEnd:
jData.Reset = p.Reset
@ -79,12 +122,18 @@ func (sb *StudyBox) Export(directory string, includeAudio bool) error {
switch jData.Type {
case "pattern":
jData.File = fmt.Sprintf("%s/segment-%02d_packet-%04d_chrData.chr", directory, pidx, dataStartId)
d := prevTok.(*build.TokenData)
d.ValType = "pattern"
case "nametable":
jData.File = fmt.Sprintf("%s/segment-%02d_packet-%04d_ntData.dat", directory, pidx, dataStartId)
d := prevTok.(*build.TokenData)
d.ValType = "tiles"
case "script":
jData.File = fmt.Sprintf("%s/segment-%02d_packet-%04d_scriptData.dat", directory, pidx, dataStartId)
d := prevTok.(*build.TokenData)
d.ValType = "script"
//script, err := DissassembleScript(scriptData)
//if err != nil {
@ -106,6 +155,16 @@ func (sb *StudyBox) Export(directory string, includeAudio bool) error {
return fmt.Errorf("[WARN] unknown end data type: %s\n", jData.Type)
}
if prevTok != nil {
if jData.Type == "delay" {
d := prevTok.(*build.TokenDelay)
d.Reset = p.Reset
} else {
d := prevTok.(*build.TokenData)
d.File = filepath.Base(jData.File)
}
}
err = os.WriteFile(jData.File, rawData, 0666)
if err != nil {
return fmt.Errorf("Unable to write data to file [%q]: %v", jData.File, err)
@ -145,5 +204,29 @@ func (sb *StudyBox) Export(directory string, includeAudio bool) error {
return err
}
return os.WriteFile(directory+".json", rawJson, 0666)
err = os.WriteFile(directory+".json", rawJson, 0666)
if err != nil {
return err
}
bfile, err := os.Create(directory+".sbb")
if err != nil {
return err
}
defer bfile.Close()
for _, tok := range bscript {
if tok.Type() == "page" {
_, err = fmt.Fprintln(bfile, "")
if err != nil {
return fmt.Errorf("error writing bscript file: %w", err)
}
}
_, err = fmt.Fprintln(bfile, tok.Text())
if err != nil {
return fmt.Errorf("error writing bscript file: %w", err)
}
}
return nil
}