From 2c68123738b176ef7de20fc9db33758a3bfa3884 Mon Sep 17 00:00:00 2001 From: bnnm Date: Thu, 5 Dec 2019 22:20:06 +0100 Subject: [PATCH] CRLF to LF --- doc/TXTP.md | 1420 +++++++++++----------- src/meta/txtp.c | 3002 +++++++++++++++++++++++------------------------ 2 files changed, 2211 insertions(+), 2211 deletions(-) diff --git a/doc/TXTP.md b/doc/TXTP.md index e0afd858..9cbbe418 100644 --- a/doc/TXTP.md +++ b/doc/TXTP.md @@ -1,710 +1,710 @@ -# TXTP FORMAT - -TXTP is a text file with commands, to improve support for games using audio in certain uncommon or undesirable ways. It's in the form of a mini-playlist or a wrapper with play settings, meant to do post-processing over playable files. - -Simply create a file named `(filename).txtp`, and inside write the commands described below. - - -## TXTP MODES -TXTP can join and play together multiple songs in various ways by setting a file list and mode: -``` -file1 -... -fileN - -mode = (mode) # "segments" is the default if not set -``` -You can set commands to alter how files play (described later). Having a single file is ok too, so are subdirs: -``` -# set "subsong" command for single file inside subdir -sounds/file#12 - -# will be ignored as none make sense here and is treated as "single" mode -#mode = layers/segments/mixed -``` - - -### Segments mode -Some games clumsily loop audio by using multiple full file "segments", so you can play separate intro + loop files together as a single track. - -**Ratchet & Clank (PS2)**: *bgm01.txtp* -``` -# define 2 or more segments to play as one -BGM01_BEGIN.VAG -BGM01_LOOPED.VAG - -# segments must define loops -loop_start_segment = 2 # 2nd file start -loop_end_segment = 2 # optional, default is last -mode = segments # optional, default is segments -``` - -If your loop segment has proper loops you want to keep, you can use: -``` -BGM_SUMMON_0001_01-Intro.hca -BGM_SUMMON_0001_01-Intro2.hca -BGM_SUMMON_0001_01.hca - -loop_start_segment = 3 -loop_mode = keep # loops in 3rd file's loop_start to 3rd file's loop_end -``` -``` -bgm_intro.adx -bgm_main.adx -bgm_main2.adx - -loop_start_segment = 2 -loop_end_segment = 3 -loop_mode = keep # loops in 2nd file's loop_start to 3rd file's loop_end -``` -Mixing sample rates is ok (uses first) but channel number must be equal for all files. You can use mixing (explained later) to join segments of different channels though. - - -### Layers mode -Some games layer channels or dynamic parts that must play at the same time, for example main melody + vocal track. - -**Nier Automata**: *BGM_0_012_song2.txtp* -``` -# mix dynamic sections (2ch * 2) -BGM_0_012_04.wem -BGM_0_012_07.wem - -mode = layers -``` - -**Life is Strange**: *BIK_E1_6A_DialEnd.txtp* -``` -# bik multichannel isn't autodetectable so must mix manually (1ch * 3) -BIK_E1_6A_DialEnd_00000000.audio.multi.bik#1 -BIK_E1_6A_DialEnd_00000000.audio.multi.bik#2 -BIK_E1_6A_DialEnd_00000000.audio.multi.bik#3 - -mode = layers -``` -Note that the number of channels is the sum of all layers, so three 2ch layers play as a 6ch file. If all layers share loop points they are automatically kept. - -### Mixed groups -You can set "groups" to 'fold' various files into one, as layers or segments, to allow complex cases: -``` -# commands to make two 6ch segments with layered intro + layered loop: - -# - set introA+B+C as layer (this group becomes position 1, and loopA_2ch position 2) -introA_2ch.at3 #position 1 -introB_2ch.at3 -introC_2ch.at3 -group = 1L3 - -# - set loopA+B+C as layer (this group becomes position 2) -loopA_2ch.at3 #position 4 -loopB_2ch.at3 -loopC_2ch.at3 -group = 2L3 - -# - play both as segments (this step is optional if using mode = segments) -group = S2 - -# - set loop start loopA+B+C (new position 2, not original position 4) -loop_start_segment = 2 - -# optional, to avoid "segments" default (for debugging) -mode = mixed -``` -From TXTP's perspective, it starts with N separate files and every command joins some files that are treated as a single new file, so positions are reassigned. End result will be a single "file" that may contain groups within groups. It's pretty flexible so you can express similar things in various ways: -``` -# commands to make a 6ch with segmented intro + loop: -introA_2ch.at3 -mainA_2ch.at3 - -introB_2ch.at3 -mainB_2ch.at3 - -introC_2ch.at3 -mainC_2ch.at3 - -# - group intro/main pairs as segments, starting from 1 and repeating for A/B/C -group = S2R - -# - play all as layer (can't set loop_start_segment in this case) -mode = layers - -# you could also set: group = L and mode = mixed, same thing -``` - -`group` can go anywhere in the .txtp, as many times as needed (groups are read and kept in an list that is applied in order at the end). Format is `(position)(type)(count)(repeat)`: -- `position`: file start (optional, default is 1 = first) -- `type`: group as S=segments or L=layers -- `count`: number of files in group (optional, default is all) -- `repeat`: R=repeat group of `count` files until end (optional, default is no repeat) - -Examples: -- `L`: take all files as layers (equivalent to `mode = layers`) -- `S`: take all files as segments (equivalent to `mode = segments`) -- `3L2`: layer 2 files starting from file 3 -- `2L3R`: group every 3 files from position 2 as layers -- `1S1`: segment of one file (useless thus ignored) -- `1L1`: layer of one file (same) -- `9999L`: absurd values are ignored - -Segments and layer settings and rules still apply, so you can't make segments of files with different total channels. To do it you can use commands to "downmix" the group, as well as giving it some config (explained later): -``` -# this doesn't need to be grouped -intro_2ch.at3 - -# this is grouped into a single 4ch file, then downmixed to stereo -mainA_2ch.at3 -mainB_2ch.at3 -group = 2L2 #@layer-v 2 - -# finally resulting layers are played as segments (2ch, 2ch) -# (could set a group = S and ommit S here, too) -mode = segments - -# if the last group joins all as segments you can use loop_start -loop_start_segment = 3 #refers to final group at position 2 -loop_mode = keep -``` - - -## TXTP COMMANDS -You can set file commands by adding multiple `#(command)` after the name. `# (anything)` is considered a comment and ignored, as well as any command not understood. - -### Subsong selection for bank formats -**`#(number)` or `#s(number)`**: set subsong (number) - -**Super Robot Taisen OG Saga - Masou Kishin III - Pride of Justice (Vita)**: *bgm_12.txtp* -``` -# select subsong 12 -bgm.sxd2#12 - -#bgm.sxd2#s12 # "sN" is alt for subsong - -# single files loop normally by default (see below to force looping) -``` - -### Play segmented subsong ranges as one -**`#(number)~(number)` or `#s(number)~(number)`**: set multiple subsong segments at a time, to avoid so much C&P - -**Prince of Persia Sands of Time**: *song_01.txtp* -``` -amb_fx.sb0#254 -amb_fx.sb0#122~144 -amb_fx.sb0#121 - -#3rd segment = subsong 123, not 3rd subsong -loop_start_segment = 3 -``` -This is just a shorthand, so `song#1~3#h22050` is equivalent to: -``` -song#1#h22050 -song#2#h22050 -song#3#h22050 -``` - - -### Channel mask for channel subsongs/layers -**`#c(number)`** (single) or **`#c(number)~(number)`** (range): set number of channels to play. You can add multiple comma-separated numbers, or use ` ` space or `-` as separator and combine multiple ranges with single channels too. - -**Final Fantasy XIII-2**: *music_Home_01.ps3.txtp* -``` -#plays channels 1 and 2 = 1st subsong -music_Home.ps3.scd#c1,2 -``` - -``` -#plays channels 3 and 4 = 2nd subsong -music_Home.ps3.scd#c3 4 - -#plays 1 to 3 -music_Home.ps3.scd#c1~3 -``` -Doesn't change the final number of channels though, just mutes non-selected channels. - -If you use **`C(number)`** it will remove non-selected channels (not done directly for backwards compatibility). This just a shortcut for macro `#@track` (described later): -``` -#plays channels 3 and 4 = 2nd subsong and removes other channels -music_Home.ps3.scd#C3 4 -``` - - -### Play settings -**`#l(loops)`**, **`#f(fade)`**, **`#d(fade-delay)`**, **`#i(ignore loop)`**, **`#F(ignore fade)`**, **`#E(end-to-end loop)`** - -Those setting should override player's defaults if set (except "loop forever"). They are equivalent to some test.exe options. - -**God Hand (PS2)**: *boss2_3ningumi_ver6.txtp* (each line is a separate TXTP) -``` -# set number of loops -boss2_3ningumi_ver6.adx#l3 - -# set fade time (in seconds) -boss2_3ningumi_ver6.adx#f10.5 - -# set fade delay (in seconds) -boss2_3ningumi_ver6.adx#d0.5 - -# ignore and disable loops -boss2_3ningumi_ver6.adx#i - -# don't fade out and instead play the song ending after -boss2_3ningumi_ver6.adx#F # this song has a nice stop - -# force full loops from end-to-end -boss2_3ningumi_ver6.adx#E - -# settings can be combined -boss2_3ningumi_ver6.adx#l2#F # 2 loops + ending - -# settings can be combined -boss2_3ningumi_ver6.adx#l1.5#d1#f5 - -# boss2_3ningumi_ver6.adx#l1.0#F # this is equivalent to #i -``` - - -### Time modifications -**`#t(time)`**: trims the file so base duration (before applying loops/fades/etc) is `(time)`. If value is negative substracts `(time)` to duration. Loop end is adjusted when necessary, and ignored if value is bigger than possible (use `#l(loops)` config to extend time instead). - -Time values can be `M:S(.n)` (minutes and seconds), `S.n` (seconds with dot), `0xN` (samples in hex format) or `N` (samples). Beware of the subtle difference between 10.0 (ten seconds) and 10 (ten samples). - -Some segments have padding/silence at the end for some reason, that don't allow smooth transitions. You can fix it like this: -``` -intro.fsb #t -1.0 #intro segment has 1 second of silence. -main.fsb -``` - -Similarly other games don't use loop points, but rather repeat/loops the song internally many times: -``` -intro.vag #t3:20 #i #l1.0 #trim + combine with forced loops for easy fades -``` - -Note that if you need to remove very few samples (like 1) to get smooth transitions it may be a bug in vgmstream, consider reporting. - - -### Force sample rate -**`#h(sample rate)`**: changes sample rate to selected value (within some limits). - -Needed for a few games set a sample rate value in the header but actually play with other (applying some of pitch or just forcing it). - -**Super Paper Mario (Wii)** -``` -btl_koopa1_44k_lp.brstm#h22050 #in hz -``` -**Patapon (PSP)** -``` -ptp_btl_bgm_voice.sgd#s1#h11050 -``` - - -### Install loops -**`#I(loop start time) (loop end time)`**: force/override looping values, same as .pos but nicer. Loop end is optional and defaults to total samples. - -Time values can be `M:S(.n)` (minutes and seconds), `S.n` (seconds with dot), `0xN` (samples in hex format) or `N` (samples). Beware of the subtle difference between 10.0 (ten seconds) and 10 (ten samples). Wrong loop values (for example loop end being much larger than file's samples) will be ignored, but there is some leeway when using seconds for the loop end. - -**Jewels Ocean (PC)** -``` -bgm01.ogg#I32.231 # from ~1421387 samples to end - -#bgm01.ogg#I 0:32.231 # equivalent -#bgm01.ogg#I 1421387 4212984 # equivalent, end is 4212984 -#bgm01.ogg#I32.231 1_35.533 # equivalent, end over file samples (~4213005) but adjusted to 4212984 -#bgm01.ogg#I 1421387 4212985 # ignored, end over file samples -#bgm01.ogg#I32.231 1_37 # ignored, end over file (~4277700) but clearly wrong -``` - -Use this feature responsibly, though. If you find a format that should loop using internal values that vgmstream doesn't detect correctly, consider reporting the bug for the benefit of all users and other games using the same format, and don't throw out the original loop definitions (as sometimes they may not take into account "encoder delay" and must be carefully adjusted). - -Note that a few codecs may not work with arbitrary loop values since they weren't tested with loops. Misaligned loops will cause audible "clicks" at loop point too. - - -### Channel mixing -**`#m(op),(...),(op)`**: mix channels in various ways by specifying multiple comma-separated sub-commands: - -Possible operations: -- `N-M`: swaps M with N -- `N+M*(volume)`: mixes M * volume to N -- `N+M`: mixes M to N -- `N*(volume)`: changes volume of N -- `N=(volume)`: limits volume of N -- `Nu`: upmix (insert) N ('pushing' all following channels forward) -- `Nd`: downmix (remove) N ('pulling' all following channels backward) -- `ND`: downmix (remove) N and all following channels -- `N(type)(position)(time-start)+(time-length)`: defines a fade - * `type` can be `{` = fade-in, `}` = fade-out, `(` = crossfade-in, `)` = crossfade-out - * crossfades are better tuned to use when changing between tracks - * `(position)` pre-adjusts `(time-start)` to start after certain time (optional) - * using multiple fades in the same channel will cancel previous fades - * may only cancel when fade is after previous one - * `}` then `{` or `{` then `}` makes sense, but `}` then `}` will make funny volume bumps - * example: `1{0:10+0:5, 1}0:30+0:5` fades-in at 10 seconds, then fades-out at 30 seconds -- `N^(volume-start)~(volume-end)=(shape)@(time-pre)~(time-start)+(time-length)~(time-last)`: defines full fade envelope - * full definition of a fade envelope to allow precise volume changes over time - * not necessarily fades, as you could set length 0 for volume "bumps" like `1.0~0.5` - * `(shape)` can be `{` = fade, `(` = crossfade, other values are reserved for internal testing and may change anytime - * `(time-start)`+`(time-length)` make `(time-end)` - * between `(time-pre)` and `(time-start)` song uses `(volume-start)` - * between `(time-start)` and `(time-end)` song gradually changes `(volume-start)` to `(volume-end)` (depending on `(shape)`) - * between `(time-end)` and `(time-post)` song uses `(volume-end)` - * `time-pre/post` may be -1 to set "file start" and "file end", cancelled by next fade - -Considering: -- `N` and `M` are channels (*current* value after previous operators are applied) -- channel 1 is first -- channel 0 is shorthand for all channels where applicable (`0*V`, `0=V`, `0^...`) -- may use `x` instead of `*` and `_` instead of `:` (for mini-TXTP) -- `(volume)` is a `N.N` decimal value where 1.0 is 100% base volume - - negative volume inverts the waveform (for weird effects) -- `(position)` can be `N.NL` or `NL` = N.N loops - - if loop start is 1000 and loop end 5000, `0.0L` = 1000 samples, `1.0L` = 5000 samples, `2.0L` = 9000 samples, etc -- `(time)` can be `N:NN(.n)` (minutes:seconds), `N.N` (seconds) or `N` (samples) - - represents the file's global play time, so it may be set after N loops - - beware of `10.0` (ten seconds) vs `10` (ten samples) - - may not work with huge numbers (like several hours) -- adding trailing channels must be done 1 by 1 at the end (for stereo: `3u,4u,(...)` -- nonsensical values are ignored (like referencing channel 3 in a stereo file) - -Main usage would be creating stereo files for games that layer channels. -``` -# quad to stereo: all layers must play at the same time -# - mix 75% of channel 3/4 into channel 1/2, then drop channel 3 and 4 -song#m1+3*0.75,2+4*.75,3D - -# quad to stereo: only channel 3 and 4 should play -# - swap channel 1/2 with 3/4, then drop channel 3/4 -song#m1-3,2-4,3D - -# also equivalent, but notice the order -# - drop channel 1 then 2 (now 1) -song#m1d,1d -``` -Proper mixing requires some basic knowledge though, it's further explained later. Order matters and operations are applied sequentially, for extra flexibility at the cost of complexity and user-friendliness, and may result in surprising mixes. Try to stick to macros and simple combos, using later examples as a base. - -This can be applied to individual layers and segments, but normally you want to use `commands` to apply mixing to the resulting file (see examples). Per-segment mixing should be reserved to specific up/downmixings. - -Mixing must be supported by the plugin, otherwise it's ignored (there is a negligible performance penalty per mix operation though). - - -### Macros -**`#@(macro name and parameters)`**: adds a new macro - -Manually setting values gets old, so TXTP supports a bunch of simple macros. They automate some of the above commands (analyzing the file), and may be combined, so order still matters. -- `volume N (channels)`: sets volume V to selected channels -- `track (channels)`: makes a file of selected channels -- `layer-v N (channels)`: mixes selected channels to N channels with default volume (for layered vocals) -- `layer-b N (channels)`: same, but adjusts volume depending on layers (for layered bgm) -- `layer-e N (channels)`: same, but adjusts volume equally for all layers (for generic downmixing) -- `remix N (channels)`: same, but mixes selected channels to N channels properly adjusting volume (for layered bgm) -- `crosstrack N`: crossfades between Nch tracks after every loop (loop count is adjusted as needed) -- `crosslayer-v/b/e N`: crossfades Nch layers to the main track after every loop (loop count is adjusted as needed) -- `downmix`: downmixes up to 8 channels (7.1, 5.1, etc) to stereo, using standard downmixing formulas. - -`channels` can be multiple comma-separated channels or N~M ranges and may be ommited were applicable to mean "all channels" (channel order doesn't matter but it's internally fixed). - -Examples: -``` -# plays 2ch layer1 (base melody) -okami-ryoshima_coast.aix#@track 1,2 - -# plays 2ch layer1+2 (base melody+percussion) -okami-ryoshima_coast.aix#@layer-b 2 1~4 #1~4 may be skipped - -# uses 2ch layer1 (base melody) in the first loop, adds 2ch layer2 (percussion) to layer1 in the second -okami-ryoshima_coast.aix#@crosslayer-b 2 - -# uses 2ch track1 (exploration) in the first loop, changes to 2ch track2 (combat) in the second -ffxiii2-eclipse.scd#@crosstrack 2 - -# plays 2ch from 4ch track1 (sneaking) -mgs4-bgm_ee_alert_01.mta2#@layer-e 2 1~4 - -# downmix bgm + vocals to stereo -nier_automata-BGM_0_012_04.wem -nier_automata-BGM_0_012_07.wem -mode = layers -commands = #@layer-v 2 - -# can be combined with normal mixes too for creative results -# (add channel clone of ch1, then 50% of range) -song#m4u,4+1#@volume 0.5 2~4 - -# equivalent to #@layer-e 2 1~4 -mgs4-bgm_ee_alert_01.mta2#@track 1~4#@layer-b 2 - -# equivalent to #@track 1,2 -okami-ryoshima_coast.aix#@layer-b 2 1,2 -``` - - -## OTHER FEATURES - -### Default commands -You can set defaults that apply to the *resulting* file. This has subtle differences vs per-file config: -``` -BGM01_BEGIN.VAG -BGM01_LOOPED.VAG - -# force looping from begin to end of the whole thing -commands = #E -``` -``` -# mix 2ch * 2 -BGM_0_012_04.wem -BGM_0_012_07.wem -mode = layers - -# plays R of BGM_0_012_04 and L of BGM_0_012_07 -commands = #c2,3 -``` - -As it applies at the end, some options with ambiguous or technically hard to handle meanings may be ignored: -``` -bgm.sxd2 -bgm.sxd2 - -# ignored (resulting file has no subsongs, or should apply to all?) -commands = #s12 -``` - -### Force plugin extensions -vgmstream supports a few common extensions that confuse plugins, like .wav/ogg/aac/opus/etc, so for them those extensions are disabled and are expected to be renamed to .lwav/logg/laac/lopus/etc. TXTP can make plugins play those disabled extensions, since it calls files directly by filename. - -Combined with TXTH, this can also be used for extensions that aren't normally accepted by vgmstream. - - -### TXTP combos -TXTP may even reference other TXTP, or files that require TXTH, for extra complex cases. Each file defined in TXTP is internally parsed like it was a completely separate file, so there is a bunch of valid ways to mix them. - - -### TXTP parsing -*Filenames* may be anything accepted by the file system, including spaces and symbols, and multiple *commands* can be chained: -``` -bgm bank#s2#c1,2 -``` - -You may add spaces as needed (but try to keep it simple and don't go overboard), though commands *must* start with `#(command)` (`#(space)(anything)` is a comment). Commands without corresponding file are ignored too (seen as comments), while incorrect commands are ignored and skip to next, though the parser may try to make something usable of them (this may be change anytime without warning): -``` -# those are all equivalent -song#s2#c1,2 -song #s2#c1,2 # comment -song #s 2 #c1,2# comment -song #s 2 #c 1 , 2# comment - -#s2 #ignores rogue commands/comments - -# seen as incorrect and ignored -song #s TWO -song #E enable -song #E 1 -song #Enable -song #h -48000 - -# accepted -song #E # comment -song #c1, 2, 3 -song #c 1 2 3 - -# ignores first and reads second -song #s TWO#c1,2 - -# seen as #s1#c1,2 -song #s 1,2 #c1,2 - -# all seen as #h48000 -song #h48000 -song #h 48000hz -song #h 48000mhz - -# ignored -song #h hz48000 - -# ignored as channels don't go that high (may be modified on request) -song #c32,41 - -# swaps 1 with 2 -song #m1-2 -song #m 1 - 2 - -# swaps 1 with "-2", ignored -song #m1 -2 -``` - -*Values* found after *=* allow spaces as well: -``` -song#s2 -loop_start_segment = 1 #s2# #commands here are ignored - -song -commands=#s2 # commands here are allowed -commands= #c1,2 -``` - -Repeated commands overwrite previous setting, except comma-separated commands that are additive: -``` -# overwrites, equivalent to #s2 -song#s1#s2 -``` -``` -# adds, equivalent to #m1-2,3-4,5-6 -song#m1-2#m3-4 -commands = #m5-6 -# also added to song -commands = #l 3.0 -``` - -The parser is fairly simplistic and lax, and may be erratic with edge cases or behave unexpectedly due to unforeseen use-cases and bugs. As filenames may contain spaces or #, certain name patterns could fool it too. Keep in mind this while making .txtp files. - - -## MINI-TXTP -To simplify TXTP creation, if the .txtp doesn't set a name inside then its filename is used directly, including config. Note that extension must be included (since vgmstream needs a full filename). You can set `commands` inside the .txtp too: -- *bgm.sxd2#12.txtp*: plays subsong 12 -- *bgm.sxd2#12.txtp*, , inside has `commands = #@volume 0.5`: plays subsong 12 at half volume -- *bgm.sxd2.txtp*, , inside has `commands = #12 #@volume 0.5`: plays subsong 12 at half volume -- *Ryoshima Coast 1 & 2.aix#C1,2.txtp*: channel downmix -- *boss2_3ningumi_ver6.adx#l2#F.txtp*: loop twice then play song end file normally -- etc - - -## MIXING -Sometimes games use multiple channels in uncommon ways, for example as layered tracks for dynamic music (like main+vocals), or crossfading a stereo song to another stereo song. In those cases we normally would want a stereo track, but vgmstream can't guess how channels are used (since it's game-dependant). To solve this via TXTP you can set mixing output and volumes manually. - -A song file is just data that can contain a (sometimes unlimited) number of channels, that must play in physical speakers. Standard audio formats define how to "map" known channels to speakers: -- `1.0: FC` -- `2.0: FL, FR` -- `2.1: FL, FR, LF` -- `4.0: FL, FR, SL, SR` -- `5.1: FL, FR, FC, LF, SL, SR` -- ... (channels in order, where FL=front left, FC=front center, etc) - -If you only have stereo speakers, when playing a 5.1 file your player may silently transform to stereo, as otherwise you would miss some channels. But a game song's channels can be various things: standard mapped, per-format map, per-game, multilayers combined ("downmixed") to a final stereo file, music then all language tracks, etc. So you need to decide which channels drop or combine and their individual volumes via mixing. - -Say you want to mix 4ch to 2ch (ch3 to ch1, ch4 to ch2). Due to how audio signals work, mixing just combines (adds) sounds. So if channels 1/2 are LOUD, and channels 3/4 are LOUD, you get a LOUDER channel 1/2. To fix this we set mixing volume, for example: `mix channel 3/4 * 0.707 (-3db/30% lower volume) to channel 1/2`: the resulting stereo file is now more listenable. Those volumes are just for standard audio and may work ok for every game though. - -All this means there is no simple, standard way to mix, so you must experiment a bit. - - -### MIXING EXAMPLES -For most common usages you can stick with macros but actual mixing is quite flexible: -``` -# boost volume of all channels by 30% -song#m0*1.3 - -# boost but limit volume (highs don't go too high, while lows sound louder) -song#m0*1.3,0=0.9 - -# downmix 4ch layers to stereo (this may sound too loud) -song#m1+3,2+4,3D - -# downmix 4ch layers to stereo with adjusted volume for latter channels (common 4.0 to 2.0 mixdown) -song#m1+3*0.7,2+4*0.7,3D - -# downmix 4ch layers to stereo with equal adjusted volume (common layer mixdown) -song#m0*0.7,1*0.7,1+3*0.7,2+4*0.7,3D - -# downmix stereo to mono (ignored if file is 1ch) -zelda-cdi.xa#m1d - -# upmix mono to stereo by copying ch1 to ch2 -zelda-cdi.xa#m2*0.0,2+1 - -# downmix 5.1 wav (FL FR FC LFE BL BR) to stereo -# (uses common -3db mixing formula: Fx=Fx + FC*0.707 + Rx*0.707) -song#m1+3*0.707,2+3*0.707,1+5*0.707,2+6*0.707,3D - -# mask sfx track ch3 in a 6ch file -song#m3*0.0 - -# add a fake silent channel to a 5ch file (FL FR FC BL BR) and move it to LFE position -# "make ch6, swap BL with LFE (now FL FR FC LFE BR BL), swap BR with BL (now FL FR FC LFE BL BR) -sf5.hca#m6u,4-6,5-6 - -# mix 50% of channel 3 into 1 and 2, drop 3 -song#m1+3*0.5,2+3*0.5,3d - -# swap ch1 and 2 then change volume to the resulting swapped channel -song#m1-2,2*0.5 - -# fade-in ch3+4 percussion track layer into main track, downmix to stereo -# (may be split in multiple lines, no difference) -okami-ryoshima_coast.aix#l2 -commands = #m3(1:10+0:05 # loop happens after ~1:10 -commands = #m4(1:10+0:05 # ch3/4 are percussion tracks -commands = #m1+3*0.707,2+4*0.707 # ch3/4 always mixed but silent until 1:10 -commands = #m3D # remove channels after all mixing - -# same but fade-out percussion after second loop -okami-ryoshima_coast.aix#l3 -commands = #m3(1:10+0:05,3)2:20+0:05 -commands = #m4(1:10+0:05,4)2:20+0:05 -commands = #m1+3*0.707,2+4*0.707,3D - -# crossfade exploration and combat sections after loop -ffxiii-2~eclipse.aix -commands = #m1)1:50+0:10,2)1:50+0:10 -commands = #m3(1:50+0:10,4(1:50+0:10 -commands = #m1+3,2+4,3D # won't play at the same time, no volume needed - -# ghetto voice removal (invert channel + other channel removes duplicated parts, and vocals are often layered) -song -commands = #m3u,3+1*-1.0,4u,4+2*-1.0 -commands = #m1+4,2+3,3d,3d - -# crosstrack 4ch file 3 times, going back to first track by creating a fake 3rd track with ch1 and 2: -ffxiii2-eclipse.scd#m5u,6u,5+1,6+2#@crosstrack 2 -``` - -Segment/layer downmixing is allowed but try to keep it simple, some mixes accomplish the same things but are a bit strange. -``` -# mix one stereo segment with a mono segment -intro-stereo.hca -loop-mono.hca#m2u - -# this makes mono file -intro-stereo.hca#m2u -loop-stereo.hca#m2u - -# but you normally should do this instead as it's more natural -intro-stereo.hca -loop-stereo.hca -commands = #m2u - -# fading segments work rather unexpectedly -# fades out 1 minute into the _segment_ (could be 2 minutes into the resulting file) -segment1.hca#m0{0:10+10.0 -segment2.hca#m0}1:00+10.0 -# better use: commands = #m0{0:10+10.0,0}2:00+10.0 -# it would work ok it they were layers, but still, better to use commands with the resulting file -``` - -Combine with groups or extra complex cases: -``` -BGM_SUMMON_0001_02-Intro.hca # 2ch file -BGM_SUMMON_0001_02-Intro2.hca # 2ch file - -BGM_SUMMON_0001_02.hca -BGM_SUMMON_0001_02-Vocal.hca -group = 3L2 #@layer-v 2 # layer Main+Vocal as 4ch then downmix to 2ch - -loop_start_segment = 3 #refers to new group at position 3 -loop_mode = keep -``` - -Note how order subtly affects end results: -``` -# after silencing channel 1 mixing is meaningless -song#m1*0.0,2+1 - -# allowed but useless or ignored -song#m1u,1d,1-1,1*1.0,11d,7D - -# this creates a new ch1 with 50% of ch2 (actually old ch1), total 3ch -song#m1u,1+2*0.5 - -# so does this -song#m3u,3+1*0.5,1-3,2-3 - -# this may not be what you want -# (result is a silent ch1, and ch2 with 50% of ch3) -song#m1+2*0.5,1u - -# for a 2ch file 2nd command is ignored, since ch2 is removed after 1st command -song#m1d,2+1*0.5 -``` +# TXTP FORMAT + +TXTP is a text file with commands, to improve support for games using audio in certain uncommon or undesirable ways. It's in the form of a mini-playlist or a wrapper with play settings, meant to do post-processing over playable files. + +Simply create a file named `(filename).txtp`, and inside write the commands described below. + + +## TXTP MODES +TXTP can join and play together multiple songs in various ways by setting a file list and mode: +``` +file1 +... +fileN + +mode = (mode) # "segments" is the default if not set +``` +You can set commands to alter how files play (described later). Having a single file is ok too, so are subdirs: +``` +# set "subsong" command for single file inside subdir +sounds/file#12 + +# will be ignored as none make sense here and is treated as "single" mode +#mode = layers/segments/mixed +``` + + +### Segments mode +Some games clumsily loop audio by using multiple full file "segments", so you can play separate intro + loop files together as a single track. + +**Ratchet & Clank (PS2)**: *bgm01.txtp* +``` +# define 2 or more segments to play as one +BGM01_BEGIN.VAG +BGM01_LOOPED.VAG + +# segments must define loops +loop_start_segment = 2 # 2nd file start +loop_end_segment = 2 # optional, default is last +mode = segments # optional, default is segments +``` + +If your loop segment has proper loops you want to keep, you can use: +``` +BGM_SUMMON_0001_01-Intro.hca +BGM_SUMMON_0001_01-Intro2.hca +BGM_SUMMON_0001_01.hca + +loop_start_segment = 3 +loop_mode = keep # loops in 3rd file's loop_start to 3rd file's loop_end +``` +``` +bgm_intro.adx +bgm_main.adx +bgm_main2.adx + +loop_start_segment = 2 +loop_end_segment = 3 +loop_mode = keep # loops in 2nd file's loop_start to 3rd file's loop_end +``` +Mixing sample rates is ok (uses first) but channel number must be equal for all files. You can use mixing (explained later) to join segments of different channels though. + + +### Layers mode +Some games layer channels or dynamic parts that must play at the same time, for example main melody + vocal track. + +**Nier Automata**: *BGM_0_012_song2.txtp* +``` +# mix dynamic sections (2ch * 2) +BGM_0_012_04.wem +BGM_0_012_07.wem + +mode = layers +``` + +**Life is Strange**: *BIK_E1_6A_DialEnd.txtp* +``` +# bik multichannel isn't autodetectable so must mix manually (1ch * 3) +BIK_E1_6A_DialEnd_00000000.audio.multi.bik#1 +BIK_E1_6A_DialEnd_00000000.audio.multi.bik#2 +BIK_E1_6A_DialEnd_00000000.audio.multi.bik#3 + +mode = layers +``` +Note that the number of channels is the sum of all layers, so three 2ch layers play as a 6ch file. If all layers share loop points they are automatically kept. + +### Mixed groups +You can set "groups" to 'fold' various files into one, as layers or segments, to allow complex cases: +``` +# commands to make two 6ch segments with layered intro + layered loop: + +# - set introA+B+C as layer (this group becomes position 1, and loopA_2ch position 2) +introA_2ch.at3 #position 1 +introB_2ch.at3 +introC_2ch.at3 +group = 1L3 + +# - set loopA+B+C as layer (this group becomes position 2) +loopA_2ch.at3 #position 4 +loopB_2ch.at3 +loopC_2ch.at3 +group = 2L3 + +# - play both as segments (this step is optional if using mode = segments) +group = S2 + +# - set loop start loopA+B+C (new position 2, not original position 4) +loop_start_segment = 2 + +# optional, to avoid "segments" default (for debugging) +mode = mixed +``` +From TXTP's perspective, it starts with N separate files and every command joins some files that are treated as a single new file, so positions are reassigned. End result will be a single "file" that may contain groups within groups. It's pretty flexible so you can express similar things in various ways: +``` +# commands to make a 6ch with segmented intro + loop: +introA_2ch.at3 +mainA_2ch.at3 + +introB_2ch.at3 +mainB_2ch.at3 + +introC_2ch.at3 +mainC_2ch.at3 + +# - group intro/main pairs as segments, starting from 1 and repeating for A/B/C +group = S2R + +# - play all as layer (can't set loop_start_segment in this case) +mode = layers + +# you could also set: group = L and mode = mixed, same thing +``` + +`group` can go anywhere in the .txtp, as many times as needed (groups are read and kept in an list that is applied in order at the end). Format is `(position)(type)(count)(repeat)`: +- `position`: file start (optional, default is 1 = first) +- `type`: group as S=segments or L=layers +- `count`: number of files in group (optional, default is all) +- `repeat`: R=repeat group of `count` files until end (optional, default is no repeat) + +Examples: +- `L`: take all files as layers (equivalent to `mode = layers`) +- `S`: take all files as segments (equivalent to `mode = segments`) +- `3L2`: layer 2 files starting from file 3 +- `2L3R`: group every 3 files from position 2 as layers +- `1S1`: segment of one file (useless thus ignored) +- `1L1`: layer of one file (same) +- `9999L`: absurd values are ignored + +Segments and layer settings and rules still apply, so you can't make segments of files with different total channels. To do it you can use commands to "downmix" the group, as well as giving it some config (explained later): +``` +# this doesn't need to be grouped +intro_2ch.at3 + +# this is grouped into a single 4ch file, then downmixed to stereo +mainA_2ch.at3 +mainB_2ch.at3 +group = 2L2 #@layer-v 2 + +# finally resulting layers are played as segments (2ch, 2ch) +# (could set a group = S and ommit S here, too) +mode = segments + +# if the last group joins all as segments you can use loop_start +loop_start_segment = 3 #refers to final group at position 2 +loop_mode = keep +``` + + +## TXTP COMMANDS +You can set file commands by adding multiple `#(command)` after the name. `# (anything)` is considered a comment and ignored, as well as any command not understood. + +### Subsong selection for bank formats +**`#(number)` or `#s(number)`**: set subsong (number) + +**Super Robot Taisen OG Saga - Masou Kishin III - Pride of Justice (Vita)**: *bgm_12.txtp* +``` +# select subsong 12 +bgm.sxd2#12 + +#bgm.sxd2#s12 # "sN" is alt for subsong + +# single files loop normally by default (see below to force looping) +``` + +### Play segmented subsong ranges as one +**`#(number)~(number)` or `#s(number)~(number)`**: set multiple subsong segments at a time, to avoid so much C&P + +**Prince of Persia Sands of Time**: *song_01.txtp* +``` +amb_fx.sb0#254 +amb_fx.sb0#122~144 +amb_fx.sb0#121 + +#3rd segment = subsong 123, not 3rd subsong +loop_start_segment = 3 +``` +This is just a shorthand, so `song#1~3#h22050` is equivalent to: +``` +song#1#h22050 +song#2#h22050 +song#3#h22050 +``` + + +### Channel mask for channel subsongs/layers +**`#c(number)`** (single) or **`#c(number)~(number)`** (range): set number of channels to play. You can add multiple comma-separated numbers, or use ` ` space or `-` as separator and combine multiple ranges with single channels too. + +**Final Fantasy XIII-2**: *music_Home_01.ps3.txtp* +``` +#plays channels 1 and 2 = 1st subsong +music_Home.ps3.scd#c1,2 +``` + +``` +#plays channels 3 and 4 = 2nd subsong +music_Home.ps3.scd#c3 4 + +#plays 1 to 3 +music_Home.ps3.scd#c1~3 +``` +Doesn't change the final number of channels though, just mutes non-selected channels. + +If you use **`C(number)`** it will remove non-selected channels (not done directly for backwards compatibility). This just a shortcut for macro `#@track` (described later): +``` +#plays channels 3 and 4 = 2nd subsong and removes other channels +music_Home.ps3.scd#C3 4 +``` + + +### Play settings +**`#l(loops)`**, **`#f(fade)`**, **`#d(fade-delay)`**, **`#i(ignore loop)`**, **`#F(ignore fade)`**, **`#E(end-to-end loop)`** + +Those setting should override player's defaults if set (except "loop forever"). They are equivalent to some test.exe options. + +**God Hand (PS2)**: *boss2_3ningumi_ver6.txtp* (each line is a separate TXTP) +``` +# set number of loops +boss2_3ningumi_ver6.adx#l3 + +# set fade time (in seconds) +boss2_3ningumi_ver6.adx#f10.5 + +# set fade delay (in seconds) +boss2_3ningumi_ver6.adx#d0.5 + +# ignore and disable loops +boss2_3ningumi_ver6.adx#i + +# don't fade out and instead play the song ending after +boss2_3ningumi_ver6.adx#F # this song has a nice stop + +# force full loops from end-to-end +boss2_3ningumi_ver6.adx#E + +# settings can be combined +boss2_3ningumi_ver6.adx#l2#F # 2 loops + ending + +# settings can be combined +boss2_3ningumi_ver6.adx#l1.5#d1#f5 + +# boss2_3ningumi_ver6.adx#l1.0#F # this is equivalent to #i +``` + + +### Time modifications +**`#t(time)`**: trims the file so base duration (before applying loops/fades/etc) is `(time)`. If value is negative substracts `(time)` to duration. Loop end is adjusted when necessary, and ignored if value is bigger than possible (use `#l(loops)` config to extend time instead). + +Time values can be `M:S(.n)` (minutes and seconds), `S.n` (seconds with dot), `0xN` (samples in hex format) or `N` (samples). Beware of the subtle difference between 10.0 (ten seconds) and 10 (ten samples). + +Some segments have padding/silence at the end for some reason, that don't allow smooth transitions. You can fix it like this: +``` +intro.fsb #t -1.0 #intro segment has 1 second of silence. +main.fsb +``` + +Similarly other games don't use loop points, but rather repeat/loops the song internally many times: +``` +intro.vag #t3:20 #i #l1.0 #trim + combine with forced loops for easy fades +``` + +Note that if you need to remove very few samples (like 1) to get smooth transitions it may be a bug in vgmstream, consider reporting. + + +### Force sample rate +**`#h(sample rate)`**: changes sample rate to selected value (within some limits). + +Needed for a few games set a sample rate value in the header but actually play with other (applying some of pitch or just forcing it). + +**Super Paper Mario (Wii)** +``` +btl_koopa1_44k_lp.brstm#h22050 #in hz +``` +**Patapon (PSP)** +``` +ptp_btl_bgm_voice.sgd#s1#h11050 +``` + + +### Install loops +**`#I(loop start time) (loop end time)`**: force/override looping values, same as .pos but nicer. Loop end is optional and defaults to total samples. + +Time values can be `M:S(.n)` (minutes and seconds), `S.n` (seconds with dot), `0xN` (samples in hex format) or `N` (samples). Beware of the subtle difference between 10.0 (ten seconds) and 10 (ten samples). Wrong loop values (for example loop end being much larger than file's samples) will be ignored, but there is some leeway when using seconds for the loop end. + +**Jewels Ocean (PC)** +``` +bgm01.ogg#I32.231 # from ~1421387 samples to end + +#bgm01.ogg#I 0:32.231 # equivalent +#bgm01.ogg#I 1421387 4212984 # equivalent, end is 4212984 +#bgm01.ogg#I32.231 1_35.533 # equivalent, end over file samples (~4213005) but adjusted to 4212984 +#bgm01.ogg#I 1421387 4212985 # ignored, end over file samples +#bgm01.ogg#I32.231 1_37 # ignored, end over file (~4277700) but clearly wrong +``` + +Use this feature responsibly, though. If you find a format that should loop using internal values that vgmstream doesn't detect correctly, consider reporting the bug for the benefit of all users and other games using the same format, and don't throw out the original loop definitions (as sometimes they may not take into account "encoder delay" and must be carefully adjusted). + +Note that a few codecs may not work with arbitrary loop values since they weren't tested with loops. Misaligned loops will cause audible "clicks" at loop point too. + + +### Channel mixing +**`#m(op),(...),(op)`**: mix channels in various ways by specifying multiple comma-separated sub-commands: + +Possible operations: +- `N-M`: swaps M with N +- `N+M*(volume)`: mixes M * volume to N +- `N+M`: mixes M to N +- `N*(volume)`: changes volume of N +- `N=(volume)`: limits volume of N +- `Nu`: upmix (insert) N ('pushing' all following channels forward) +- `Nd`: downmix (remove) N ('pulling' all following channels backward) +- `ND`: downmix (remove) N and all following channels +- `N(type)(position)(time-start)+(time-length)`: defines a fade + * `type` can be `{` = fade-in, `}` = fade-out, `(` = crossfade-in, `)` = crossfade-out + * crossfades are better tuned to use when changing between tracks + * `(position)` pre-adjusts `(time-start)` to start after certain time (optional) + * using multiple fades in the same channel will cancel previous fades + * may only cancel when fade is after previous one + * `}` then `{` or `{` then `}` makes sense, but `}` then `}` will make funny volume bumps + * example: `1{0:10+0:5, 1}0:30+0:5` fades-in at 10 seconds, then fades-out at 30 seconds +- `N^(volume-start)~(volume-end)=(shape)@(time-pre)~(time-start)+(time-length)~(time-last)`: defines full fade envelope + * full definition of a fade envelope to allow precise volume changes over time + * not necessarily fades, as you could set length 0 for volume "bumps" like `1.0~0.5` + * `(shape)` can be `{` = fade, `(` = crossfade, other values are reserved for internal testing and may change anytime + * `(time-start)`+`(time-length)` make `(time-end)` + * between `(time-pre)` and `(time-start)` song uses `(volume-start)` + * between `(time-start)` and `(time-end)` song gradually changes `(volume-start)` to `(volume-end)` (depending on `(shape)`) + * between `(time-end)` and `(time-post)` song uses `(volume-end)` + * `time-pre/post` may be -1 to set "file start" and "file end", cancelled by next fade + +Considering: +- `N` and `M` are channels (*current* value after previous operators are applied) +- channel 1 is first +- channel 0 is shorthand for all channels where applicable (`0*V`, `0=V`, `0^...`) +- may use `x` instead of `*` and `_` instead of `:` (for mini-TXTP) +- `(volume)` is a `N.N` decimal value where 1.0 is 100% base volume + - negative volume inverts the waveform (for weird effects) +- `(position)` can be `N.NL` or `NL` = N.N loops + - if loop start is 1000 and loop end 5000, `0.0L` = 1000 samples, `1.0L` = 5000 samples, `2.0L` = 9000 samples, etc +- `(time)` can be `N:NN(.n)` (minutes:seconds), `N.N` (seconds) or `N` (samples) + - represents the file's global play time, so it may be set after N loops + - beware of `10.0` (ten seconds) vs `10` (ten samples) + - may not work with huge numbers (like several hours) +- adding trailing channels must be done 1 by 1 at the end (for stereo: `3u,4u,(...)` +- nonsensical values are ignored (like referencing channel 3 in a stereo file) + +Main usage would be creating stereo files for games that layer channels. +``` +# quad to stereo: all layers must play at the same time +# - mix 75% of channel 3/4 into channel 1/2, then drop channel 3 and 4 +song#m1+3*0.75,2+4*.75,3D + +# quad to stereo: only channel 3 and 4 should play +# - swap channel 1/2 with 3/4, then drop channel 3/4 +song#m1-3,2-4,3D + +# also equivalent, but notice the order +# - drop channel 1 then 2 (now 1) +song#m1d,1d +``` +Proper mixing requires some basic knowledge though, it's further explained later. Order matters and operations are applied sequentially, for extra flexibility at the cost of complexity and user-friendliness, and may result in surprising mixes. Try to stick to macros and simple combos, using later examples as a base. + +This can be applied to individual layers and segments, but normally you want to use `commands` to apply mixing to the resulting file (see examples). Per-segment mixing should be reserved to specific up/downmixings. + +Mixing must be supported by the plugin, otherwise it's ignored (there is a negligible performance penalty per mix operation though). + + +### Macros +**`#@(macro name and parameters)`**: adds a new macro + +Manually setting values gets old, so TXTP supports a bunch of simple macros. They automate some of the above commands (analyzing the file), and may be combined, so order still matters. +- `volume N (channels)`: sets volume V to selected channels +- `track (channels)`: makes a file of selected channels +- `layer-v N (channels)`: mixes selected channels to N channels with default volume (for layered vocals) +- `layer-b N (channels)`: same, but adjusts volume depending on layers (for layered bgm) +- `layer-e N (channels)`: same, but adjusts volume equally for all layers (for generic downmixing) +- `remix N (channels)`: same, but mixes selected channels to N channels properly adjusting volume (for layered bgm) +- `crosstrack N`: crossfades between Nch tracks after every loop (loop count is adjusted as needed) +- `crosslayer-v/b/e N`: crossfades Nch layers to the main track after every loop (loop count is adjusted as needed) +- `downmix`: downmixes up to 8 channels (7.1, 5.1, etc) to stereo, using standard downmixing formulas. + +`channels` can be multiple comma-separated channels or N~M ranges and may be ommited were applicable to mean "all channels" (channel order doesn't matter but it's internally fixed). + +Examples: +``` +# plays 2ch layer1 (base melody) +okami-ryoshima_coast.aix#@track 1,2 + +# plays 2ch layer1+2 (base melody+percussion) +okami-ryoshima_coast.aix#@layer-b 2 1~4 #1~4 may be skipped + +# uses 2ch layer1 (base melody) in the first loop, adds 2ch layer2 (percussion) to layer1 in the second +okami-ryoshima_coast.aix#@crosslayer-b 2 + +# uses 2ch track1 (exploration) in the first loop, changes to 2ch track2 (combat) in the second +ffxiii2-eclipse.scd#@crosstrack 2 + +# plays 2ch from 4ch track1 (sneaking) +mgs4-bgm_ee_alert_01.mta2#@layer-e 2 1~4 + +# downmix bgm + vocals to stereo +nier_automata-BGM_0_012_04.wem +nier_automata-BGM_0_012_07.wem +mode = layers +commands = #@layer-v 2 + +# can be combined with normal mixes too for creative results +# (add channel clone of ch1, then 50% of range) +song#m4u,4+1#@volume 0.5 2~4 + +# equivalent to #@layer-e 2 1~4 +mgs4-bgm_ee_alert_01.mta2#@track 1~4#@layer-b 2 + +# equivalent to #@track 1,2 +okami-ryoshima_coast.aix#@layer-b 2 1,2 +``` + + +## OTHER FEATURES + +### Default commands +You can set defaults that apply to the *resulting* file. This has subtle differences vs per-file config: +``` +BGM01_BEGIN.VAG +BGM01_LOOPED.VAG + +# force looping from begin to end of the whole thing +commands = #E +``` +``` +# mix 2ch * 2 +BGM_0_012_04.wem +BGM_0_012_07.wem +mode = layers + +# plays R of BGM_0_012_04 and L of BGM_0_012_07 +commands = #c2,3 +``` + +As it applies at the end, some options with ambiguous or technically hard to handle meanings may be ignored: +``` +bgm.sxd2 +bgm.sxd2 + +# ignored (resulting file has no subsongs, or should apply to all?) +commands = #s12 +``` + +### Force plugin extensions +vgmstream supports a few common extensions that confuse plugins, like .wav/ogg/aac/opus/etc, so for them those extensions are disabled and are expected to be renamed to .lwav/logg/laac/lopus/etc. TXTP can make plugins play those disabled extensions, since it calls files directly by filename. + +Combined with TXTH, this can also be used for extensions that aren't normally accepted by vgmstream. + + +### TXTP combos +TXTP may even reference other TXTP, or files that require TXTH, for extra complex cases. Each file defined in TXTP is internally parsed like it was a completely separate file, so there is a bunch of valid ways to mix them. + + +### TXTP parsing +*Filenames* may be anything accepted by the file system, including spaces and symbols, and multiple *commands* can be chained: +``` +bgm bank#s2#c1,2 +``` + +You may add spaces as needed (but try to keep it simple and don't go overboard), though commands *must* start with `#(command)` (`#(space)(anything)` is a comment). Commands without corresponding file are ignored too (seen as comments), while incorrect commands are ignored and skip to next, though the parser may try to make something usable of them (this may be change anytime without warning): +``` +# those are all equivalent +song#s2#c1,2 +song #s2#c1,2 # comment +song #s 2 #c1,2# comment +song #s 2 #c 1 , 2# comment + +#s2 #ignores rogue commands/comments + +# seen as incorrect and ignored +song #s TWO +song #E enable +song #E 1 +song #Enable +song #h -48000 + +# accepted +song #E # comment +song #c1, 2, 3 +song #c 1 2 3 + +# ignores first and reads second +song #s TWO#c1,2 + +# seen as #s1#c1,2 +song #s 1,2 #c1,2 + +# all seen as #h48000 +song #h48000 +song #h 48000hz +song #h 48000mhz + +# ignored +song #h hz48000 + +# ignored as channels don't go that high (may be modified on request) +song #c32,41 + +# swaps 1 with 2 +song #m1-2 +song #m 1 - 2 + +# swaps 1 with "-2", ignored +song #m1 -2 +``` + +*Values* found after *=* allow spaces as well: +``` +song#s2 +loop_start_segment = 1 #s2# #commands here are ignored + +song +commands=#s2 # commands here are allowed +commands= #c1,2 +``` + +Repeated commands overwrite previous setting, except comma-separated commands that are additive: +``` +# overwrites, equivalent to #s2 +song#s1#s2 +``` +``` +# adds, equivalent to #m1-2,3-4,5-6 +song#m1-2#m3-4 +commands = #m5-6 +# also added to song +commands = #l 3.0 +``` + +The parser is fairly simplistic and lax, and may be erratic with edge cases or behave unexpectedly due to unforeseen use-cases and bugs. As filenames may contain spaces or #, certain name patterns could fool it too. Keep in mind this while making .txtp files. + + +## MINI-TXTP +To simplify TXTP creation, if the .txtp doesn't set a name inside then its filename is used directly, including config. Note that extension must be included (since vgmstream needs a full filename). You can set `commands` inside the .txtp too: +- *bgm.sxd2#12.txtp*: plays subsong 12 +- *bgm.sxd2#12.txtp*, , inside has `commands = #@volume 0.5`: plays subsong 12 at half volume +- *bgm.sxd2.txtp*, , inside has `commands = #12 #@volume 0.5`: plays subsong 12 at half volume +- *Ryoshima Coast 1 & 2.aix#C1,2.txtp*: channel downmix +- *boss2_3ningumi_ver6.adx#l2#F.txtp*: loop twice then play song end file normally +- etc + + +## MIXING +Sometimes games use multiple channels in uncommon ways, for example as layered tracks for dynamic music (like main+vocals), or crossfading a stereo song to another stereo song. In those cases we normally would want a stereo track, but vgmstream can't guess how channels are used (since it's game-dependant). To solve this via TXTP you can set mixing output and volumes manually. + +A song file is just data that can contain a (sometimes unlimited) number of channels, that must play in physical speakers. Standard audio formats define how to "map" known channels to speakers: +- `1.0: FC` +- `2.0: FL, FR` +- `2.1: FL, FR, LF` +- `4.0: FL, FR, SL, SR` +- `5.1: FL, FR, FC, LF, SL, SR` +- ... (channels in order, where FL=front left, FC=front center, etc) + +If you only have stereo speakers, when playing a 5.1 file your player may silently transform to stereo, as otherwise you would miss some channels. But a game song's channels can be various things: standard mapped, per-format map, per-game, multilayers combined ("downmixed") to a final stereo file, music then all language tracks, etc. So you need to decide which channels drop or combine and their individual volumes via mixing. + +Say you want to mix 4ch to 2ch (ch3 to ch1, ch4 to ch2). Due to how audio signals work, mixing just combines (adds) sounds. So if channels 1/2 are LOUD, and channels 3/4 are LOUD, you get a LOUDER channel 1/2. To fix this we set mixing volume, for example: `mix channel 3/4 * 0.707 (-3db/30% lower volume) to channel 1/2`: the resulting stereo file is now more listenable. Those volumes are just for standard audio and may work ok for every game though. + +All this means there is no simple, standard way to mix, so you must experiment a bit. + + +### MIXING EXAMPLES +For most common usages you can stick with macros but actual mixing is quite flexible: +``` +# boost volume of all channels by 30% +song#m0*1.3 + +# boost but limit volume (highs don't go too high, while lows sound louder) +song#m0*1.3,0=0.9 + +# downmix 4ch layers to stereo (this may sound too loud) +song#m1+3,2+4,3D + +# downmix 4ch layers to stereo with adjusted volume for latter channels (common 4.0 to 2.0 mixdown) +song#m1+3*0.7,2+4*0.7,3D + +# downmix 4ch layers to stereo with equal adjusted volume (common layer mixdown) +song#m0*0.7,1*0.7,1+3*0.7,2+4*0.7,3D + +# downmix stereo to mono (ignored if file is 1ch) +zelda-cdi.xa#m1d + +# upmix mono to stereo by copying ch1 to ch2 +zelda-cdi.xa#m2*0.0,2+1 + +# downmix 5.1 wav (FL FR FC LFE BL BR) to stereo +# (uses common -3db mixing formula: Fx=Fx + FC*0.707 + Rx*0.707) +song#m1+3*0.707,2+3*0.707,1+5*0.707,2+6*0.707,3D + +# mask sfx track ch3 in a 6ch file +song#m3*0.0 + +# add a fake silent channel to a 5ch file (FL FR FC BL BR) and move it to LFE position +# "make ch6, swap BL with LFE (now FL FR FC LFE BR BL), swap BR with BL (now FL FR FC LFE BL BR) +sf5.hca#m6u,4-6,5-6 + +# mix 50% of channel 3 into 1 and 2, drop 3 +song#m1+3*0.5,2+3*0.5,3d + +# swap ch1 and 2 then change volume to the resulting swapped channel +song#m1-2,2*0.5 + +# fade-in ch3+4 percussion track layer into main track, downmix to stereo +# (may be split in multiple lines, no difference) +okami-ryoshima_coast.aix#l2 +commands = #m3(1:10+0:05 # loop happens after ~1:10 +commands = #m4(1:10+0:05 # ch3/4 are percussion tracks +commands = #m1+3*0.707,2+4*0.707 # ch3/4 always mixed but silent until 1:10 +commands = #m3D # remove channels after all mixing + +# same but fade-out percussion after second loop +okami-ryoshima_coast.aix#l3 +commands = #m3(1:10+0:05,3)2:20+0:05 +commands = #m4(1:10+0:05,4)2:20+0:05 +commands = #m1+3*0.707,2+4*0.707,3D + +# crossfade exploration and combat sections after loop +ffxiii-2~eclipse.aix +commands = #m1)1:50+0:10,2)1:50+0:10 +commands = #m3(1:50+0:10,4(1:50+0:10 +commands = #m1+3,2+4,3D # won't play at the same time, no volume needed + +# ghetto voice removal (invert channel + other channel removes duplicated parts, and vocals are often layered) +song +commands = #m3u,3+1*-1.0,4u,4+2*-1.0 +commands = #m1+4,2+3,3d,3d + +# crosstrack 4ch file 3 times, going back to first track by creating a fake 3rd track with ch1 and 2: +ffxiii2-eclipse.scd#m5u,6u,5+1,6+2#@crosstrack 2 +``` + +Segment/layer downmixing is allowed but try to keep it simple, some mixes accomplish the same things but are a bit strange. +``` +# mix one stereo segment with a mono segment +intro-stereo.hca +loop-mono.hca#m2u + +# this makes mono file +intro-stereo.hca#m2u +loop-stereo.hca#m2u + +# but you normally should do this instead as it's more natural +intro-stereo.hca +loop-stereo.hca +commands = #m2u + +# fading segments work rather unexpectedly +# fades out 1 minute into the _segment_ (could be 2 minutes into the resulting file) +segment1.hca#m0{0:10+10.0 +segment2.hca#m0}1:00+10.0 +# better use: commands = #m0{0:10+10.0,0}2:00+10.0 +# it would work ok it they were layers, but still, better to use commands with the resulting file +``` + +Combine with groups or extra complex cases: +``` +BGM_SUMMON_0001_02-Intro.hca # 2ch file +BGM_SUMMON_0001_02-Intro2.hca # 2ch file + +BGM_SUMMON_0001_02.hca +BGM_SUMMON_0001_02-Vocal.hca +group = 3L2 #@layer-v 2 # layer Main+Vocal as 4ch then downmix to 2ch + +loop_start_segment = 3 #refers to new group at position 3 +loop_mode = keep +``` + +Note how order subtly affects end results: +``` +# after silencing channel 1 mixing is meaningless +song#m1*0.0,2+1 + +# allowed but useless or ignored +song#m1u,1d,1-1,1*1.0,11d,7D + +# this creates a new ch1 with 50% of ch2 (actually old ch1), total 3ch +song#m1u,1+2*0.5 + +# so does this +song#m3u,3+1*0.5,1-3,2-3 + +# this may not be what you want +# (result is a silent ch1, and ch2 with 50% of ch3) +song#m1+2*0.5,1u + +# for a 2ch file 2nd command is ignored, since ch2 is removed after 1st command +song#m1d,2+1*0.5 +``` diff --git a/src/meta/txtp.c b/src/meta/txtp.c index c9102e93..5cf9c8af 100644 --- a/src/meta/txtp.c +++ b/src/meta/txtp.c @@ -1,1501 +1,1501 @@ -#include "meta.h" -#include "../coding/coding.h" -#include "../layout/layout.h" -#include "../mixing.h" - - -#define TXTP_LINE_MAX 1024 -#define TXTP_MIXING_MAX 512 -#define TXTP_GROUP_MODE_SEGMENTED 'S' -#define TXTP_GROUP_MODE_LAYERED 'L' -#define TXTP_GROUP_REPEAT 'R' -#define TXTP_POSITION_LOOPS 'L' - -/* mixing info */ -typedef enum { - MIX_SWAP, - MIX_ADD, - MIX_ADD_VOLUME, - MIX_VOLUME, - MIX_LIMIT, - MIX_DOWNMIX, - MIX_KILLMIX, - MIX_UPMIX, - MIX_FADE, - - MACRO_VOLUME, - MACRO_TRACK, - MACRO_LAYER, - MACRO_CROSSTRACK, - MACRO_CROSSLAYER, - MACRO_DOWNMIX, - -} txtp_mix_t; - -typedef struct { - txtp_mix_t command; - /* common */ - int ch_dst; - int ch_src; - double vol; - - /* fade envelope */ - double vol_start; - double vol_end; - char shape; - int32_t sample_pre; - int32_t sample_start; - int32_t sample_end; - int32_t sample_post; - double time_pre; - double time_start; - double time_end; - double time_post; - double position; - char position_type; - - /* macros */ - int max; - uint32_t mask; - char mode; -} txtp_mix_data; - - -typedef struct { - char filename[TXTP_LINE_MAX]; - - int range_start; - int range_end; - int subsong; - - uint32_t channel_mask; - int mixing_count; - txtp_mix_data mixing[TXTP_MIXING_MAX]; - - int config_loop_count_set; - double config_loop_count; - int config_fade_time_set; - double config_fade_time; - int config_fade_delay_set; - double config_fade_delay; - int config_ignore_loop; - int config_force_loop; - int config_ignore_fade; - - int sample_rate; - - int loop_install_set; - int loop_end_max; - double loop_start_second; - int32_t loop_start_sample; - double loop_end_second; - int32_t loop_end_sample; - - int trim_set; - double trim_second; - int32_t trim_sample; - -} txtp_entry; - - -typedef struct { - int position; - char type; - int count; - char repeat; - - txtp_entry group_config; - -} txtp_group; - -typedef struct { - txtp_entry *entry; - size_t entry_count; - size_t entry_max; - - txtp_group *group; - size_t group_count; - size_t group_max; - - VGMSTREAM* *vgmstream; - size_t vgmstream_count; - - uint32_t loop_start_segment; - uint32_t loop_end_segment; - int is_loop_keep; - - txtp_entry default_entry; - int default_entry_set; - - int is_segmented; - int is_layered; - int is_single; -} txtp_header; - -static txtp_header* parse_txtp(STREAMFILE* streamFile); -static void clean_txtp(txtp_header* txtp, int fail); -static void apply_config(VGMSTREAM *vgmstream, txtp_entry *current); -void add_mixing(txtp_entry* cfg, txtp_mix_data* mix, txtp_mix_t command); - -static int make_group_segment(txtp_header* txtp, int from, int count); -static int make_group_layer(txtp_header* txtp, int from, int count); - - -/* TXTP - an artificial playlist-like format to play files with segments/layers/config */ -VGMSTREAM * init_vgmstream_txtp(STREAMFILE *streamFile) { - VGMSTREAM *vgmstream = NULL; - txtp_header* txtp = NULL; - int i; - - - /* checks */ - if (!check_extensions(streamFile, "txtp")) - goto fail; - - /* read .txtp with all files and config */ - txtp = parse_txtp(streamFile); - if (!txtp) goto fail; - - /* post-process */ - { - if (txtp->entry_count == 0) - goto fail; - - txtp->vgmstream = calloc(txtp->entry_count, sizeof(VGMSTREAM*)); - if (!txtp->vgmstream) goto fail; - - txtp->vgmstream_count = txtp->entry_count; - } - - - /* detect single files before grouping */ - if (txtp->group_count == 0 && txtp->vgmstream_count == 1) { - txtp->is_single = 1; - txtp->is_segmented = 0; - txtp->is_layered = 0; - } - - - /* open all entry files first as they'll be modified by modes */ - for (i = 0; i < txtp->vgmstream_count; i++) { - STREAMFILE* temp_streamFile = open_streamfile_by_filename(streamFile, txtp->entry[i].filename); - if (!temp_streamFile) { - VGM_LOG("TXTP: cannot open streamfile for %s\n", txtp->entry[i].filename); - goto fail; - } - temp_streamFile->stream_index = txtp->entry[i].subsong; - - txtp->vgmstream[i] = init_vgmstream_from_STREAMFILE(temp_streamFile); - close_streamfile(temp_streamFile); - if (!txtp->vgmstream[i]) { - VGM_LOG("TXTP: cannot open vgmstream for %s\n", txtp->entry[i].filename); - goto fail; - } - - apply_config(txtp->vgmstream[i], &txtp->entry[i]); - } - - - /* group files as needed */ - for (i = 0; i < txtp->group_count; i++) { - txtp_group *grp = &txtp->group[i]; - int pos, groups; - - //;VGM_LOG("TXTP: apply group %i%c%i%c\n",txtp->group[i].position,txtp->group[i].type,txtp->group[i].count,txtp->group[i].repeat); - - /* special meaning of "all files" */ - if (grp->position < 0 || grp->position >= txtp->vgmstream_count) - grp->position = 0; - if (grp->count <= 0) - grp->count = txtp->vgmstream_count - grp->position; - - /* repeats N groups (trailing files are not grouped) */ - if (grp->repeat == TXTP_GROUP_REPEAT) { - groups = ((txtp->vgmstream_count - grp->position) / grp->count); - } - else { - groups = 1; - } - - /* as groups are compacted position goes 1 by 1 */ - for (pos = grp->position; pos < grp->position + groups; pos++) { - //;VGM_LOG("TXTP: group=%i, count=%i, groups=%i\n", pos, grp->count, groups); - switch(grp->type) { - case TXTP_GROUP_MODE_LAYERED: - if (!make_group_layer(txtp, pos, grp->count)) - goto fail; - break; - case TXTP_GROUP_MODE_SEGMENTED: - if (!make_group_segment(txtp, pos, grp->count)) - goto fail; - break; - default: - goto fail; - } - } - - /* group may also have config (like downmixing) */ - apply_config(txtp->vgmstream[grp->position], &grp->group_config); - } - - /* final tweaks (should be integrated with the above?) */ - if (txtp->is_layered) { - if (!make_group_layer(txtp, 0, txtp->vgmstream_count)) - goto fail; - } - if (txtp->is_segmented) { - if (!make_group_segment(txtp, 0, txtp->vgmstream_count)) - goto fail; - } - if (txtp->is_single) { - /* special case of setting start_segment to force/overwrite looping - * (better to use #E but left for compatibility with older TXTPs) */ - if (txtp->loop_start_segment == 1 && !txtp->loop_end_segment) { - vgmstream_force_loop(txtp->vgmstream[0], 1, txtp->vgmstream[0]->loop_start_sample, txtp->vgmstream[0]->num_samples); - } - } - - - /* may happen if using mixed mode but some files weren't grouped */ - if (txtp->vgmstream_count != 1) { - VGM_LOG("TXTP: wrong final vgmstream count %i\n", txtp->vgmstream_count); - goto fail; - } - - /* apply default config to the resulting file */ - if (txtp->default_entry_set) { - apply_config(txtp->vgmstream[0], &txtp->default_entry); - } - - - vgmstream = txtp->vgmstream[0]; - - clean_txtp(txtp, 0); - return vgmstream; - -fail: - clean_txtp(txtp, 1); - return NULL; -} - -static void update_vgmstream_list(VGMSTREAM* vgmstream, txtp_header* txtp, int position, int count) { - int i; - - //;VGM_LOG("TXTP: compact position=%i count=%i, vgmstreams=%i\n", position, count, txtp->vgmstream_count); - - /* sets and compacts vgmstream list pulling back all following entries */ - txtp->vgmstream[position] = vgmstream; - for (i = position + count; i < txtp->vgmstream_count; i++) { - //;VGM_LOG("TXTP: copy %i to %i\n", i, i + 1 - count); - txtp->vgmstream[i + 1 - count] = txtp->vgmstream[i]; - } - - /* list can only become smaller, no need to alloc/free/etc */ - txtp->vgmstream_count = txtp->vgmstream_count + 1 - count; - //;VGM_LOG("TXTP: compact vgmstreams=%i\n", txtp->vgmstream_count); -} - -static int make_group_segment(txtp_header* txtp, int position, int count) { - VGMSTREAM * vgmstream = NULL; - segmented_layout_data *data_s = NULL; - int i, loop_flag = 0; - - - if (count == 1) { /* nothing to do */ - //;VGM_LOG("TXTP: ignored segments of 1\n"); - return 1; - } - - if (position + count > txtp->vgmstream_count || position < 0 || count < 0) { - VGM_LOG("TXTP: ignored segment position=%i, count=%i, entries=%i\n", position, count, txtp->vgmstream_count); - return 1; - } - - /* loop settings only make sense if this group becomes final vgmstream */ - if (position == 0 && txtp->vgmstream_count == count) { - if (txtp->loop_start_segment && !txtp->loop_end_segment) - txtp->loop_end_segment = count; - loop_flag = (txtp->loop_start_segment > 0 && txtp->loop_start_segment <= count); - } - - - /* init layout */ - data_s = init_layout_segmented(count); - if (!data_s) goto fail; - - /* copy each subfile */ - for (i = 0; i < count; i++) { - data_s->segments[i] = txtp->vgmstream[i + position]; - txtp->vgmstream[i + position] = NULL; /* will be freed by layout */ - } - - /* setup VGMSTREAMs */ - if (!setup_layout_segmented(data_s)) - goto fail; - - /* build the layout VGMSTREAM */ - vgmstream = allocate_segmented_vgmstream(data_s,loop_flag, txtp->loop_start_segment - 1, txtp->loop_end_segment - 1); - if (!vgmstream) goto fail; - - /* custom meta name if all parts don't match */ - for (i = 0; i < data_s->segment_count; i++) { - if (vgmstream->meta_type != data_s->segments[i]->meta_type) { - vgmstream->meta_type = meta_TXTP; - break; - } - } - - /* fix loop keep */ - if (loop_flag && txtp->is_loop_keep) { - int32_t current_samples = 0; - for (i = 0; i < data_s->segment_count; i++) { - if (txtp->loop_start_segment == i+1 /*&& data_s->segments[i]->loop_start_sample*/) { - vgmstream->loop_start_sample = current_samples + data_s->segments[i]->loop_start_sample; - } - - current_samples += data_s->segments[i]->num_samples; - - if (txtp->loop_end_segment == i+1 && data_s->segments[i]->loop_end_sample) { - vgmstream->loop_end_sample = current_samples - data_s->segments[i]->num_samples + data_s->segments[i]->loop_end_sample; - } - } - } - - - /* set new vgmstream and reorder positions */ - update_vgmstream_list(vgmstream, txtp, position, count); - - return 1; -fail: - close_vgmstream(vgmstream); - if (!vgmstream) - free_layout_segmented(data_s); - return 0; -} - -static int make_group_layer(txtp_header* txtp, int position, int count) { - VGMSTREAM * vgmstream = NULL; - layered_layout_data * data_l = NULL; - int i; - - - if (count == 1) { /* nothing to do */ - //;VGM_LOG("TXTP: ignored layer of 1\n"); - return 1; - } - - if (position + count > txtp->vgmstream_count || position < 0 || count < 0) { - VGM_LOG("TXTP: ignored layer position=%i, count=%i, entries=%i\n", position, count, txtp->vgmstream_count); - return 1; - } - - - /* init layout */ - data_l = init_layout_layered(count); - if (!data_l) goto fail; - - /* copy each subfile */ - for (i = 0; i < count; i++) { - data_l->layers[i] = txtp->vgmstream[i + position]; - txtp->vgmstream[i + position] = NULL; /* will be freed by layout */ - } - - /* setup VGMSTREAMs */ - if (!setup_layout_layered(data_l)) - goto fail; - - /* build the layout VGMSTREAM */ - vgmstream = allocate_layered_vgmstream(data_l); - if (!vgmstream) goto fail; - - /* custom meta name if all parts don't match */ - for (i = 0; i < count; i++) { - if (vgmstream->meta_type != data_l->layers[i]->meta_type) { - vgmstream->meta_type = meta_TXTP; - break; - } - } - - - /* set new vgmstream and reorder positions */ - update_vgmstream_list(vgmstream, txtp, position, count); - - return 1; -fail: - close_vgmstream(vgmstream); - if (!vgmstream) - free_layout_layered(data_l); - return 0; -} - - -static void apply_config(VGMSTREAM *vgmstream, txtp_entry *current) { - - if (current->config_loop_count_set) - vgmstream->config_loop_count = current->config_loop_count; - if (current->config_fade_time_set) - vgmstream->config_fade_time = current->config_fade_time; - if (current->config_fade_delay_set) - vgmstream->config_fade_delay = current->config_fade_delay; - if (current->config_ignore_loop) - vgmstream->config_ignore_loop = current->config_ignore_loop; - if (current->config_force_loop) - vgmstream->config_force_loop = current->config_force_loop; - if (current->config_ignore_fade) - vgmstream->config_ignore_fade = current->config_ignore_fade; - - if (current->sample_rate > 0) - vgmstream->sample_rate = current->sample_rate; - - if (current->loop_install_set) { - if (current->loop_start_second > 0 || current->loop_end_second > 0) { - current->loop_start_sample = current->loop_start_second * vgmstream->sample_rate; - current->loop_end_sample = current->loop_end_second * vgmstream->sample_rate; - if (current->loop_end_sample > vgmstream->num_samples && - current->loop_end_sample - vgmstream->num_samples <= 0.1 * vgmstream->sample_rate) - current->loop_end_sample = vgmstream->num_samples; /* allow some rounding leeway */ - } - - if (current->loop_end_max) { - current->loop_end_sample = vgmstream->num_samples; - } - - vgmstream_force_loop(vgmstream, current->loop_install_set, current->loop_start_sample, current->loop_end_sample); - } - - if (current->trim_set) { - if (current->trim_second != 0.0) { - current->trim_sample = current->trim_second * vgmstream->sample_rate; - } - - if (current->trim_sample < 0) { - vgmstream->num_samples += current->trim_sample; /* trim from end (add negative) */ - } - else if (vgmstream->num_samples > current->trim_sample) { - vgmstream->num_samples = current->trim_sample; /* trim to value */ - } - - /* readjust after triming if it went over (could check for more edge cases but eh) */ - if (vgmstream->loop_end_sample > vgmstream->num_samples) - vgmstream->loop_end_sample = vgmstream->num_samples; - } - - - /* add macro to mixing list */ - if (current->channel_mask) { - int ch; - for (ch = 0; ch < vgmstream->channels; ch++) { - if (!((current->channel_mask >> ch) & 1)) { - txtp_mix_data mix = {0}; - mix.ch_dst = ch + 1; - mix.vol = 0.0f; - add_mixing(current, &mix, MIX_VOLUME); - } - } - } - - /* copy mixing list (should be done last as some mixes depend on config) */ - if (current->mixing_count > 0) { - int m, position_samples; - - for (m = 0; m < current->mixing_count; m++) { - txtp_mix_data *mix = ¤t->mixing[m]; - - switch(mix->command) { - /* base mixes */ - case MIX_SWAP: mixing_push_swap(vgmstream, mix->ch_dst, mix->ch_src); break; - case MIX_ADD: mixing_push_add(vgmstream, mix->ch_dst, mix->ch_src, 1.0); break; - case MIX_ADD_VOLUME: mixing_push_add(vgmstream, mix->ch_dst, mix->ch_src, mix->vol); break; - case MIX_VOLUME: mixing_push_volume(vgmstream, mix->ch_dst, mix->vol); break; - case MIX_LIMIT: mixing_push_limit(vgmstream, mix->ch_dst, mix->vol); break; - case MIX_UPMIX: mixing_push_upmix(vgmstream, mix->ch_dst); break; - case MIX_DOWNMIX: mixing_push_downmix(vgmstream, mix->ch_dst); break; - case MIX_KILLMIX: mixing_push_killmix(vgmstream, mix->ch_dst); break; - case MIX_FADE: - /* Convert from time to samples now that sample rate is final. - * Samples and time values may be mixed though, so it's done for every - * value (if one is 0 the other will be too, though) */ - if (mix->time_pre > 0.0) mix->sample_pre = mix->time_pre * vgmstream->sample_rate; - if (mix->time_start > 0.0) mix->sample_start = mix->time_start * vgmstream->sample_rate; - if (mix->time_end > 0.0) mix->sample_end = mix->time_end * vgmstream->sample_rate; - if (mix->time_post > 0.0) mix->sample_post = mix->time_post * vgmstream->sample_rate; - /* convert special meaning too */ - if (mix->time_pre < 0.0) mix->sample_pre = -1; - if (mix->time_post < 0.0) mix->sample_post = -1; - - if (mix->position_type == TXTP_POSITION_LOOPS && vgmstream->loop_flag) { - int loop_pre = vgmstream->loop_start_sample; - int loop_samples = (vgmstream->loop_end_sample - vgmstream->loop_start_sample); - - position_samples = loop_pre + loop_samples * mix->position; - - if (mix->sample_pre >= 0) mix->sample_pre += position_samples; - mix->sample_start += position_samples; - mix->sample_end += position_samples; - if (mix->sample_post >= 0) mix->sample_post += position_samples; - } - - - mixing_push_fade(vgmstream, mix->ch_dst, mix->vol_start, mix->vol_end, mix->shape, - mix->sample_pre, mix->sample_start, mix->sample_end, mix->sample_post); - break; - - /* macro mixes */ - case MACRO_VOLUME: mixing_macro_volume(vgmstream, mix->vol, mix->mask); break; - case MACRO_TRACK: mixing_macro_track(vgmstream, mix->mask); break; - case MACRO_LAYER: mixing_macro_layer(vgmstream, mix->max, mix->mask, mix->mode); break; - case MACRO_CROSSTRACK: mixing_macro_crosstrack(vgmstream, mix->max); break; - case MACRO_CROSSLAYER: mixing_macro_crosslayer(vgmstream, mix->max, mix->mode); break; - case MACRO_DOWNMIX: mixing_macro_downmix(vgmstream, mix->max); break; - - default: - break; - } - } - } -} - -/* ********************************** */ - -static void clean_filename(char * filename) { - int i; - size_t len; - - if (filename[0] == '\0') - return; - - /* normalize paths */ - fix_dir_separators(filename); - - /* remove trailing spaces */ - len = strlen(filename); - for (i = len-1; i > 0; i--) { - if (filename[i] != ' ') - break; - filename[i] = '\0'; - } - -} - - -/* sscanf 101: "matches = sscanf(string-from, string-commands, parameters...)" - * - reads linearly and matches "%" commands to input parameters as found - * - reads until string end (NULL) or not being able to match current parameter - * - returns number of matched % parameters until stop, or -1 if no matches and reached string end - * - must supply pointer param for every "%" in the string - * - %d/f: match number until end or *non-number* (so "%d" reads "5t" as "5") - * - %s: reads string (dangerous due to overflows and surprising as %s%d can't match numbers since string eats all chars) - * - %[^(chars)] match string with chars not in the list (stop reading at those chars) - * - %*(command) read but don't match (no need to supply parameterr) - * - " ": ignore all spaces until next non-space - * - other chars in string must exist: ("%dt t%dt" reads "5t t5t" as "5" and "5", while "t5t 5t" matches only first "5") - * - %n: special match (not counted in return value), chars consumed until that point (can appear and be set multiple times) - */ - -static int get_double(const char * config, double *value, int *is_set) { - int n, m; - double temp; - - if (is_set) *is_set = 0; - - m = sscanf(config, " %lf%n", &temp,&n); - if (m != 1 || temp < 0) - return 0; - - if (is_set) *is_set = 1; - *value = temp; - return n; -} - -static int get_int(const char * config, int *value) { - int n,m; - int temp; - - m = sscanf(config, " %i%n", &temp,&n); - if (m != 1 || temp < 0) - return 0; - - *value = temp; - return n; -} - -static int get_position(const char * config, double *value_f, char *value_type) { - int n,m; - double temp_f; - char temp_c; - - /* test if format is position: N.n(type) */ - m = sscanf(config, " %lf%c%n", &temp_f,&temp_c,&n); - if (m != 2 || temp_f < 0.0) - return 0; - /* test accepted chars as it will capture anything */ - if (temp_c != TXTP_POSITION_LOOPS) - return 0; - - *value_f = temp_f; - *value_type = temp_c; - return n; -} - - -static int get_time(const char * config, double *value_f, int32_t *value_i) { - int n,m; - int temp_i1, temp_i2; - double temp_f1, temp_f2; - char temp_c; - - /* test if format is hour: N:N(.n) or N_N(.n) */ - m = sscanf(config, " %i%c%i%n", &temp_i1,&temp_c,&temp_i2,&n); - if (m == 3 && (temp_c == ':' || temp_c == '_')) { - m = sscanf(config, " %lf%c%lf%n", &temp_f1,&temp_c,&temp_f2,&n); - if (m != 3 || /*temp_f1 < 0.0 ||*/ temp_f1 >= 60.0 || temp_f2 < 0.0 || temp_f2 >= 60.0) - return 0; - - *value_f = temp_f1 * 60.0 + temp_f2; - return n; - } - - /* test if format is seconds: N.n */ - m = sscanf(config, " %i.%i%n", &temp_i1,&temp_i2,&n); - if (m == 2) { - m = sscanf(config, " %lf%n", &temp_f1,&n); - if (m != 1 /*|| temp_f1 < 0.0*/) - return 0; - *value_f = temp_f1; - return n; - } - - /* test is format is hex samples: 0xN */ - m = sscanf(config, " 0x%x%n", &temp_i1,&n); - if (m == 1) { - /* allow negative samples for special meanings */ - //if (temp_i1 < 0) - // return 0; - - *value_i = temp_i1; - return n; - } - - /* assume format is samples: N */ - m = sscanf(config, " %i%n", &temp_i1,&n); - if (m == 1) { - /* allow negative samples for special meanings */ - //if (temp_i1 < 0) - // return 0; - - *value_i = temp_i1; - return n; - } - - return 0; -} - -static int get_bool(const char * config, int *value) { - int n,m; - char temp; - - n = 0; /* init as it's not matched if c isn't */ - m = sscanf(config, " %c%n", &temp, &n); - if (m >= 1 && !(temp == '#' || temp == '\r' || temp == '\n')) - return 0; /* ignore if anything non-space/comment matched */ - - if (m >= 1 && temp == '#') - n--; /* don't consume separator when returning totals */ - *value = 1; - return n; -} - -static int get_mask(const char * config, uint32_t *value) { - int n, m, total_n = 0; - int temp1,temp2, r1, r2; - int i; - char cmd; - uint32_t mask = *value; - - while (config[0] != '\0') { - m = sscanf(config, " %c%n", &cmd,&n); /* consume comma */ - if (m == 1 && (cmd == ',' || cmd == '-')) { /* '-' is alt separator (space is ok too, implicitly) */ - config += n; - continue; - } - - m = sscanf(config, " %d%n ~ %d%n", &temp1,&n, &temp2,&n); - if (m == 1) { /* single values */ - r1 = temp1 - 1; - r2 = temp1 - 1; - } - else if (m == 2) { /* range */ - r1 = temp1 - 1; - r2 = temp2 - 1; - } - else { /* no more matches */ - break; - } - - if (n == 0 || r1 < 0 || r1 > 31 || r2 < 0 || r2 > 31) - break; - - for (i = r1; i < r2 + 1; i++) { - mask |= (1 << i); - } - - config += n; - total_n += n; - - if (config[0]== ',' || config[0]== '-') - config++; - } - - *value = mask; - return total_n; -} - - -static int get_fade(const char * config, txtp_mix_data *mix, int *out_n) { - int n, m, tn = 0; - char type, separator; - - m = sscanf(config, " %d %c%n", &mix->ch_dst, &type, &n); - if (m != 2 || n == 0) goto fail; - config += n; - tn += n; - - if (type == '^') { - /* full definition */ - m = sscanf(config, " %lf ~ %lf = %c @%n", &mix->vol_start, &mix->vol_end, &mix->shape, &n); - if (m != 3 || n == 0) goto fail; - config += n; - tn += n; - - n = get_time(config, &mix->time_pre, &mix->sample_pre); - if (n == 0) goto fail; - config += n; - tn += n; - - m = sscanf(config, " %c%n", &separator, &n); - if ( m != 1 || n == 0 || separator != '~') goto fail; - config += n; - tn += n; - - n = get_time(config, &mix->time_start, &mix->sample_start); - if (n == 0) goto fail; - config += n; - tn += n; - - m = sscanf(config, " %c%n", &separator, &n); - if (m != 1 || n == 0 || separator != '+') goto fail; - config += n; - tn += n; - - n = get_time(config, &mix->time_end, &mix->sample_end); - if (n == 0) goto fail; - config += n; - tn += n; - - m = sscanf(config, " %c%n", &separator, &n); - if (m != 1 || n == 0 || separator != '~') goto fail; - config += n; - tn += n; - - n = get_time(config, &mix->time_post, &mix->sample_post); - if (n == 0) goto fail; - config += n; - tn += n; - } - else { - /* simplified definition */ - if (type == '{' || type == '(') { - mix->vol_start = 0.0; - mix->vol_end = 1.0; - } - else if (type == '}' || type == ')') { - mix->vol_start = 1.0; - mix->vol_end = 0.0; - } - else { - goto fail; - } - - mix->shape = type; /* internally converted */ - - mix->time_pre = -1.0; - mix->sample_pre = -1; - - n = get_position(config, &mix->position, &mix->position_type); - //if (n == 0) goto fail; /* optional */ - config += n; - tn += n; - - n = get_time(config, &mix->time_start, &mix->sample_start); - if (n == 0) goto fail; - config += n; - tn += n; - - m = sscanf(config, " %c%n", &separator, &n); - if (m != 1 || n == 0 || separator != '+') goto fail; - config += n; - tn += n; - - n = get_time(config, &mix->time_end, &mix->sample_end); - if (n == 0) goto fail; - config += n; - tn += n; - - mix->time_post = -1.0; - mix->sample_post = -1; - } - - mix->time_end = mix->time_start + mix->time_end; /* defined as length */ - - *out_n = tn; - return 1; -fail: - return 0; -} - -void add_mixing(txtp_entry* cfg, txtp_mix_data* mix, txtp_mix_t command) { - if (cfg->mixing_count + 1 > TXTP_MIXING_MAX) { - VGM_LOG("TXTP: too many mixes\n"); - return; - } - - /* parser reads ch1 = first, but for mixing code ch0 = first - * (if parser reads ch0 here it'll become -1 with meaning of "all channels" in mixing code) */ - mix->ch_dst--; - mix->ch_src--; - mix->command = command; - - cfg->mixing[cfg->mixing_count] = *mix; /* memcpy'ed */ - cfg->mixing_count++; -} - - -static void add_config(txtp_entry* current, txtp_entry* cfg, const char* filename) { - - /* don't memcopy to allow list additions and ignore values not set, - * as current can be "default" config */ - //*current = *cfg; - - if (filename) - strcpy(current->filename, filename); - - if (cfg->subsong) - current->subsong = cfg->subsong; - - if (cfg->channel_mask) - current->channel_mask = cfg->channel_mask; - - if (cfg->mixing_count > 0) { - int i; - for (i = 0; i < cfg->mixing_count; i++) { - current->mixing[current->mixing_count] = cfg->mixing[i]; - current->mixing_count++; - } - } - - if (cfg->config_loop_count_set) { - current->config_loop_count_set = cfg->config_loop_count_set; - current->config_loop_count = cfg->config_loop_count; - } - if (cfg->config_fade_time_set) { - current->config_fade_time_set = cfg->config_fade_time_set; - current->config_fade_time = cfg->config_fade_time; - } - if (cfg->config_fade_delay_set) { - current->config_fade_delay_set = cfg->config_fade_delay_set; - current->config_fade_delay = cfg->config_fade_delay; - } - if (cfg->config_ignore_loop) { - current->config_ignore_loop = cfg->config_ignore_loop; - } - if (cfg->config_force_loop) { - current->config_force_loop = cfg->config_force_loop; - } - if (cfg->config_ignore_fade) { - current->config_ignore_fade = cfg->config_ignore_fade; - } - - if (cfg->sample_rate > 0) { - current->sample_rate = cfg->sample_rate; - } - - if (cfg->loop_install_set) { - current->loop_install_set = cfg->loop_install_set; - current->loop_end_max = cfg->loop_end_max; - current->loop_start_sample = cfg->loop_start_sample; - current->loop_start_second = cfg->loop_start_second; - current->loop_end_sample = cfg->loop_end_sample; - current->loop_end_second = cfg->loop_end_second; - } - - if (cfg->trim_set) { - current->trim_set = cfg->trim_set; - current->trim_second = cfg->trim_second; - current->trim_sample = cfg->trim_sample; - } -} - -static void parse_config(txtp_entry *cfg, char *config) { - /* parse config: #(commands) */ - int n, nc, nm, mc; - char command[TXTP_LINE_MAX] = {0}; - - cfg->range_start = 0; - cfg->range_end = 1; - - while (config != NULL) { - /* position in next #(command) */ - config = strchr(config, '#'); - if (!config) break; - //;VGM_LOG("TXTP: config='%s'\n", config); - - /* get command until next space/number/comment/end */ - command[0] = '\0'; - mc = sscanf(config, "#%n%[^ #0-9\r\n]%n", &nc, command, &nc); - //;VGM_LOG("TXTP: command='%s', nc=%i, mc=%i\n", command, nc, mc); - if (mc <= 0 && nc == 0) break; - - config[0] = '\0'; //todo don't modify input string and properly calculate filename end - - config += nc; /* skip '#' and command */ - - /* check command string (though at the moment we only use single letters) */ - if (strcmp(command,"c") == 0) { - /* channel mask: file.ext#c1,2 = play channels 1,2 and mutes rest */ - - config += get_mask(config, &cfg->channel_mask); - //;VGM_LOG("TXTP: channel_mask ");{int i; for (i=0;i<16;i++)VGM_LOG("%i ",(cfg->channel_mask>>i)&1);}VGM_LOG("\n"); - } - else if (strcmp(command,"m") == 0) { - /* channel mixing: file.ext#m(sub-command),(sub-command),etc */ - char cmd; - - while (config[0] != '\0') { - txtp_mix_data mix = {0}; - - //;VGM_LOG("TXTP: subcommand='%s'\n", config); - - //todo use strchr instead? - if (sscanf(config, " %c%n", &cmd, &n) == 1 && n != 0 && cmd == ',') { - config += n; - continue; - } - - if (sscanf(config, " %d - %d%n", &mix.ch_dst, &mix.ch_src, &n) == 2 && n != 0) { - //;VGM_LOG("TXTP: mix %i-%i\n", mix.ch_dst, mix.ch_src); - add_mixing(cfg, &mix, MIX_SWAP); /* N-M: swaps M with N */ - config += n; - continue; - } - - if ((sscanf(config, " %d + %d * %lf%n", &mix.ch_dst, &mix.ch_src, &mix.vol, &n) == 3 && n != 0) || - (sscanf(config, " %d + %d x %lf%n", &mix.ch_dst, &mix.ch_src, &mix.vol, &n) == 3 && n != 0)) { - //;VGM_LOG("TXTP: mix %i+%i*%f\n", mix.ch_dst, mix.ch_src, mix.vol); - add_mixing(cfg, &mix, MIX_ADD_VOLUME); /* N+M*V: mixes M*volume to N */ - config += n; - continue; - } - - if (sscanf(config, " %d + %d%n", &mix.ch_dst, &mix.ch_src, &n) == 2 && n != 0) { - //;VGM_LOG("TXTP: mix %i+%i\n", mix.ch_dst, mix.ch_src); - add_mixing(cfg, &mix, MIX_ADD); /* N+M: mixes M to N */ - config += n; - continue; - } - - if ((sscanf(config, " %d * %lf%n", &mix.ch_dst, &mix.vol, &n) == 2 && n != 0) || - (sscanf(config, " %d x %lf%n", &mix.ch_dst, &mix.vol, &n) == 2 && n != 0)) { - //;VGM_LOG("TXTP: mix %i*%f\n", mix.ch_dst, mix.vol); - add_mixing(cfg, &mix, MIX_VOLUME); /* N*V: changes volume of N */ - config += n; - continue; - } - - if ((sscanf(config, " %d = %lf%n", &mix.ch_dst, &mix.vol, &n) == 2 && n != 0)) { - //;VGM_LOG("TXTP: mix %i=%f\n", mix.ch_dst, mix.vol); - add_mixing(cfg, &mix, MIX_LIMIT); /* N=V: limits volume of N */ - config += n; - continue; - } - - if (sscanf(config, " %d%c%n", &mix.ch_dst, &cmd, &n) == 2 && n != 0 && cmd == 'D') { - //;VGM_LOG("TXTP: mix %iD\n", mix.ch_dst); - add_mixing(cfg, &mix, MIX_KILLMIX); /* ND: downmix N and all following channels */ - config += n; - continue; - } - - if (sscanf(config, " %d%c%n", &mix.ch_dst, &cmd, &n) == 2 && n != 0 && cmd == 'd') { - //;VGM_LOG("TXTP: mix %id\n", mix.ch_dst); - add_mixing(cfg, &mix, MIX_DOWNMIX);/* Nd: downmix N only */ - config += n; - continue; - } - - if (sscanf(config, " %d%c%n", &mix.ch_dst, &cmd, &n) == 2 && n != 0 && cmd == 'u') { - //;VGM_LOG("TXTP: mix %iu\n", mix.ch_dst); - add_mixing(cfg, &mix, MIX_UPMIX); /* Nu: upmix N */ - config += n; - continue; - } - - if (get_fade(config, &mix, &n) != 0) { - //;VGM_LOG("TXTP: fade %d^%f~%f=%c@%f~%f+%f~%f\n", - // mix.ch_dst, mix.vol_start, mix.vol_end, mix.shape, - // mix.time_pre, mix.time_start, mix.time_end, mix.time_post); - add_mixing(cfg, &mix, MIX_FADE); /* N^V1~V2@T1~T2+T3~T4: fades volumes between positions */ - config += n; - continue; - } - - break; /* unknown mix/new command/end */ - } - } - else if (strcmp(command,"s") == 0 || (nc == 1 && config[0] >= '0' && config[0] <= '9')) { - /* subsongs: file.ext#s2 = play subsong 2, file.ext#2~10 = play subsong range */ - int subsong_start = 0, subsong_end = 0; - - //todo also advance config? - if (sscanf(config, " %d ~ %d", &subsong_start, &subsong_end) == 2) { - if (subsong_start > 0 && subsong_end > 0) { - cfg->range_start = subsong_start-1; - cfg->range_end = subsong_end; - } - //;VGM_LOG("TXTP: subsong range %i~%i\n", range_start, range_end); - } - else if (sscanf(config, " %d", &subsong_start) == 1) { - if (subsong_start > 0) { - cfg->range_start = subsong_start-1; - cfg->range_end = subsong_start; - } - //;VGM_LOG("TXTP: subsong single %i-%i\n", range_start, range_end); - } - else { /* wrong config, ignore */ - //;VGM_LOG("TXTP: subsong none\n"); - } - } - else if (strcmp(command,"i") == 0) { - config += get_bool(config, &cfg->config_ignore_loop); - //;VGM_LOG("TXTP: ignore_loop=%i\n", cfg->config_ignore_loop); - } - else if (strcmp(command,"E") == 0) { - config += get_bool(config, &cfg->config_force_loop); - //;VGM_LOG("TXTP: force_loop=%i\n", cfg->config_force_loop); - } - else if (strcmp(command,"F") == 0) { - config += get_bool(config, &cfg->config_ignore_fade); - //;VGM_LOG("TXTP: ignore_fade=%i\n", cfg->config_ignore_fade); - } - else if (strcmp(command,"l") == 0) { - config += get_double(config, &cfg->config_loop_count, &cfg->config_loop_count_set); - //;VGM_LOG("TXTP: loop_count=%f\n", cfg->config_loop_count); - } - else if (strcmp(command,"f") == 0) { - config += get_double(config, &cfg->config_fade_time, &cfg->config_fade_time_set); - //;VGM_LOG("TXTP: fade_time=%f\n", cfg->config_fade_time); - } - else if (strcmp(command,"d") == 0) { - config += get_double(config, &cfg->config_fade_delay, &cfg->config_fade_delay_set); - //;VGM_LOG("TXTP: fade_delay %f\n", cfg->config_fade_delay); - } - else if (strcmp(command,"h") == 0) { - config += get_int(config, &cfg->sample_rate); - //;VGM_LOG("TXTP: sample_rate %i\n", cfg->sample_rate); - } - else if (strcmp(command,"I") == 0) { - n = get_time(config, &cfg->loop_start_second, &cfg->loop_start_sample); - if (n > 0) { /* first value must exist */ - config += n; - - n = get_time(config, &cfg->loop_end_second, &cfg->loop_end_sample); - if (n == 0) { /* second value is optional */ - cfg->loop_end_max = 1; - } - - config += n; - cfg->loop_install_set = 1; - } - - //;VGM_LOG("TXTP: loop_install %i (max=%i): %i %i / %f %f\n", cfg->loop_install, cfg->loop_end_max, - // cfg->loop_start_sample, cfg->loop_end_sample, cfg->loop_start_second, cfg->loop_end_second); - } - else if (strcmp(command,"t") == 0) { - n = get_time(config, &cfg->trim_second, &cfg->trim_sample); - cfg->trim_set = (n > 0); - //;VGM_LOG("TXTP: trim %i - %f / %i\n", cfg->trim_set, cfg->trim_second, cfg->trim_sample); - } - //todo cleanup - else if (strcmp(command,"@volume") == 0) { - txtp_mix_data mix = {0}; - - nm = get_double(config, &mix.vol, NULL); - config += nm; - - if (nm == 0) continue; - - nm = get_mask(config, &mix.mask); - config += nm; - - add_mixing(cfg, &mix, MACRO_VOLUME); - } - else if (strcmp(command,"@track") == 0 || - strcmp(command,"C") == 0 ) { - txtp_mix_data mix = {0}; - - nm = get_mask(config, &mix.mask); - config += nm; - if (nm == 0) continue; - - add_mixing(cfg, &mix, MACRO_TRACK); - } - else if (strcmp(command,"@layer-v") == 0 || - strcmp(command,"@layer-b") == 0 || - strcmp(command,"@layer-e") == 0) { - txtp_mix_data mix = {0}; - - nm = get_int(config, &mix.max); - config += nm; - if (nm == 0) continue; - - nm = get_mask(config, &mix.mask); - config += nm; - - mix.mode = command[7]; /* pass letter */ - add_mixing(cfg, &mix, MACRO_LAYER); - } - else if (strcmp(command,"@crosslayer-v") == 0 || - strcmp(command,"@crosslayer-b") == 0 || - strcmp(command,"@crosslayer-e") == 0 || - strcmp(command,"@crosstrack") == 0) { - txtp_mix_data mix = {0}; - txtp_mix_t type; - if (strcmp(command,"@crosstrack") == 0) { - type = MACRO_CROSSTRACK; - } - else { - type = MACRO_CROSSLAYER; - mix.mode = command[12]; /* pass letter */ - } - - nm = get_int(config, &mix.max); - config += nm; - if (nm == 0) continue; - - add_mixing(cfg, &mix, type); - } - else if (strcmp(command,"@downmix") == 0) { - txtp_mix_data mix = {0}; - - mix.max = 2; /* stereo only for now */ - //nm = get_int(config, &mix.max); - //config += nm; - //if (nm == 0) continue; - - add_mixing(cfg, &mix, MACRO_DOWNMIX); - } - else if (config[nc] == ' ') { - //;VGM_LOG("TXTP: comment\n"); - break; /* comment, ignore rest */ - } - else { - //;VGM_LOG("TXTP: unknown command\n"); - /* end, incorrect command, or possibly a comment or double ## comment too - * (shouldn't fail for forward compatibility) */ - break; - } - } -} - - - -static int add_group(txtp_header * txtp, char *line) { - int n, m; - txtp_group cfg = {0}; - - /* parse group: (position)(type)(count)(repeat) #(commands) */ - //;VGM_LOG("TXTP: parse group '%s'\n", line); - - m = sscanf(line, " %d%n", &cfg.position, &n); - if (m == 1) { - cfg.position--; /* externally 1=first but internally 0=first */ - line += n; - } - - m = sscanf(line, " %c%n", &cfg.type, &n); - if (m == 1) { - line += n; - } - - m = sscanf(line, " %d%n", &cfg.count, &n); - if (m == 1) { - line += n; - } - - m = sscanf(line, " %c%n", &cfg.repeat, &n); - if (m == 1 && cfg.repeat == TXTP_GROUP_REPEAT) { - line += n; - } - - - parse_config(&cfg.group_config, line); - - //;VGM_LOG("TXTP: parsed group %i%c%i%c\n",cfg.position+1,cfg.type,cfg.count,cfg.repeat); - - /* add final group */ - { - /* resize in steps if not enough */ - if (txtp->group_count+1 > txtp->group_max) { - txtp_group *temp_group; - - txtp->group_max += 5; - temp_group = realloc(txtp->group, sizeof(txtp_group) * txtp->group_max); - if (!temp_group) goto fail; - txtp->group = temp_group; - } - - /* new group */ - txtp->group[txtp->group_count] = cfg; /* memcpy */ - - txtp->group_count++; - } - - return 1; -fail: - return 0; -} - - -static int add_entry(txtp_header * txtp, char *filename, int is_default) { - int i; - txtp_entry cfg = {0}; - - - //;VGM_LOG("TXTP: filename=%s\n", filename); - - /* parse filename: file.ext#(commands) */ - { - char *config; - - if (is_default) { - config = filename; /* multiple commands without filename */ - } - else { - /* find config start (filenames and config can contain multiple dots and #, - * so this may be fooled by certain patterns of . and #) */ - config = strchr(filename, '.'); /* first dot (may be a false positive) */ - if (!config) /* extensionless */ - config = filename; - config = strchr(config, '#'); /* next should be config */ - if (!config) /* no config */ - config = NULL; - } - - parse_config(&cfg, config); - } - - - clean_filename(filename); - //;VGM_LOG("TXTP: clean filename='%s'\n", filename); - - /* config that applies to all files */ - if (is_default) { - txtp->default_entry_set = 1; - add_config(&txtp->default_entry, &cfg, NULL); - return 1; - } - - /* add final entry */ - for (i = cfg.range_start; i < cfg.range_end; i++){ - txtp_entry *current; - - /* resize in steps if not enough */ - if (txtp->entry_count+1 > txtp->entry_max) { - txtp_entry *temp_entry; - - txtp->entry_max += 5; - temp_entry = realloc(txtp->entry, sizeof(txtp_entry) * txtp->entry_max); - if (!temp_entry) goto fail; - txtp->entry = temp_entry; - } - - /* new entry */ - current = &txtp->entry[txtp->entry_count]; - memset(current,0, sizeof(txtp_entry)); - cfg.subsong = (i+1); - - add_config(current, &cfg, filename); - - txtp->entry_count++; - } - - return 1; -fail: - return 0; -} - -/* ************************************************************************ */ - -static int is_substring(const char * val, const char * cmp) { - int n; - char subval[TXTP_LINE_MAX] = {0}; - - /* read string without trailing spaces or comments/commands */ - if (sscanf(val, " %s%n[^ #\t\r\n]%n", subval, &n, &n) != 1) - return 0; - - if (0 != strcmp(subval,cmp)) - return 0; - return n; -} - -static int parse_num(const char * val, uint32_t * out_value) { - int hex = (val[0]=='0' && val[1]=='x'); - if (sscanf(val, hex ? "%x" : "%u", out_value) != 1) - goto fail; - - return 1; -fail: - return 0; -} - -static int parse_keyval(txtp_header * txtp, const char * key, const char * val) { - //;VGM_LOG("TXTP: key=val '%s'='%s'\n", key,val); - - - if (0==strcmp(key,"loop_start_segment")) { - if (!parse_num(val, &txtp->loop_start_segment)) goto fail; - } - else if (0==strcmp(key,"loop_end_segment")) { - if (!parse_num(val, &txtp->loop_end_segment)) goto fail; - } - else if (0==strcmp(key,"mode")) { - if (is_substring(val,"layers")) { - txtp->is_segmented = 0; - txtp->is_layered = 1; - } - else if (is_substring(val,"segments")) { - txtp->is_segmented = 1; - txtp->is_layered = 0; - } - else if (is_substring(val,"mixed")) { - txtp->is_segmented = 0; - txtp->is_layered = 0; - } - else { - goto fail; - } - } - else if (0==strcmp(key,"loop_mode")) { - if (is_substring(val,"keep")) { - txtp->is_loop_keep = 1; - } - else { - goto fail; - } - } - else if (0==strcmp(key,"commands")) { - char val2[TXTP_LINE_MAX]; - strcpy(val2, val); /* copy since val is modified here but probably not important */ - if (!add_entry(txtp, val2, 1)) goto fail; - } - else if (0==strcmp(key,"group")) { - char val2[TXTP_LINE_MAX]; - strcpy(val2, val); /* copy since val is modified here but probably not important */ - if (!add_group(txtp, val2)) goto fail; - - } - else { - goto fail; - } - - return 1; -fail: - VGM_LOG("TXTP: error while parsing key=val '%s'='%s'\n", key,val); - return 0; -} - -static txtp_header* parse_txtp(STREAMFILE* streamFile) { - txtp_header* txtp = NULL; - off_t txt_offset = 0x00; - off_t file_size = get_streamfile_size(streamFile); - - - txtp = calloc(1,sizeof(txtp_header)); - if (!txtp) goto fail; - - /* defaults */ - txtp->is_segmented = 1; - - - /* skip BOM if needed */ - if (file_size > 0 && - ((uint16_t)read_16bitLE(0x00, streamFile) == 0xFFFE || (uint16_t)read_16bitLE(0x00, streamFile) == 0xFEFF)) - txt_offset = 0x02; - - /* read and parse lines */ - while (txt_offset < file_size) { - char line[TXTP_LINE_MAX]; - char key[TXTP_LINE_MAX] = {0}, val[TXTP_LINE_MAX] = {0}; /* at least as big as a line to avoid overflows (I hope) */ - char filename[TXTP_LINE_MAX] = {0}; - int ok, bytes_read, line_ok; - - bytes_read = read_line(line, sizeof(line), txt_offset, streamFile, &line_ok); - if (!line_ok) goto fail; - - txt_offset += bytes_read; - - /* get key/val (ignores lead/trail spaces, # may be commands or comments) */ - ok = sscanf(line, " %[^ \t#=] = %[^\t\r\n] ", key,val); - if (ok == 2) { /* key=val */ - if (!parse_keyval(txtp, key, val)) /* read key/val */ - goto fail; - continue; - } - - /* must be a filename (only remove spaces from start/end, as filenames con contain mid spaces/#/etc) */ - ok = sscanf(line, " %[^\t\r\n] ", filename); - if (ok != 1) /* not a filename either */ - continue; - if (filename[0] == '#') - continue; /* simple comment */ - - /* filename with config */ - if (!add_entry(txtp, filename, 0)) - goto fail; - } - - /* mini-txth: if no entries are set try with filename, ex. from "song.ext#3.txtp" use "song.ext#3" - * (it's possible to have default "commands" inside the .txtp plus filename+config) */ - if (txtp->entry_count == 0) { - char filename[PATH_LIMIT] = {0}; - - get_streamfile_basename(streamFile, filename, sizeof(filename)); - - add_entry(txtp, filename, 0); - } - - - return txtp; -fail: - clean_txtp(txtp, 1); - return NULL; -} - -static void clean_txtp(txtp_header* txtp, int fail) { - int i, start; - - if (!txtp) - return; - - /* returns first vgmstream on success so it's not closed */ - start = fail ? 0 : 1; - - for (i = start; i < txtp->vgmstream_count; i++) { - close_vgmstream(txtp->vgmstream[i]); - } - - free(txtp->vgmstream); - free(txtp->group); - free(txtp->entry); - free(txtp); -} +#include "meta.h" +#include "../coding/coding.h" +#include "../layout/layout.h" +#include "../mixing.h" + + +#define TXTP_LINE_MAX 1024 +#define TXTP_MIXING_MAX 512 +#define TXTP_GROUP_MODE_SEGMENTED 'S' +#define TXTP_GROUP_MODE_LAYERED 'L' +#define TXTP_GROUP_REPEAT 'R' +#define TXTP_POSITION_LOOPS 'L' + +/* mixing info */ +typedef enum { + MIX_SWAP, + MIX_ADD, + MIX_ADD_VOLUME, + MIX_VOLUME, + MIX_LIMIT, + MIX_DOWNMIX, + MIX_KILLMIX, + MIX_UPMIX, + MIX_FADE, + + MACRO_VOLUME, + MACRO_TRACK, + MACRO_LAYER, + MACRO_CROSSTRACK, + MACRO_CROSSLAYER, + MACRO_DOWNMIX, + +} txtp_mix_t; + +typedef struct { + txtp_mix_t command; + /* common */ + int ch_dst; + int ch_src; + double vol; + + /* fade envelope */ + double vol_start; + double vol_end; + char shape; + int32_t sample_pre; + int32_t sample_start; + int32_t sample_end; + int32_t sample_post; + double time_pre; + double time_start; + double time_end; + double time_post; + double position; + char position_type; + + /* macros */ + int max; + uint32_t mask; + char mode; +} txtp_mix_data; + + +typedef struct { + char filename[TXTP_LINE_MAX]; + + int range_start; + int range_end; + int subsong; + + uint32_t channel_mask; + int mixing_count; + txtp_mix_data mixing[TXTP_MIXING_MAX]; + + int config_loop_count_set; + double config_loop_count; + int config_fade_time_set; + double config_fade_time; + int config_fade_delay_set; + double config_fade_delay; + int config_ignore_loop; + int config_force_loop; + int config_ignore_fade; + + int sample_rate; + + int loop_install_set; + int loop_end_max; + double loop_start_second; + int32_t loop_start_sample; + double loop_end_second; + int32_t loop_end_sample; + + int trim_set; + double trim_second; + int32_t trim_sample; + +} txtp_entry; + + +typedef struct { + int position; + char type; + int count; + char repeat; + + txtp_entry group_config; + +} txtp_group; + +typedef struct { + txtp_entry *entry; + size_t entry_count; + size_t entry_max; + + txtp_group *group; + size_t group_count; + size_t group_max; + + VGMSTREAM* *vgmstream; + size_t vgmstream_count; + + uint32_t loop_start_segment; + uint32_t loop_end_segment; + int is_loop_keep; + + txtp_entry default_entry; + int default_entry_set; + + int is_segmented; + int is_layered; + int is_single; +} txtp_header; + +static txtp_header* parse_txtp(STREAMFILE* streamFile); +static void clean_txtp(txtp_header* txtp, int fail); +static void apply_config(VGMSTREAM *vgmstream, txtp_entry *current); +void add_mixing(txtp_entry* cfg, txtp_mix_data* mix, txtp_mix_t command); + +static int make_group_segment(txtp_header* txtp, int from, int count); +static int make_group_layer(txtp_header* txtp, int from, int count); + + +/* TXTP - an artificial playlist-like format to play files with segments/layers/config */ +VGMSTREAM * init_vgmstream_txtp(STREAMFILE *streamFile) { + VGMSTREAM *vgmstream = NULL; + txtp_header* txtp = NULL; + int i; + + + /* checks */ + if (!check_extensions(streamFile, "txtp")) + goto fail; + + /* read .txtp with all files and config */ + txtp = parse_txtp(streamFile); + if (!txtp) goto fail; + + /* post-process */ + { + if (txtp->entry_count == 0) + goto fail; + + txtp->vgmstream = calloc(txtp->entry_count, sizeof(VGMSTREAM*)); + if (!txtp->vgmstream) goto fail; + + txtp->vgmstream_count = txtp->entry_count; + } + + + /* detect single files before grouping */ + if (txtp->group_count == 0 && txtp->vgmstream_count == 1) { + txtp->is_single = 1; + txtp->is_segmented = 0; + txtp->is_layered = 0; + } + + + /* open all entry files first as they'll be modified by modes */ + for (i = 0; i < txtp->vgmstream_count; i++) { + STREAMFILE* temp_streamFile = open_streamfile_by_filename(streamFile, txtp->entry[i].filename); + if (!temp_streamFile) { + VGM_LOG("TXTP: cannot open streamfile for %s\n", txtp->entry[i].filename); + goto fail; + } + temp_streamFile->stream_index = txtp->entry[i].subsong; + + txtp->vgmstream[i] = init_vgmstream_from_STREAMFILE(temp_streamFile); + close_streamfile(temp_streamFile); + if (!txtp->vgmstream[i]) { + VGM_LOG("TXTP: cannot open vgmstream for %s\n", txtp->entry[i].filename); + goto fail; + } + + apply_config(txtp->vgmstream[i], &txtp->entry[i]); + } + + + /* group files as needed */ + for (i = 0; i < txtp->group_count; i++) { + txtp_group *grp = &txtp->group[i]; + int pos, groups; + + //;VGM_LOG("TXTP: apply group %i%c%i%c\n",txtp->group[i].position,txtp->group[i].type,txtp->group[i].count,txtp->group[i].repeat); + + /* special meaning of "all files" */ + if (grp->position < 0 || grp->position >= txtp->vgmstream_count) + grp->position = 0; + if (grp->count <= 0) + grp->count = txtp->vgmstream_count - grp->position; + + /* repeats N groups (trailing files are not grouped) */ + if (grp->repeat == TXTP_GROUP_REPEAT) { + groups = ((txtp->vgmstream_count - grp->position) / grp->count); + } + else { + groups = 1; + } + + /* as groups are compacted position goes 1 by 1 */ + for (pos = grp->position; pos < grp->position + groups; pos++) { + //;VGM_LOG("TXTP: group=%i, count=%i, groups=%i\n", pos, grp->count, groups); + switch(grp->type) { + case TXTP_GROUP_MODE_LAYERED: + if (!make_group_layer(txtp, pos, grp->count)) + goto fail; + break; + case TXTP_GROUP_MODE_SEGMENTED: + if (!make_group_segment(txtp, pos, grp->count)) + goto fail; + break; + default: + goto fail; + } + } + + /* group may also have config (like downmixing) */ + apply_config(txtp->vgmstream[grp->position], &grp->group_config); + } + + /* final tweaks (should be integrated with the above?) */ + if (txtp->is_layered) { + if (!make_group_layer(txtp, 0, txtp->vgmstream_count)) + goto fail; + } + if (txtp->is_segmented) { + if (!make_group_segment(txtp, 0, txtp->vgmstream_count)) + goto fail; + } + if (txtp->is_single) { + /* special case of setting start_segment to force/overwrite looping + * (better to use #E but left for compatibility with older TXTPs) */ + if (txtp->loop_start_segment == 1 && !txtp->loop_end_segment) { + vgmstream_force_loop(txtp->vgmstream[0], 1, txtp->vgmstream[0]->loop_start_sample, txtp->vgmstream[0]->num_samples); + } + } + + + /* may happen if using mixed mode but some files weren't grouped */ + if (txtp->vgmstream_count != 1) { + VGM_LOG("TXTP: wrong final vgmstream count %i\n", txtp->vgmstream_count); + goto fail; + } + + /* apply default config to the resulting file */ + if (txtp->default_entry_set) { + apply_config(txtp->vgmstream[0], &txtp->default_entry); + } + + + vgmstream = txtp->vgmstream[0]; + + clean_txtp(txtp, 0); + return vgmstream; + +fail: + clean_txtp(txtp, 1); + return NULL; +} + +static void update_vgmstream_list(VGMSTREAM* vgmstream, txtp_header* txtp, int position, int count) { + int i; + + //;VGM_LOG("TXTP: compact position=%i count=%i, vgmstreams=%i\n", position, count, txtp->vgmstream_count); + + /* sets and compacts vgmstream list pulling back all following entries */ + txtp->vgmstream[position] = vgmstream; + for (i = position + count; i < txtp->vgmstream_count; i++) { + //;VGM_LOG("TXTP: copy %i to %i\n", i, i + 1 - count); + txtp->vgmstream[i + 1 - count] = txtp->vgmstream[i]; + } + + /* list can only become smaller, no need to alloc/free/etc */ + txtp->vgmstream_count = txtp->vgmstream_count + 1 - count; + //;VGM_LOG("TXTP: compact vgmstreams=%i\n", txtp->vgmstream_count); +} + +static int make_group_segment(txtp_header* txtp, int position, int count) { + VGMSTREAM * vgmstream = NULL; + segmented_layout_data *data_s = NULL; + int i, loop_flag = 0; + + + if (count == 1) { /* nothing to do */ + //;VGM_LOG("TXTP: ignored segments of 1\n"); + return 1; + } + + if (position + count > txtp->vgmstream_count || position < 0 || count < 0) { + VGM_LOG("TXTP: ignored segment position=%i, count=%i, entries=%i\n", position, count, txtp->vgmstream_count); + return 1; + } + + /* loop settings only make sense if this group becomes final vgmstream */ + if (position == 0 && txtp->vgmstream_count == count) { + if (txtp->loop_start_segment && !txtp->loop_end_segment) + txtp->loop_end_segment = count; + loop_flag = (txtp->loop_start_segment > 0 && txtp->loop_start_segment <= count); + } + + + /* init layout */ + data_s = init_layout_segmented(count); + if (!data_s) goto fail; + + /* copy each subfile */ + for (i = 0; i < count; i++) { + data_s->segments[i] = txtp->vgmstream[i + position]; + txtp->vgmstream[i + position] = NULL; /* will be freed by layout */ + } + + /* setup VGMSTREAMs */ + if (!setup_layout_segmented(data_s)) + goto fail; + + /* build the layout VGMSTREAM */ + vgmstream = allocate_segmented_vgmstream(data_s,loop_flag, txtp->loop_start_segment - 1, txtp->loop_end_segment - 1); + if (!vgmstream) goto fail; + + /* custom meta name if all parts don't match */ + for (i = 0; i < data_s->segment_count; i++) { + if (vgmstream->meta_type != data_s->segments[i]->meta_type) { + vgmstream->meta_type = meta_TXTP; + break; + } + } + + /* fix loop keep */ + if (loop_flag && txtp->is_loop_keep) { + int32_t current_samples = 0; + for (i = 0; i < data_s->segment_count; i++) { + if (txtp->loop_start_segment == i+1 /*&& data_s->segments[i]->loop_start_sample*/) { + vgmstream->loop_start_sample = current_samples + data_s->segments[i]->loop_start_sample; + } + + current_samples += data_s->segments[i]->num_samples; + + if (txtp->loop_end_segment == i+1 && data_s->segments[i]->loop_end_sample) { + vgmstream->loop_end_sample = current_samples - data_s->segments[i]->num_samples + data_s->segments[i]->loop_end_sample; + } + } + } + + + /* set new vgmstream and reorder positions */ + update_vgmstream_list(vgmstream, txtp, position, count); + + return 1; +fail: + close_vgmstream(vgmstream); + if (!vgmstream) + free_layout_segmented(data_s); + return 0; +} + +static int make_group_layer(txtp_header* txtp, int position, int count) { + VGMSTREAM * vgmstream = NULL; + layered_layout_data * data_l = NULL; + int i; + + + if (count == 1) { /* nothing to do */ + //;VGM_LOG("TXTP: ignored layer of 1\n"); + return 1; + } + + if (position + count > txtp->vgmstream_count || position < 0 || count < 0) { + VGM_LOG("TXTP: ignored layer position=%i, count=%i, entries=%i\n", position, count, txtp->vgmstream_count); + return 1; + } + + + /* init layout */ + data_l = init_layout_layered(count); + if (!data_l) goto fail; + + /* copy each subfile */ + for (i = 0; i < count; i++) { + data_l->layers[i] = txtp->vgmstream[i + position]; + txtp->vgmstream[i + position] = NULL; /* will be freed by layout */ + } + + /* setup VGMSTREAMs */ + if (!setup_layout_layered(data_l)) + goto fail; + + /* build the layout VGMSTREAM */ + vgmstream = allocate_layered_vgmstream(data_l); + if (!vgmstream) goto fail; + + /* custom meta name if all parts don't match */ + for (i = 0; i < count; i++) { + if (vgmstream->meta_type != data_l->layers[i]->meta_type) { + vgmstream->meta_type = meta_TXTP; + break; + } + } + + + /* set new vgmstream and reorder positions */ + update_vgmstream_list(vgmstream, txtp, position, count); + + return 1; +fail: + close_vgmstream(vgmstream); + if (!vgmstream) + free_layout_layered(data_l); + return 0; +} + + +static void apply_config(VGMSTREAM *vgmstream, txtp_entry *current) { + + if (current->config_loop_count_set) + vgmstream->config_loop_count = current->config_loop_count; + if (current->config_fade_time_set) + vgmstream->config_fade_time = current->config_fade_time; + if (current->config_fade_delay_set) + vgmstream->config_fade_delay = current->config_fade_delay; + if (current->config_ignore_loop) + vgmstream->config_ignore_loop = current->config_ignore_loop; + if (current->config_force_loop) + vgmstream->config_force_loop = current->config_force_loop; + if (current->config_ignore_fade) + vgmstream->config_ignore_fade = current->config_ignore_fade; + + if (current->sample_rate > 0) + vgmstream->sample_rate = current->sample_rate; + + if (current->loop_install_set) { + if (current->loop_start_second > 0 || current->loop_end_second > 0) { + current->loop_start_sample = current->loop_start_second * vgmstream->sample_rate; + current->loop_end_sample = current->loop_end_second * vgmstream->sample_rate; + if (current->loop_end_sample > vgmstream->num_samples && + current->loop_end_sample - vgmstream->num_samples <= 0.1 * vgmstream->sample_rate) + current->loop_end_sample = vgmstream->num_samples; /* allow some rounding leeway */ + } + + if (current->loop_end_max) { + current->loop_end_sample = vgmstream->num_samples; + } + + vgmstream_force_loop(vgmstream, current->loop_install_set, current->loop_start_sample, current->loop_end_sample); + } + + if (current->trim_set) { + if (current->trim_second != 0.0) { + current->trim_sample = current->trim_second * vgmstream->sample_rate; + } + + if (current->trim_sample < 0) { + vgmstream->num_samples += current->trim_sample; /* trim from end (add negative) */ + } + else if (vgmstream->num_samples > current->trim_sample) { + vgmstream->num_samples = current->trim_sample; /* trim to value */ + } + + /* readjust after triming if it went over (could check for more edge cases but eh) */ + if (vgmstream->loop_end_sample > vgmstream->num_samples) + vgmstream->loop_end_sample = vgmstream->num_samples; + } + + + /* add macro to mixing list */ + if (current->channel_mask) { + int ch; + for (ch = 0; ch < vgmstream->channels; ch++) { + if (!((current->channel_mask >> ch) & 1)) { + txtp_mix_data mix = {0}; + mix.ch_dst = ch + 1; + mix.vol = 0.0f; + add_mixing(current, &mix, MIX_VOLUME); + } + } + } + + /* copy mixing list (should be done last as some mixes depend on config) */ + if (current->mixing_count > 0) { + int m, position_samples; + + for (m = 0; m < current->mixing_count; m++) { + txtp_mix_data *mix = ¤t->mixing[m]; + + switch(mix->command) { + /* base mixes */ + case MIX_SWAP: mixing_push_swap(vgmstream, mix->ch_dst, mix->ch_src); break; + case MIX_ADD: mixing_push_add(vgmstream, mix->ch_dst, mix->ch_src, 1.0); break; + case MIX_ADD_VOLUME: mixing_push_add(vgmstream, mix->ch_dst, mix->ch_src, mix->vol); break; + case MIX_VOLUME: mixing_push_volume(vgmstream, mix->ch_dst, mix->vol); break; + case MIX_LIMIT: mixing_push_limit(vgmstream, mix->ch_dst, mix->vol); break; + case MIX_UPMIX: mixing_push_upmix(vgmstream, mix->ch_dst); break; + case MIX_DOWNMIX: mixing_push_downmix(vgmstream, mix->ch_dst); break; + case MIX_KILLMIX: mixing_push_killmix(vgmstream, mix->ch_dst); break; + case MIX_FADE: + /* Convert from time to samples now that sample rate is final. + * Samples and time values may be mixed though, so it's done for every + * value (if one is 0 the other will be too, though) */ + if (mix->time_pre > 0.0) mix->sample_pre = mix->time_pre * vgmstream->sample_rate; + if (mix->time_start > 0.0) mix->sample_start = mix->time_start * vgmstream->sample_rate; + if (mix->time_end > 0.0) mix->sample_end = mix->time_end * vgmstream->sample_rate; + if (mix->time_post > 0.0) mix->sample_post = mix->time_post * vgmstream->sample_rate; + /* convert special meaning too */ + if (mix->time_pre < 0.0) mix->sample_pre = -1; + if (mix->time_post < 0.0) mix->sample_post = -1; + + if (mix->position_type == TXTP_POSITION_LOOPS && vgmstream->loop_flag) { + int loop_pre = vgmstream->loop_start_sample; + int loop_samples = (vgmstream->loop_end_sample - vgmstream->loop_start_sample); + + position_samples = loop_pre + loop_samples * mix->position; + + if (mix->sample_pre >= 0) mix->sample_pre += position_samples; + mix->sample_start += position_samples; + mix->sample_end += position_samples; + if (mix->sample_post >= 0) mix->sample_post += position_samples; + } + + + mixing_push_fade(vgmstream, mix->ch_dst, mix->vol_start, mix->vol_end, mix->shape, + mix->sample_pre, mix->sample_start, mix->sample_end, mix->sample_post); + break; + + /* macro mixes */ + case MACRO_VOLUME: mixing_macro_volume(vgmstream, mix->vol, mix->mask); break; + case MACRO_TRACK: mixing_macro_track(vgmstream, mix->mask); break; + case MACRO_LAYER: mixing_macro_layer(vgmstream, mix->max, mix->mask, mix->mode); break; + case MACRO_CROSSTRACK: mixing_macro_crosstrack(vgmstream, mix->max); break; + case MACRO_CROSSLAYER: mixing_macro_crosslayer(vgmstream, mix->max, mix->mode); break; + case MACRO_DOWNMIX: mixing_macro_downmix(vgmstream, mix->max); break; + + default: + break; + } + } + } +} + +/* ********************************** */ + +static void clean_filename(char * filename) { + int i; + size_t len; + + if (filename[0] == '\0') + return; + + /* normalize paths */ + fix_dir_separators(filename); + + /* remove trailing spaces */ + len = strlen(filename); + for (i = len-1; i > 0; i--) { + if (filename[i] != ' ') + break; + filename[i] = '\0'; + } + +} + + +/* sscanf 101: "matches = sscanf(string-from, string-commands, parameters...)" + * - reads linearly and matches "%" commands to input parameters as found + * - reads until string end (NULL) or not being able to match current parameter + * - returns number of matched % parameters until stop, or -1 if no matches and reached string end + * - must supply pointer param for every "%" in the string + * - %d/f: match number until end or *non-number* (so "%d" reads "5t" as "5") + * - %s: reads string (dangerous due to overflows and surprising as %s%d can't match numbers since string eats all chars) + * - %[^(chars)] match string with chars not in the list (stop reading at those chars) + * - %*(command) read but don't match (no need to supply parameterr) + * - " ": ignore all spaces until next non-space + * - other chars in string must exist: ("%dt t%dt" reads "5t t5t" as "5" and "5", while "t5t 5t" matches only first "5") + * - %n: special match (not counted in return value), chars consumed until that point (can appear and be set multiple times) + */ + +static int get_double(const char * config, double *value, int *is_set) { + int n, m; + double temp; + + if (is_set) *is_set = 0; + + m = sscanf(config, " %lf%n", &temp,&n); + if (m != 1 || temp < 0) + return 0; + + if (is_set) *is_set = 1; + *value = temp; + return n; +} + +static int get_int(const char * config, int *value) { + int n,m; + int temp; + + m = sscanf(config, " %i%n", &temp,&n); + if (m != 1 || temp < 0) + return 0; + + *value = temp; + return n; +} + +static int get_position(const char * config, double *value_f, char *value_type) { + int n,m; + double temp_f; + char temp_c; + + /* test if format is position: N.n(type) */ + m = sscanf(config, " %lf%c%n", &temp_f,&temp_c,&n); + if (m != 2 || temp_f < 0.0) + return 0; + /* test accepted chars as it will capture anything */ + if (temp_c != TXTP_POSITION_LOOPS) + return 0; + + *value_f = temp_f; + *value_type = temp_c; + return n; +} + + +static int get_time(const char * config, double *value_f, int32_t *value_i) { + int n,m; + int temp_i1, temp_i2; + double temp_f1, temp_f2; + char temp_c; + + /* test if format is hour: N:N(.n) or N_N(.n) */ + m = sscanf(config, " %i%c%i%n", &temp_i1,&temp_c,&temp_i2,&n); + if (m == 3 && (temp_c == ':' || temp_c == '_')) { + m = sscanf(config, " %lf%c%lf%n", &temp_f1,&temp_c,&temp_f2,&n); + if (m != 3 || /*temp_f1 < 0.0 ||*/ temp_f1 >= 60.0 || temp_f2 < 0.0 || temp_f2 >= 60.0) + return 0; + + *value_f = temp_f1 * 60.0 + temp_f2; + return n; + } + + /* test if format is seconds: N.n */ + m = sscanf(config, " %i.%i%n", &temp_i1,&temp_i2,&n); + if (m == 2) { + m = sscanf(config, " %lf%n", &temp_f1,&n); + if (m != 1 /*|| temp_f1 < 0.0*/) + return 0; + *value_f = temp_f1; + return n; + } + + /* test is format is hex samples: 0xN */ + m = sscanf(config, " 0x%x%n", &temp_i1,&n); + if (m == 1) { + /* allow negative samples for special meanings */ + //if (temp_i1 < 0) + // return 0; + + *value_i = temp_i1; + return n; + } + + /* assume format is samples: N */ + m = sscanf(config, " %i%n", &temp_i1,&n); + if (m == 1) { + /* allow negative samples for special meanings */ + //if (temp_i1 < 0) + // return 0; + + *value_i = temp_i1; + return n; + } + + return 0; +} + +static int get_bool(const char * config, int *value) { + int n,m; + char temp; + + n = 0; /* init as it's not matched if c isn't */ + m = sscanf(config, " %c%n", &temp, &n); + if (m >= 1 && !(temp == '#' || temp == '\r' || temp == '\n')) + return 0; /* ignore if anything non-space/comment matched */ + + if (m >= 1 && temp == '#') + n--; /* don't consume separator when returning totals */ + *value = 1; + return n; +} + +static int get_mask(const char * config, uint32_t *value) { + int n, m, total_n = 0; + int temp1,temp2, r1, r2; + int i; + char cmd; + uint32_t mask = *value; + + while (config[0] != '\0') { + m = sscanf(config, " %c%n", &cmd,&n); /* consume comma */ + if (m == 1 && (cmd == ',' || cmd == '-')) { /* '-' is alt separator (space is ok too, implicitly) */ + config += n; + continue; + } + + m = sscanf(config, " %d%n ~ %d%n", &temp1,&n, &temp2,&n); + if (m == 1) { /* single values */ + r1 = temp1 - 1; + r2 = temp1 - 1; + } + else if (m == 2) { /* range */ + r1 = temp1 - 1; + r2 = temp2 - 1; + } + else { /* no more matches */ + break; + } + + if (n == 0 || r1 < 0 || r1 > 31 || r2 < 0 || r2 > 31) + break; + + for (i = r1; i < r2 + 1; i++) { + mask |= (1 << i); + } + + config += n; + total_n += n; + + if (config[0]== ',' || config[0]== '-') + config++; + } + + *value = mask; + return total_n; +} + + +static int get_fade(const char * config, txtp_mix_data *mix, int *out_n) { + int n, m, tn = 0; + char type, separator; + + m = sscanf(config, " %d %c%n", &mix->ch_dst, &type, &n); + if (m != 2 || n == 0) goto fail; + config += n; + tn += n; + + if (type == '^') { + /* full definition */ + m = sscanf(config, " %lf ~ %lf = %c @%n", &mix->vol_start, &mix->vol_end, &mix->shape, &n); + if (m != 3 || n == 0) goto fail; + config += n; + tn += n; + + n = get_time(config, &mix->time_pre, &mix->sample_pre); + if (n == 0) goto fail; + config += n; + tn += n; + + m = sscanf(config, " %c%n", &separator, &n); + if ( m != 1 || n == 0 || separator != '~') goto fail; + config += n; + tn += n; + + n = get_time(config, &mix->time_start, &mix->sample_start); + if (n == 0) goto fail; + config += n; + tn += n; + + m = sscanf(config, " %c%n", &separator, &n); + if (m != 1 || n == 0 || separator != '+') goto fail; + config += n; + tn += n; + + n = get_time(config, &mix->time_end, &mix->sample_end); + if (n == 0) goto fail; + config += n; + tn += n; + + m = sscanf(config, " %c%n", &separator, &n); + if (m != 1 || n == 0 || separator != '~') goto fail; + config += n; + tn += n; + + n = get_time(config, &mix->time_post, &mix->sample_post); + if (n == 0) goto fail; + config += n; + tn += n; + } + else { + /* simplified definition */ + if (type == '{' || type == '(') { + mix->vol_start = 0.0; + mix->vol_end = 1.0; + } + else if (type == '}' || type == ')') { + mix->vol_start = 1.0; + mix->vol_end = 0.0; + } + else { + goto fail; + } + + mix->shape = type; /* internally converted */ + + mix->time_pre = -1.0; + mix->sample_pre = -1; + + n = get_position(config, &mix->position, &mix->position_type); + //if (n == 0) goto fail; /* optional */ + config += n; + tn += n; + + n = get_time(config, &mix->time_start, &mix->sample_start); + if (n == 0) goto fail; + config += n; + tn += n; + + m = sscanf(config, " %c%n", &separator, &n); + if (m != 1 || n == 0 || separator != '+') goto fail; + config += n; + tn += n; + + n = get_time(config, &mix->time_end, &mix->sample_end); + if (n == 0) goto fail; + config += n; + tn += n; + + mix->time_post = -1.0; + mix->sample_post = -1; + } + + mix->time_end = mix->time_start + mix->time_end; /* defined as length */ + + *out_n = tn; + return 1; +fail: + return 0; +} + +void add_mixing(txtp_entry* cfg, txtp_mix_data* mix, txtp_mix_t command) { + if (cfg->mixing_count + 1 > TXTP_MIXING_MAX) { + VGM_LOG("TXTP: too many mixes\n"); + return; + } + + /* parser reads ch1 = first, but for mixing code ch0 = first + * (if parser reads ch0 here it'll become -1 with meaning of "all channels" in mixing code) */ + mix->ch_dst--; + mix->ch_src--; + mix->command = command; + + cfg->mixing[cfg->mixing_count] = *mix; /* memcpy'ed */ + cfg->mixing_count++; +} + + +static void add_config(txtp_entry* current, txtp_entry* cfg, const char* filename) { + + /* don't memcopy to allow list additions and ignore values not set, + * as current can be "default" config */ + //*current = *cfg; + + if (filename) + strcpy(current->filename, filename); + + if (cfg->subsong) + current->subsong = cfg->subsong; + + if (cfg->channel_mask) + current->channel_mask = cfg->channel_mask; + + if (cfg->mixing_count > 0) { + int i; + for (i = 0; i < cfg->mixing_count; i++) { + current->mixing[current->mixing_count] = cfg->mixing[i]; + current->mixing_count++; + } + } + + if (cfg->config_loop_count_set) { + current->config_loop_count_set = cfg->config_loop_count_set; + current->config_loop_count = cfg->config_loop_count; + } + if (cfg->config_fade_time_set) { + current->config_fade_time_set = cfg->config_fade_time_set; + current->config_fade_time = cfg->config_fade_time; + } + if (cfg->config_fade_delay_set) { + current->config_fade_delay_set = cfg->config_fade_delay_set; + current->config_fade_delay = cfg->config_fade_delay; + } + if (cfg->config_ignore_loop) { + current->config_ignore_loop = cfg->config_ignore_loop; + } + if (cfg->config_force_loop) { + current->config_force_loop = cfg->config_force_loop; + } + if (cfg->config_ignore_fade) { + current->config_ignore_fade = cfg->config_ignore_fade; + } + + if (cfg->sample_rate > 0) { + current->sample_rate = cfg->sample_rate; + } + + if (cfg->loop_install_set) { + current->loop_install_set = cfg->loop_install_set; + current->loop_end_max = cfg->loop_end_max; + current->loop_start_sample = cfg->loop_start_sample; + current->loop_start_second = cfg->loop_start_second; + current->loop_end_sample = cfg->loop_end_sample; + current->loop_end_second = cfg->loop_end_second; + } + + if (cfg->trim_set) { + current->trim_set = cfg->trim_set; + current->trim_second = cfg->trim_second; + current->trim_sample = cfg->trim_sample; + } +} + +static void parse_config(txtp_entry *cfg, char *config) { + /* parse config: #(commands) */ + int n, nc, nm, mc; + char command[TXTP_LINE_MAX] = {0}; + + cfg->range_start = 0; + cfg->range_end = 1; + + while (config != NULL) { + /* position in next #(command) */ + config = strchr(config, '#'); + if (!config) break; + //;VGM_LOG("TXTP: config='%s'\n", config); + + /* get command until next space/number/comment/end */ + command[0] = '\0'; + mc = sscanf(config, "#%n%[^ #0-9\r\n]%n", &nc, command, &nc); + //;VGM_LOG("TXTP: command='%s', nc=%i, mc=%i\n", command, nc, mc); + if (mc <= 0 && nc == 0) break; + + config[0] = '\0'; //todo don't modify input string and properly calculate filename end + + config += nc; /* skip '#' and command */ + + /* check command string (though at the moment we only use single letters) */ + if (strcmp(command,"c") == 0) { + /* channel mask: file.ext#c1,2 = play channels 1,2 and mutes rest */ + + config += get_mask(config, &cfg->channel_mask); + //;VGM_LOG("TXTP: channel_mask ");{int i; for (i=0;i<16;i++)VGM_LOG("%i ",(cfg->channel_mask>>i)&1);}VGM_LOG("\n"); + } + else if (strcmp(command,"m") == 0) { + /* channel mixing: file.ext#m(sub-command),(sub-command),etc */ + char cmd; + + while (config[0] != '\0') { + txtp_mix_data mix = {0}; + + //;VGM_LOG("TXTP: subcommand='%s'\n", config); + + //todo use strchr instead? + if (sscanf(config, " %c%n", &cmd, &n) == 1 && n != 0 && cmd == ',') { + config += n; + continue; + } + + if (sscanf(config, " %d - %d%n", &mix.ch_dst, &mix.ch_src, &n) == 2 && n != 0) { + //;VGM_LOG("TXTP: mix %i-%i\n", mix.ch_dst, mix.ch_src); + add_mixing(cfg, &mix, MIX_SWAP); /* N-M: swaps M with N */ + config += n; + continue; + } + + if ((sscanf(config, " %d + %d * %lf%n", &mix.ch_dst, &mix.ch_src, &mix.vol, &n) == 3 && n != 0) || + (sscanf(config, " %d + %d x %lf%n", &mix.ch_dst, &mix.ch_src, &mix.vol, &n) == 3 && n != 0)) { + //;VGM_LOG("TXTP: mix %i+%i*%f\n", mix.ch_dst, mix.ch_src, mix.vol); + add_mixing(cfg, &mix, MIX_ADD_VOLUME); /* N+M*V: mixes M*volume to N */ + config += n; + continue; + } + + if (sscanf(config, " %d + %d%n", &mix.ch_dst, &mix.ch_src, &n) == 2 && n != 0) { + //;VGM_LOG("TXTP: mix %i+%i\n", mix.ch_dst, mix.ch_src); + add_mixing(cfg, &mix, MIX_ADD); /* N+M: mixes M to N */ + config += n; + continue; + } + + if ((sscanf(config, " %d * %lf%n", &mix.ch_dst, &mix.vol, &n) == 2 && n != 0) || + (sscanf(config, " %d x %lf%n", &mix.ch_dst, &mix.vol, &n) == 2 && n != 0)) { + //;VGM_LOG("TXTP: mix %i*%f\n", mix.ch_dst, mix.vol); + add_mixing(cfg, &mix, MIX_VOLUME); /* N*V: changes volume of N */ + config += n; + continue; + } + + if ((sscanf(config, " %d = %lf%n", &mix.ch_dst, &mix.vol, &n) == 2 && n != 0)) { + //;VGM_LOG("TXTP: mix %i=%f\n", mix.ch_dst, mix.vol); + add_mixing(cfg, &mix, MIX_LIMIT); /* N=V: limits volume of N */ + config += n; + continue; + } + + if (sscanf(config, " %d%c%n", &mix.ch_dst, &cmd, &n) == 2 && n != 0 && cmd == 'D') { + //;VGM_LOG("TXTP: mix %iD\n", mix.ch_dst); + add_mixing(cfg, &mix, MIX_KILLMIX); /* ND: downmix N and all following channels */ + config += n; + continue; + } + + if (sscanf(config, " %d%c%n", &mix.ch_dst, &cmd, &n) == 2 && n != 0 && cmd == 'd') { + //;VGM_LOG("TXTP: mix %id\n", mix.ch_dst); + add_mixing(cfg, &mix, MIX_DOWNMIX);/* Nd: downmix N only */ + config += n; + continue; + } + + if (sscanf(config, " %d%c%n", &mix.ch_dst, &cmd, &n) == 2 && n != 0 && cmd == 'u') { + //;VGM_LOG("TXTP: mix %iu\n", mix.ch_dst); + add_mixing(cfg, &mix, MIX_UPMIX); /* Nu: upmix N */ + config += n; + continue; + } + + if (get_fade(config, &mix, &n) != 0) { + //;VGM_LOG("TXTP: fade %d^%f~%f=%c@%f~%f+%f~%f\n", + // mix.ch_dst, mix.vol_start, mix.vol_end, mix.shape, + // mix.time_pre, mix.time_start, mix.time_end, mix.time_post); + add_mixing(cfg, &mix, MIX_FADE); /* N^V1~V2@T1~T2+T3~T4: fades volumes between positions */ + config += n; + continue; + } + + break; /* unknown mix/new command/end */ + } + } + else if (strcmp(command,"s") == 0 || (nc == 1 && config[0] >= '0' && config[0] <= '9')) { + /* subsongs: file.ext#s2 = play subsong 2, file.ext#2~10 = play subsong range */ + int subsong_start = 0, subsong_end = 0; + + //todo also advance config? + if (sscanf(config, " %d ~ %d", &subsong_start, &subsong_end) == 2) { + if (subsong_start > 0 && subsong_end > 0) { + cfg->range_start = subsong_start-1; + cfg->range_end = subsong_end; + } + //;VGM_LOG("TXTP: subsong range %i~%i\n", range_start, range_end); + } + else if (sscanf(config, " %d", &subsong_start) == 1) { + if (subsong_start > 0) { + cfg->range_start = subsong_start-1; + cfg->range_end = subsong_start; + } + //;VGM_LOG("TXTP: subsong single %i-%i\n", range_start, range_end); + } + else { /* wrong config, ignore */ + //;VGM_LOG("TXTP: subsong none\n"); + } + } + else if (strcmp(command,"i") == 0) { + config += get_bool(config, &cfg->config_ignore_loop); + //;VGM_LOG("TXTP: ignore_loop=%i\n", cfg->config_ignore_loop); + } + else if (strcmp(command,"E") == 0) { + config += get_bool(config, &cfg->config_force_loop); + //;VGM_LOG("TXTP: force_loop=%i\n", cfg->config_force_loop); + } + else if (strcmp(command,"F") == 0) { + config += get_bool(config, &cfg->config_ignore_fade); + //;VGM_LOG("TXTP: ignore_fade=%i\n", cfg->config_ignore_fade); + } + else if (strcmp(command,"l") == 0) { + config += get_double(config, &cfg->config_loop_count, &cfg->config_loop_count_set); + //;VGM_LOG("TXTP: loop_count=%f\n", cfg->config_loop_count); + } + else if (strcmp(command,"f") == 0) { + config += get_double(config, &cfg->config_fade_time, &cfg->config_fade_time_set); + //;VGM_LOG("TXTP: fade_time=%f\n", cfg->config_fade_time); + } + else if (strcmp(command,"d") == 0) { + config += get_double(config, &cfg->config_fade_delay, &cfg->config_fade_delay_set); + //;VGM_LOG("TXTP: fade_delay %f\n", cfg->config_fade_delay); + } + else if (strcmp(command,"h") == 0) { + config += get_int(config, &cfg->sample_rate); + //;VGM_LOG("TXTP: sample_rate %i\n", cfg->sample_rate); + } + else if (strcmp(command,"I") == 0) { + n = get_time(config, &cfg->loop_start_second, &cfg->loop_start_sample); + if (n > 0) { /* first value must exist */ + config += n; + + n = get_time(config, &cfg->loop_end_second, &cfg->loop_end_sample); + if (n == 0) { /* second value is optional */ + cfg->loop_end_max = 1; + } + + config += n; + cfg->loop_install_set = 1; + } + + //;VGM_LOG("TXTP: loop_install %i (max=%i): %i %i / %f %f\n", cfg->loop_install, cfg->loop_end_max, + // cfg->loop_start_sample, cfg->loop_end_sample, cfg->loop_start_second, cfg->loop_end_second); + } + else if (strcmp(command,"t") == 0) { + n = get_time(config, &cfg->trim_second, &cfg->trim_sample); + cfg->trim_set = (n > 0); + //;VGM_LOG("TXTP: trim %i - %f / %i\n", cfg->trim_set, cfg->trim_second, cfg->trim_sample); + } + //todo cleanup + else if (strcmp(command,"@volume") == 0) { + txtp_mix_data mix = {0}; + + nm = get_double(config, &mix.vol, NULL); + config += nm; + + if (nm == 0) continue; + + nm = get_mask(config, &mix.mask); + config += nm; + + add_mixing(cfg, &mix, MACRO_VOLUME); + } + else if (strcmp(command,"@track") == 0 || + strcmp(command,"C") == 0 ) { + txtp_mix_data mix = {0}; + + nm = get_mask(config, &mix.mask); + config += nm; + if (nm == 0) continue; + + add_mixing(cfg, &mix, MACRO_TRACK); + } + else if (strcmp(command,"@layer-v") == 0 || + strcmp(command,"@layer-b") == 0 || + strcmp(command,"@layer-e") == 0) { + txtp_mix_data mix = {0}; + + nm = get_int(config, &mix.max); + config += nm; + if (nm == 0) continue; + + nm = get_mask(config, &mix.mask); + config += nm; + + mix.mode = command[7]; /* pass letter */ + add_mixing(cfg, &mix, MACRO_LAYER); + } + else if (strcmp(command,"@crosslayer-v") == 0 || + strcmp(command,"@crosslayer-b") == 0 || + strcmp(command,"@crosslayer-e") == 0 || + strcmp(command,"@crosstrack") == 0) { + txtp_mix_data mix = {0}; + txtp_mix_t type; + if (strcmp(command,"@crosstrack") == 0) { + type = MACRO_CROSSTRACK; + } + else { + type = MACRO_CROSSLAYER; + mix.mode = command[12]; /* pass letter */ + } + + nm = get_int(config, &mix.max); + config += nm; + if (nm == 0) continue; + + add_mixing(cfg, &mix, type); + } + else if (strcmp(command,"@downmix") == 0) { + txtp_mix_data mix = {0}; + + mix.max = 2; /* stereo only for now */ + //nm = get_int(config, &mix.max); + //config += nm; + //if (nm == 0) continue; + + add_mixing(cfg, &mix, MACRO_DOWNMIX); + } + else if (config[nc] == ' ') { + //;VGM_LOG("TXTP: comment\n"); + break; /* comment, ignore rest */ + } + else { + //;VGM_LOG("TXTP: unknown command\n"); + /* end, incorrect command, or possibly a comment or double ## comment too + * (shouldn't fail for forward compatibility) */ + break; + } + } +} + + + +static int add_group(txtp_header * txtp, char *line) { + int n, m; + txtp_group cfg = {0}; + + /* parse group: (position)(type)(count)(repeat) #(commands) */ + //;VGM_LOG("TXTP: parse group '%s'\n", line); + + m = sscanf(line, " %d%n", &cfg.position, &n); + if (m == 1) { + cfg.position--; /* externally 1=first but internally 0=first */ + line += n; + } + + m = sscanf(line, " %c%n", &cfg.type, &n); + if (m == 1) { + line += n; + } + + m = sscanf(line, " %d%n", &cfg.count, &n); + if (m == 1) { + line += n; + } + + m = sscanf(line, " %c%n", &cfg.repeat, &n); + if (m == 1 && cfg.repeat == TXTP_GROUP_REPEAT) { + line += n; + } + + + parse_config(&cfg.group_config, line); + + //;VGM_LOG("TXTP: parsed group %i%c%i%c\n",cfg.position+1,cfg.type,cfg.count,cfg.repeat); + + /* add final group */ + { + /* resize in steps if not enough */ + if (txtp->group_count+1 > txtp->group_max) { + txtp_group *temp_group; + + txtp->group_max += 5; + temp_group = realloc(txtp->group, sizeof(txtp_group) * txtp->group_max); + if (!temp_group) goto fail; + txtp->group = temp_group; + } + + /* new group */ + txtp->group[txtp->group_count] = cfg; /* memcpy */ + + txtp->group_count++; + } + + return 1; +fail: + return 0; +} + + +static int add_entry(txtp_header * txtp, char *filename, int is_default) { + int i; + txtp_entry cfg = {0}; + + + //;VGM_LOG("TXTP: filename=%s\n", filename); + + /* parse filename: file.ext#(commands) */ + { + char *config; + + if (is_default) { + config = filename; /* multiple commands without filename */ + } + else { + /* find config start (filenames and config can contain multiple dots and #, + * so this may be fooled by certain patterns of . and #) */ + config = strchr(filename, '.'); /* first dot (may be a false positive) */ + if (!config) /* extensionless */ + config = filename; + config = strchr(config, '#'); /* next should be config */ + if (!config) /* no config */ + config = NULL; + } + + parse_config(&cfg, config); + } + + + clean_filename(filename); + //;VGM_LOG("TXTP: clean filename='%s'\n", filename); + + /* config that applies to all files */ + if (is_default) { + txtp->default_entry_set = 1; + add_config(&txtp->default_entry, &cfg, NULL); + return 1; + } + + /* add final entry */ + for (i = cfg.range_start; i < cfg.range_end; i++){ + txtp_entry *current; + + /* resize in steps if not enough */ + if (txtp->entry_count+1 > txtp->entry_max) { + txtp_entry *temp_entry; + + txtp->entry_max += 5; + temp_entry = realloc(txtp->entry, sizeof(txtp_entry) * txtp->entry_max); + if (!temp_entry) goto fail; + txtp->entry = temp_entry; + } + + /* new entry */ + current = &txtp->entry[txtp->entry_count]; + memset(current,0, sizeof(txtp_entry)); + cfg.subsong = (i+1); + + add_config(current, &cfg, filename); + + txtp->entry_count++; + } + + return 1; +fail: + return 0; +} + +/* ************************************************************************ */ + +static int is_substring(const char * val, const char * cmp) { + int n; + char subval[TXTP_LINE_MAX] = {0}; + + /* read string without trailing spaces or comments/commands */ + if (sscanf(val, " %s%n[^ #\t\r\n]%n", subval, &n, &n) != 1) + return 0; + + if (0 != strcmp(subval,cmp)) + return 0; + return n; +} + +static int parse_num(const char * val, uint32_t * out_value) { + int hex = (val[0]=='0' && val[1]=='x'); + if (sscanf(val, hex ? "%x" : "%u", out_value) != 1) + goto fail; + + return 1; +fail: + return 0; +} + +static int parse_keyval(txtp_header * txtp, const char * key, const char * val) { + //;VGM_LOG("TXTP: key=val '%s'='%s'\n", key,val); + + + if (0==strcmp(key,"loop_start_segment")) { + if (!parse_num(val, &txtp->loop_start_segment)) goto fail; + } + else if (0==strcmp(key,"loop_end_segment")) { + if (!parse_num(val, &txtp->loop_end_segment)) goto fail; + } + else if (0==strcmp(key,"mode")) { + if (is_substring(val,"layers")) { + txtp->is_segmented = 0; + txtp->is_layered = 1; + } + else if (is_substring(val,"segments")) { + txtp->is_segmented = 1; + txtp->is_layered = 0; + } + else if (is_substring(val,"mixed")) { + txtp->is_segmented = 0; + txtp->is_layered = 0; + } + else { + goto fail; + } + } + else if (0==strcmp(key,"loop_mode")) { + if (is_substring(val,"keep")) { + txtp->is_loop_keep = 1; + } + else { + goto fail; + } + } + else if (0==strcmp(key,"commands")) { + char val2[TXTP_LINE_MAX]; + strcpy(val2, val); /* copy since val is modified here but probably not important */ + if (!add_entry(txtp, val2, 1)) goto fail; + } + else if (0==strcmp(key,"group")) { + char val2[TXTP_LINE_MAX]; + strcpy(val2, val); /* copy since val is modified here but probably not important */ + if (!add_group(txtp, val2)) goto fail; + + } + else { + goto fail; + } + + return 1; +fail: + VGM_LOG("TXTP: error while parsing key=val '%s'='%s'\n", key,val); + return 0; +} + +static txtp_header* parse_txtp(STREAMFILE* streamFile) { + txtp_header* txtp = NULL; + off_t txt_offset = 0x00; + off_t file_size = get_streamfile_size(streamFile); + + + txtp = calloc(1,sizeof(txtp_header)); + if (!txtp) goto fail; + + /* defaults */ + txtp->is_segmented = 1; + + + /* skip BOM if needed */ + if (file_size > 0 && + ((uint16_t)read_16bitLE(0x00, streamFile) == 0xFFFE || (uint16_t)read_16bitLE(0x00, streamFile) == 0xFEFF)) + txt_offset = 0x02; + + /* read and parse lines */ + while (txt_offset < file_size) { + char line[TXTP_LINE_MAX]; + char key[TXTP_LINE_MAX] = {0}, val[TXTP_LINE_MAX] = {0}; /* at least as big as a line to avoid overflows (I hope) */ + char filename[TXTP_LINE_MAX] = {0}; + int ok, bytes_read, line_ok; + + bytes_read = read_line(line, sizeof(line), txt_offset, streamFile, &line_ok); + if (!line_ok) goto fail; + + txt_offset += bytes_read; + + /* get key/val (ignores lead/trail spaces, # may be commands or comments) */ + ok = sscanf(line, " %[^ \t#=] = %[^\t\r\n] ", key,val); + if (ok == 2) { /* key=val */ + if (!parse_keyval(txtp, key, val)) /* read key/val */ + goto fail; + continue; + } + + /* must be a filename (only remove spaces from start/end, as filenames con contain mid spaces/#/etc) */ + ok = sscanf(line, " %[^\t\r\n] ", filename); + if (ok != 1) /* not a filename either */ + continue; + if (filename[0] == '#') + continue; /* simple comment */ + + /* filename with config */ + if (!add_entry(txtp, filename, 0)) + goto fail; + } + + /* mini-txth: if no entries are set try with filename, ex. from "song.ext#3.txtp" use "song.ext#3" + * (it's possible to have default "commands" inside the .txtp plus filename+config) */ + if (txtp->entry_count == 0) { + char filename[PATH_LIMIT] = {0}; + + get_streamfile_basename(streamFile, filename, sizeof(filename)); + + add_entry(txtp, filename, 0); + } + + + return txtp; +fail: + clean_txtp(txtp, 1); + return NULL; +} + +static void clean_txtp(txtp_header* txtp, int fail) { + int i, start; + + if (!txtp) + return; + + /* returns first vgmstream on success so it's not closed */ + start = fail ? 0 : 1; + + for (i = start; i < txtp->vgmstream_count; i++) { + close_vgmstream(txtp->vgmstream[i]); + } + + free(txtp->vgmstream); + free(txtp->group); + free(txtp->entry); + free(txtp); +}