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
|
||||
|
||||
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 $@ $<
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
//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
|
||||
|
||||
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
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.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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue