Lameguy64 / PSn00bSDK

The most powerful open source SDK for the PS1 (as far as open source PS1 SDKs go). Not recommended for beginner use.
Other
819 stars 66 forks source link

Audio streaming sample breaking on custom VAG file #77

Closed GlaireDaggers closed 9 months ago

GlaireDaggers commented 9 months ago

Decided to give the samples a spin today, I tried the VAG streaming sample in Duckstation which indeed works, but then when I substitute my own custom VAG file, it totally blows up. I'm lucky if I can hear anything more than a second or so of audio before it just halts playback altogether, occasionally I've had it produce short bursts of screeching sounds before halting, or even interleaved playback with what sounds like portions of the PSX startup jingle?

I've attached the VAG file as well as the source WAV that was used to encode it. I used psxavenc to encode it (with psxavenc -t vagi -f 44100 -c 2 -i 4096 title_test.wav title.vag).

The included VAG file plays perfectly, so I'm really at a loss as to what I could be doing wrong. It seems like maybe a format problem, but adding some more debug info confirms it's the same samplerate, same number of channels, same chunk size, etc... I also checked with vgmstream and it has no problems decoding my VAG file so 🤷‍♀️

title_test.zip

spicyjpeg commented 9 months ago

This is something I think I forgot to document in the code. The .vag file must have SPU loop flags set at the end of each interleaved chunk in order for the example to work properly, otherwise the channels allocated to the stream will end up jumping to other data in SPU RAM (hence the startup sound left into RAM by the BIOS playing) and never trigger the interrupt handler that loads the next chunk. psxavenc can add those flags automatically with the -L option, so that should fix it.

GlaireDaggers commented 9 months ago

Oooh that makes sense I think, got it

EDIT: OK yeah, that works perfectly now. If I may hijack my own thread before I close it: looking at the playback code, particularly the logic at line 214 in main.c - theoretically, would it be possible to instead of looping back to the start of the file, redirect playback to a totally different file on disk? or to a chunk in the middle of the song instead? Would there be any SPU flags or state I need to be careful about in that case? Use case I have in mind would be having a song that has an "intro" portion that only plays once and then a looping portion afterwards.

EDIT 2: Dang, first attempt at just naively checking if it ran past the end and setting the read pos back to a loop address. It almost works perfectly. There's just a little "glitch" at the very end, like the last buffer of audio gets repeated before it loops. I wonder why it doesn't do this when it loops back to the very start instead... NVM figured it out. Tool I wrote to produce VAG files for this scenario was not calculating padding to align the loop point correctly, so I think it might've been looping to the middle of a chunk, whoops. Though now the file ends with a tiny gap, so it seems my tool is also not properly stretching the file to fit the loop portion on exact chunk boundaries... that'll be a thing for tomorrow me to fix. Maybe I'll post a demo when I get it all working.

EDIT 3: HECK YEAH, fixed my tool and got it working! Seamless loop points and all :D

spicyjpeg commented 9 months ago

Glad to hear you got it working. Looping should indeed be fairly trivial if the loop point is chunk-aligned, less so if you have to jump to the middle of an interleaved chunk in order to make it work. A possible workaround is lowering chunk size, which will however have the side effect of increasing CPU load as SPU IRQs will be more frequent.

would it be possible to instead of looping back to the start of the file, redirect playback to a totally different file on disk?

Sure. As you've probably noticed already, the code that handles streaming/buffer management and runs in the IRQ handler is completely separate from the functions that feed chunks from the disc into the buffer in main RAM, with its API being documented here. As long as the two streams have the same chunk size and sample rate, the function in charge of filling the buffer from the main thread (feed_stream() in the example) can simply stop filling it up, seek to another file and resume filling it up with chunks from the new file. The switch will be seamless since the stream is never actually interrupted.

The buffer must of course be large enough to cover the entire duration of the seek, which will vary depending on the physical distance between the two files on the disc and how worn out the console's laser is but is generally in the 100-300ms range. You'll probably want to perform the filesystem lookup and read the .vag header in advance, as CdSearchFile() is blocking and extremely slow, or alternatively design a custom archive format that minimizes the number of seeks required to look up a file (by e.g. storing the starting address and metadata of each stream in a single sector that can be cached into RAM on startup, thus removing the need for separate .vag headers). If you need help with that (or anything else really), feel free to ask on the PSX.Dev Discord server.

GlaireDaggers commented 9 months ago

Gotcha. The approach I've ended up taking is using a single file, and making my content pipeline tool be responsible for 1.) inserting padding at the start of the song to make the loop point align on an exact chunk boundary, 2.) slightly stretch/resample the file to make the looped section line up on exact chunk boundaries, and 3.) report the lba offset corresponding to the new loop point so I can plug it into my program. At some point might write this into a file somewhere or maybe even just use non standard vag headers.

Going to go ahead and close this though since my questions have been answered. Thank you!