Compare commits
5 Commits
75e0ee2ba3
...
ac5f29a696
| Author | SHA1 | Date |
|---|---|---|
|
|
ac5f29a696 | |
|
|
b1d8d8335a | |
|
|
5189f69583 | |
|
|
1515b4113f | |
|
|
0d019ec696 |
|
|
@ -1 +1,2 @@
|
||||||
bin/*
|
bin/
|
||||||
|
tmp/
|
||||||
|
|
|
||||||
3
Makefile
3
Makefile
|
|
@ -1,10 +1,11 @@
|
||||||
.PHONY: all
|
.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/script-decode: script/*.go
|
||||||
bin/sbutil: rom/*.go
|
bin/sbutil: rom/*.go
|
||||||
bin/just-stats: script/*.go
|
bin/just-stats: script/*.go
|
||||||
|
bin/sbx2wav: rom/*.go audio/*.go
|
||||||
|
|
||||||
bin/%: cmd/%.go
|
bin/%: cmd/%.go
|
||||||
go build -o $@ $<
|
go build -o $@ $<
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -141,83 +141,3 @@ func exists(filename string) bool {
|
||||||
|
|
||||||
return true
|
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
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
|
||||||
|
|
@ -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
11
go.mod
|
|
@ -3,8 +3,13 @@ module git.zorchenhimer.com/Zorchenhimer/go-studybox
|
||||||
go 1.24.1
|
go 1.24.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alexflint/go-arg v1.5.1
|
github.com/alexflint/go-arg v1.6.0
|
||||||
github.com/zorchenhimer/go-retroimg v0.0.0-20251108020316-705ea2ebacd6
|
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
14
go.sum
|
|
@ -1,15 +1,21 @@
|
||||||
github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y=
|
github.com/alexflint/go-arg v1.6.0 h1:wPP9TwTPO54fUVQl4nZoxbFfKCcy5E6HBCumj1XVRSo=
|
||||||
github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8=
|
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 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
|
||||||
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
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-20251111010417-7299e86df5a9 h1:tDALzDZa+sEzKDNDtPkbWIIxcfR3lB5X6ghUieaPLnE=
|
||||||
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/go.mod h1:iQUJQkvvbgycl7TS2OWdSC0+kHYypOASX129xmnv+SE=
|
||||||
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
|
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
|
||||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.zorchenhimer.com/Zorchenhimer/go-studybox/build-script"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (sb *StudyBox) Export(directory string, includeAudio bool) error {
|
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(),
|
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
|
// 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
|
// title screen. These are really segments. The "Page" that is entered on
|
||||||
// the title screen is stored in the header of a segment. Multiple
|
// 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)
|
jp.Data = append(jp.Data, jData)
|
||||||
jData = jsonData{}
|
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:
|
case *packetDelay:
|
||||||
jData.Type = "delay"
|
jData.Type = "delay"
|
||||||
jData.Values = []int{p.Length}
|
jData.Values = []int{p.Length}
|
||||||
|
|
||||||
|
prevTok = &build.TokenDelay{Value: int(p.Length)}
|
||||||
|
bscript = append(bscript, prevTok)
|
||||||
|
|
||||||
case *packetWorkRamLoad:
|
case *packetWorkRamLoad:
|
||||||
jData.Type = "script"
|
jData.Type = "script"
|
||||||
jData.Values = []int{int(p.bankId), int(p.loadAddressHigh)}
|
jData.Values = []int{int(p.bankId), int(p.loadAddressHigh)}
|
||||||
dataStartId = i
|
dataStartId = i
|
||||||
|
|
||||||
|
prevTok = &build.TokenData{
|
||||||
|
Bank: int(p.bankId),
|
||||||
|
Addr: int(p.loadAddressHigh),
|
||||||
|
}
|
||||||
|
bscript = append(bscript, prevTok)
|
||||||
|
|
||||||
case *packetPadding:
|
case *packetPadding:
|
||||||
jData.Type = "padding"
|
jData.Type = "padding"
|
||||||
jData.Values = []int{p.Length}
|
jData.Values = []int{p.Length}
|
||||||
|
|
@ -61,11 +92,23 @@ func (sb *StudyBox) Export(directory string, includeAudio bool) error {
|
||||||
jp.Data = append(jp.Data, jData)
|
jp.Data = append(jp.Data, jData)
|
||||||
jData = jsonData{}
|
jData = jsonData{}
|
||||||
|
|
||||||
|
prevTok = nil
|
||||||
|
bscript = append(bscript, &build.TokenNumValue{
|
||||||
|
ValType: "padding",
|
||||||
|
Value: int(p.Length),
|
||||||
|
})
|
||||||
|
|
||||||
case *packetMarkDataStart:
|
case *packetMarkDataStart:
|
||||||
jData.Values = []int{int(p.ArgA), int(p.ArgB)}
|
jData.Values = []int{int(p.ArgA), int(p.ArgB)}
|
||||||
jData.Type = p.dataType()
|
jData.Type = p.dataType()
|
||||||
dataStartId = i
|
dataStartId = i
|
||||||
|
|
||||||
|
prevTok = &build.TokenData{
|
||||||
|
Bank: int(p.ArgA),
|
||||||
|
Addr: int(p.ArgB),
|
||||||
|
}
|
||||||
|
bscript = append(bscript, prevTok)
|
||||||
|
|
||||||
case *packetMarkDataEnd:
|
case *packetMarkDataEnd:
|
||||||
jData.Reset = p.Reset
|
jData.Reset = p.Reset
|
||||||
|
|
||||||
|
|
@ -79,12 +122,18 @@ func (sb *StudyBox) Export(directory string, includeAudio bool) error {
|
||||||
switch jData.Type {
|
switch jData.Type {
|
||||||
case "pattern":
|
case "pattern":
|
||||||
jData.File = fmt.Sprintf("%s/segment-%02d_packet-%04d_chrData.chr", directory, pidx, dataStartId)
|
jData.File = fmt.Sprintf("%s/segment-%02d_packet-%04d_chrData.chr", directory, pidx, dataStartId)
|
||||||
|
d := prevTok.(*build.TokenData)
|
||||||
|
d.ValType = "pattern"
|
||||||
|
|
||||||
case "nametable":
|
case "nametable":
|
||||||
jData.File = fmt.Sprintf("%s/segment-%02d_packet-%04d_ntData.dat", directory, pidx, dataStartId)
|
jData.File = fmt.Sprintf("%s/segment-%02d_packet-%04d_ntData.dat", directory, pidx, dataStartId)
|
||||||
|
d := prevTok.(*build.TokenData)
|
||||||
|
d.ValType = "tiles"
|
||||||
|
|
||||||
case "script":
|
case "script":
|
||||||
jData.File = fmt.Sprintf("%s/segment-%02d_packet-%04d_scriptData.dat", directory, pidx, dataStartId)
|
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)
|
//script, err := DissassembleScript(scriptData)
|
||||||
//if err != nil {
|
//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)
|
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)
|
err = os.WriteFile(jData.File, rawData, 0666)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Unable to write data to file [%q]: %v", jData.File, err)
|
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 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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue