mirror of
https://github.com/vgmstream/vgmstream.git
synced 2025-01-17 23:36:41 +01:00
Add inline TXTH math: (key) = (number) (op) (number) ... [op: +-*/]
This commit is contained in:
parent
97c9df30aa
commit
ddfb46d1bd
76
doc/TXTH.md
76
doc/TXTH.md
@ -55,7 +55,9 @@ A text file with the above commands must be saved as ".vag.txth" or ".txth", not
|
||||
# - start_offset, data_size
|
||||
# - num_samples, loop_start_sample, loop_end_sample
|
||||
# - subsong_count, subsong_offset
|
||||
# - {string}: other special values for certain keys, described below
|
||||
# - (string): other special values for certain keys, described below
|
||||
# Those may be combined with math operations (+-*/):
|
||||
# "field = (number) (op) (offset) (op) (field) (...)"
|
||||
|
||||
# Codec used to encode the data [REQUIRED]
|
||||
# Accepted codec strings:
|
||||
@ -153,7 +155,7 @@ codec_mode = (number)
|
||||
|
||||
# Modifies next values [OPTIONAL]
|
||||
# Values will be "(key) = (number)|(offset)|(field) */+- value_(op)"
|
||||
# Useful when a size or such needs adjustments (like given in 0x800 sectors).
|
||||
# Deprecated, should use inline math instead.
|
||||
# Set to 0 when done using, as it affects ANY value. Priority is as listed.
|
||||
value_mul|value_* = (number)|(offset)|(field)
|
||||
value_div|value_/ = (number)|(offset)|(field)
|
||||
@ -339,44 +341,60 @@ sample_rate = 0x04 # sample rate is the same for all subsongs
|
||||
# Nth subsong ch: 0x04+0x00*N: 0x08
|
||||
```
|
||||
|
||||
### Modifiers
|
||||
### Math
|
||||
Sometimes header values are in "sectors" or similar concepts (typical in DVD games), and need to be adjusted to a real value.
|
||||
```
|
||||
value_multiply = 0x800 # offsets are in DVD sector size
|
||||
start_offset = @0x10 # 0x15*0x800, for example
|
||||
value_multiply = 0 # next values don't need to be multiplied
|
||||
start_offset = @0x14
|
||||
sample_type = bytes
|
||||
start_offset = @0x10 * 0x800 # 0x15 * DVD sector size, for example
|
||||
```
|
||||
|
||||
You can also use certain fields' values:
|
||||
```
|
||||
value_add = 1
|
||||
channels = @0x08 # may be 1 + 1 = 2
|
||||
value_add = 0
|
||||
num_samples = @0x10 * channels # byte-to-samples of channel_size
|
||||
```
|
||||
`data_size` is a special value for `num_samples` and `loop_end_sample` and will always convert as bytes-to-samples, though.
|
||||
|
||||
value_multiply = channels # now set to 2
|
||||
sample_type = bytes
|
||||
num_samples = @0x10 # channel_size * channels
|
||||
|
||||
Priority is left-to-right. Do add brackets though, they are accounted for and if they are implemented in the future your .txth *will* break with impunity.
|
||||
```
|
||||
# normal priority
|
||||
data_size = @0x10 * 0x800 + 0x800
|
||||
# also works
|
||||
data_size = (@0x10 + 1) * 0x800
|
||||
# same as above but don't do this
|
||||
# (may become @0x10 + (1 * 0x800) in the future
|
||||
data_size = @0x10 + 1 * 0x800
|
||||
# fails, wrong bracket count
|
||||
data_size = (@0x10 + 1 * 0x800
|
||||
# fails, wrong bracket count
|
||||
data_size = )@0x10 + 1 * 0x800
|
||||
```
|
||||
|
||||
If a TXTH needs too many calculations it may be better to implement directly in vgmstream though, consider reporting.
|
||||
|
||||
|
||||
### Modifiers
|
||||
Remnant of simpler math (priority is fixed to */+-), shouldn't be needed anymore.
|
||||
|
||||
```
|
||||
value_multiply = 0x800
|
||||
start_offset = @0x10
|
||||
value_multiply = 0
|
||||
```
|
||||
|
||||
```
|
||||
value_add = 1
|
||||
channels = @0x08
|
||||
value_add = 0
|
||||
|
||||
value_multiply = channels
|
||||
sample_type = bytes
|
||||
num_samples = @0x10
|
||||
value_multiply = 0
|
||||
```
|
||||
num_samples and loop_end_sample will always convert "data_size" field as bytes-to-samples though.
|
||||
|
||||
Priority is fixed to */+-:
|
||||
```
|
||||
value_add = 0x10
|
||||
value_mul = 0x800
|
||||
start_offset = @0x10 # (0x15*0x800) + 0x10 = 0xA810
|
||||
start_offset = @0x10
|
||||
```
|
||||
|
||||
But with some creativity you can do fairly involved stuff:
|
||||
```
|
||||
value_add = 0x10
|
||||
start_offset = @0x10 # (0x15+0x10) = 0x25
|
||||
value_add = 0
|
||||
|
||||
value_mul = 0x800
|
||||
start_offset = start_offset # (0x25*0x800) = 0x12800
|
||||
value_mul = 0
|
||||
```
|
||||
|
||||
If a TXTH needs too many complex calculations it may be better to implement directly in vgmstream though.
|
||||
|
190
src/meta/txth.c
190
src/meta/txth.c
@ -575,8 +575,8 @@ static int parse_txth(txth_header * txth) {
|
||||
|
||||
txt_offset += bytes_read;
|
||||
|
||||
/* get key/val (ignores lead/trail spaces, stops at space/comment/separator) */
|
||||
ok = sscanf(line, " %[^ \t#=] = %[^ \t#\r\n] ", key,val);
|
||||
/* get key/val (ignores lead spaces, stops at space/comment/separator) */
|
||||
ok = sscanf(line, " %[^ \t#=] = %[^\t#\r\n] ", key,val);
|
||||
if (ok != 2) /* ignore line if no key=val (comment or garbage) */
|
||||
continue;
|
||||
|
||||
@ -881,6 +881,13 @@ fail:
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int starts_with(const char * val, const char * cmp) {
|
||||
int len = strlen(cmp);
|
||||
if (strncmp(val, cmp, len) == 0)
|
||||
return len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_num(STREAMFILE * streamFile, txth_header * txth, const char * val, uint32_t * out_value) {
|
||||
/* out_value can be these, save before modifying */
|
||||
uint32_t value_mul = txth->value_mul;
|
||||
@ -889,79 +896,140 @@ static int parse_num(STREAMFILE * streamFile, txth_header * txth, const char * v
|
||||
uint32_t value_sub = txth->value_sub;
|
||||
uint32_t subsong_offset = txth->subsong_offset;
|
||||
|
||||
if (val[0] == '@') { /* offset */
|
||||
uint32_t offset = 0;
|
||||
char ed1 = 'L', ed2 = 'E';
|
||||
int size = 4;
|
||||
int big_endian = 0;
|
||||
int hex = (val[1]=='0' && val[2]=='x');
|
||||
char op = ' ';
|
||||
int brackets = 0;
|
||||
uint32_t result = 0;
|
||||
|
||||
/* can happen when loading .txth and not setting body/head */
|
||||
if (!streamFile)
|
||||
goto fail;
|
||||
//;VGM_LOG("TXTH: initial val '%s'\n", val);
|
||||
|
||||
/* read exactly N fields in the expected format */
|
||||
if (strchr(val,':') && strchr(val,'$')) {
|
||||
if (sscanf(val, hex ? "@%x:%c%c$%i" : "@%u:%c%c$%i", &offset, &ed1,&ed2, &size) != 4) goto fail;
|
||||
} else if (strchr(val,':')) {
|
||||
if (sscanf(val, hex ? "@%x:%c%c" : "@%u:%c%c", &offset, &ed1,&ed2) != 3) goto fail;
|
||||
} else if (strchr(val,'$')) {
|
||||
if (sscanf(val, hex ? "@%x$%i" : "@%u$%i", &offset, &size) != 2) goto fail;
|
||||
} else {
|
||||
if (sscanf(val, hex ? "@%x" : "@%u", &offset) != 1) goto fail;
|
||||
|
||||
/* read "val" format: @(offset) (op) (field) (op) (number) ... */
|
||||
while (val[0] != '\0') {
|
||||
uint32_t value = 0;
|
||||
char type = val[0];
|
||||
int value_read = 0;
|
||||
int n = 0;
|
||||
|
||||
if (type == ' ') { /* ignore */
|
||||
n = 1;
|
||||
}
|
||||
else if (type == '(') { /* bracket */
|
||||
brackets++;
|
||||
n = 1;
|
||||
}
|
||||
else if (type == ')') { /* bracket */
|
||||
if (brackets == 0) goto fail;
|
||||
brackets--;
|
||||
n = 1;
|
||||
}
|
||||
else if (type == '+' || type == '-' || type == '/' || type == '*') { /* op */
|
||||
op = type;
|
||||
n = 1;
|
||||
}
|
||||
else if (type == '@') { /* offset */
|
||||
uint32_t offset = 0;
|
||||
char ed1 = 'L', ed2 = 'E';
|
||||
int size = 4;
|
||||
int big_endian = 0;
|
||||
int hex = (val[1]=='0' && val[2]=='x');
|
||||
|
||||
/* can happen when loading .txth and not setting body/head */
|
||||
if (!streamFile)
|
||||
goto fail;
|
||||
|
||||
/* read exactly N fields in the expected format */
|
||||
if (strchr(val,':') && strchr(val,'$')) {
|
||||
if (sscanf(val, hex ? "@%x:%c%c$%i%n" : "@%u:%c%c$%i%n", &offset, &ed1,&ed2, &size, &n) != 4) goto fail;
|
||||
} else if (strchr(val,':')) {
|
||||
if (sscanf(val, hex ? "@%x:%c%c%n" : "@%u:%c%c%n", &offset, &ed1,&ed2, &n) != 3) goto fail;
|
||||
} else if (strchr(val,'$')) {
|
||||
if (sscanf(val, hex ? "@%x$%i%n" : "@%u$%i%n", &offset, &size, &n) != 2) goto fail;
|
||||
} else {
|
||||
if (sscanf(val, hex ? "@%x%n" : "@%u%n", &offset, &n) != 1) goto fail;
|
||||
}
|
||||
|
||||
if (/*offset < 0 ||*/ offset > get_streamfile_size(streamFile))
|
||||
goto fail;
|
||||
|
||||
if (ed1 == 'B' && ed2 == 'E')
|
||||
big_endian = 1;
|
||||
else if (!(ed1 == 'L' && ed2 == 'E'))
|
||||
goto fail;
|
||||
|
||||
if (subsong_offset)
|
||||
offset = offset + subsong_offset * (txth->target_subsong - 1);
|
||||
|
||||
switch(size) {
|
||||
case 1: value = read_8bit(offset,streamFile); break;
|
||||
case 2: value = big_endian ? (uint16_t)read_16bitBE(offset,streamFile) : (uint16_t)read_16bitLE(offset,streamFile); break;
|
||||
case 3: value = (big_endian ? (uint32_t)read_32bitBE(offset,streamFile) : (uint32_t)read_32bitLE(offset,streamFile)) & 0x00FFFFFF; break;
|
||||
case 4: value = big_endian ? (uint32_t)read_32bitBE(offset,streamFile) : (uint32_t)read_32bitLE(offset,streamFile); break;
|
||||
default: goto fail;
|
||||
}
|
||||
value_read = 1;
|
||||
}
|
||||
else if (type >= '0' && type <= '9') { /* unsigned constant */
|
||||
int hex = (val[0]=='0' && val[1]=='x');
|
||||
|
||||
if (sscanf(val, hex ? "%x%n" : "%u%n", &value, &n) != 1)
|
||||
goto fail;
|
||||
value_read = 1;
|
||||
}
|
||||
else { /* known field */
|
||||
if ((n = starts_with(val,"interleave"))) value = txth->interleave;
|
||||
if ((n = starts_with(val,"interleave_last"))) value = txth->interleave_last;
|
||||
else if ((n = starts_with(val,"channels"))) value = txth->channels;
|
||||
else if ((n = starts_with(val,"sample_rate"))) value = txth->sample_rate;
|
||||
else if ((n = starts_with(val,"start_offset"))) value = txth->start_offset;
|
||||
else if ((n = starts_with(val,"data_size"))) value = txth->data_size;
|
||||
else if ((n = starts_with(val,"num_samples"))) value = txth->num_samples;
|
||||
else if ((n = starts_with(val,"loop_start_sample"))) value = txth->loop_start_sample;
|
||||
else if ((n = starts_with(val,"loop_end_sample"))) value = txth->loop_end_sample;
|
||||
else if ((n = starts_with(val,"subsong_count"))) value = txth->subsong_count;
|
||||
else if ((n = starts_with(val,"subsong_offset"))) value = txth->subsong_offset;
|
||||
else goto fail;
|
||||
value_read = 1;
|
||||
}
|
||||
|
||||
if (/*offset < 0 ||*/ offset > get_streamfile_size(streamFile))
|
||||
goto fail;
|
||||
/* apply simple left-to-right math though, for now "(" ")" are counted and validated
|
||||
* (could use good ol' shunting-yard algo but whatevs) */
|
||||
if (value_read) {
|
||||
//;VGM_ASSERT(op != ' ', "MIX: %i %c %i\n", result, op, value);
|
||||
switch(op) {
|
||||
case '+': value = result + value; break;
|
||||
case '-': value = result - value; break;
|
||||
case '*': value = result * value; break;
|
||||
case '/': if (value == 0) goto fail; value = result / value; break;
|
||||
default: break;
|
||||
}
|
||||
op = ' '; /* consume */
|
||||
|
||||
if (ed1 == 'B' && ed2 == 'E')
|
||||
big_endian = 1;
|
||||
else if (!(ed1 == 'L' && ed2 == 'E'))
|
||||
goto fail;
|
||||
|
||||
if (subsong_offset)
|
||||
offset = offset + subsong_offset * (txth->target_subsong - 1);
|
||||
|
||||
switch(size) {
|
||||
case 1: *out_value = read_8bit(offset,streamFile); break;
|
||||
case 2: *out_value = big_endian ? (uint16_t)read_16bitBE(offset,streamFile) : (uint16_t)read_16bitLE(offset,streamFile); break;
|
||||
case 3: *out_value = (big_endian ? (uint32_t)read_32bitBE(offset,streamFile) : (uint32_t)read_32bitLE(offset,streamFile)) & 0x00FFFFFF; break;
|
||||
case 4: *out_value = big_endian ? (uint32_t)read_32bitBE(offset,streamFile) : (uint32_t)read_32bitLE(offset,streamFile); break;
|
||||
default: goto fail;
|
||||
result = value;
|
||||
}
|
||||
}
|
||||
else if (val[0] >= '0' && val[0] <= '9') { /* unsigned constant */
|
||||
int hex = (val[0]=='0' && val[1]=='x');
|
||||
|
||||
if (sscanf(val, hex ? "%x" : "%u", out_value)!=1)
|
||||
goto fail;
|
||||
}
|
||||
else { /* known field */
|
||||
if (0==strcmp(val,"interleave")) *out_value = txth->interleave;
|
||||
if (0==strcmp(val,"interleave_last")) *out_value = txth->interleave_last;
|
||||
else if (0==strcmp(val,"channels")) *out_value = txth->channels;
|
||||
else if (0==strcmp(val,"sample_rate")) *out_value = txth->sample_rate;
|
||||
else if (0==strcmp(val,"start_offset")) *out_value = txth->start_offset;
|
||||
else if (0==strcmp(val,"data_size")) *out_value = txth->data_size;
|
||||
else if (0==strcmp(val,"num_samples")) *out_value = txth->num_samples;
|
||||
else if (0==strcmp(val,"loop_start_sample")) *out_value = txth->loop_start_sample;
|
||||
else if (0==strcmp(val,"loop_end_sample")) *out_value = txth->loop_end_sample;
|
||||
else if (0==strcmp(val,"subsong_count")) *out_value = txth->subsong_count;
|
||||
else if (0==strcmp(val,"subsong_offset")) *out_value = txth->subsong_offset;
|
||||
else goto fail;
|
||||
/* move to next field (if any) */
|
||||
val += n;
|
||||
|
||||
//;VGM_LOG("TXTH: val='%s', n=%i, brackets=%i, result=%i\n", val, n, brackets, result);
|
||||
}
|
||||
|
||||
/* operators, but only if current value wasn't set to 0 right before */
|
||||
/* unbalanced brackets */
|
||||
if (brackets > 0)
|
||||
goto fail;
|
||||
|
||||
/* global operators, but only if current value wasn't set to 0 right before */
|
||||
if (value_mul && txth->value_mul)
|
||||
*out_value = (*out_value) * value_mul;
|
||||
result = result * value_mul;
|
||||
if (value_div && txth->value_div)
|
||||
*out_value = (*out_value) / value_div;
|
||||
result = result / value_div;
|
||||
if (value_add && txth->value_add)
|
||||
*out_value = (*out_value) + value_add;
|
||||
result = result + value_add;
|
||||
if (value_sub && txth->value_sub)
|
||||
*out_value = (*out_value) - value_sub;
|
||||
result = result - value_sub;
|
||||
|
||||
//;VGM_LOG("TXTH: val=%s, read %u (0x%x)\n", val, *out_value, *out_value);
|
||||
*out_value = result;
|
||||
|
||||
//;VGM_LOG("TXTH: final result %u (0x%x)\n", result, result);
|
||||
return 1;
|
||||
fail:
|
||||
return 0;
|
||||
|
Loading…
x
Reference in New Issue
Block a user