Files
Sabarivasan fe9e7b09e4 Fix(Audio): stopped mid-stream MP3 resets that cut speech (#1101)
EN-US

- Fixed words cut from the German narration in the opening cinematic: a byte pattern inside audio data was read as an FGH tag and reset the stream mid-speech
- Fixed OFL headers being parsed on every frame; they are now read only at the start of a stream, and the gapless state resets with the decoder
- Added an mp3-test case with a fake FGH signature inside an audio frame
- Credited Sabari-Vasan-SM (#1091)

---

PT-BR

- Corrigidas as palavras cortadas da narração em alemão na cinemática de abertura: um padrão de bytes dentro dos dados de áudio era lido como tag FGH e reiniciava o stream no meio da fala
- Corrigida a leitura de cabeçalhos OFL em todo quadro; agora eles só são lidos no início do stream, e o estado gapless é reiniciado junto com o decodificador
- Adicionado um caso no mp3-test com uma assinatura FGH falsa dentro de um quadro de áudio
- Creditado Sabari-Vasan-SM (#1091)
2026-11-01 16:40:34 -03:00

105 lines
5.3 KiB
C

/* MP3 through libSceAjm: header/gapless parsing and decoding jobs (tests/data/sine.mp3:
* 0.5 s of 440 Hz, 44.1 kHz stereo, made with ffmpeg -c:a libmp3lame -id3v2_version 0; the
* encoder field of its tag frame is renamed from "Lavc63.1" to "LAME3.10", the only one the
* SDK reads). */
#undef NDEBUG /* the checks are asserts; release builds define NDEBUG */
#include "../src/runtime_ajm.c"
#include <assert.h>
uintptr_t runtime_lookup(const RuntimeExport *table, size_t count, const char *nid) { (void)table; (void)count; (void)nid; return 0; }
static unsigned char *load(const char *path, uint32_t *size) {
FILE *f=fopen(path,"rb");
assert(f);
static unsigned char data[1<<16];
*size=(uint32_t)fread(data,1,sizeof(data),f);
fclose(f);
return data;
}
static uint64_t frame_size_of(const unsigned char *data) {
Mp3Frame f; assert(runtime_mp3_parse(data,4,0,1,&f)==0); return f.frame_size;
}
/* One decoding job; returns the sideband result and fills stream/gapless. */
static int32_t run(uint32_t ctx, uint32_t instance, const void *in, uint64_t in_size, void *out, uint64_t out_size,
uint64_t flags, SidebandStream *stream, SidebandGapless *gapless) {
unsigned char batch[256], side[64]={0};
unsigned char *end=job_run(batch,instance,flags,(void *)in,in_size,out,out_size,side,sizeof(side),NULL);
uint32_t id;
assert(ajm_batch_start(ctx,batch,(uint32_t)(end-batch),0,NULL,&id)==0);
assert(ajm_batch_wait(ctx,id,0,NULL)==0);
SidebandResult r; memcpy(&r,side,8);
if (stream) memcpy(stream,side+8,16);
if (gapless) memcpy(gapless,side+24,8);
return r.result;
}
int main(int argc, char **argv) {
uint32_t size;
const unsigned char *mp3=load(argc>1 ? argv[1] : "tests/data/sine.mp3",&size);
Mp3Frame frame;
assert(ajm_mp3_parse_frame(mp3,size,1,&frame)==0);
assert(frame.sample_rate==44100 && frame.num_channels==2 && frame.samples_per_channel==1152);
assert(frame.ofl_type==1 && frame.total_samples==22050 && frame.encoder_delay>1152);
assert(ajm_mp3_parse_frame((const unsigned char *)"\0\0\0\0",4,1,&frame)==ERR_INVALID_PARAMETER);
assert(ajm_mp3_parse_frame(mp3,3,1,&frame)==ERR_INVALID_PARAMETER);
uint32_t ctx, instance;
assert(ajm_initialize(0,&ctx)==0 && ajm_module_register(ctx,0,0)==0);
assert(ajm_instance_create(ctx,0,1|(FORMAT_S16<<7),&instance)==0 && ajm_instance_codec(instance)==0);
/* The whole stream in one multi-frame job: exactly the gapless samples come out. */
static int16_t pcm[44100*2];
SidebandStream stream; SidebandGapless gapless;
uint64_t multi=UINT64_C(1)<<12|UINT64_C(1)<<47|UINT64_C(1)<<45;
int32_t r=run(ctx,instance,mp3,size,pcm,sizeof(pcm),multi,&stream,&gapless);
assert((r & ~RESULT_PARTIAL_INPUT)==0);
assert(stream.output_written==22050*2*2 && stream.total_decoded_samples==22050);
assert(stream.input_consumed>0 && (uint32_t)stream.input_consumed<=size);
int16_t peak=0;
for (int i=0;i<22050*2;++i) if (pcm[i]>peak) peak=pcm[i];
/* Reference: ffmpeg -i sine.mp3 -f s16le gives 22050 samples, peak 2761, [0]=10, [1000]=-196
* (gapless trimming at the same sample; +-1 for float to s16 rounding). */
assert(peak>=2759 && peak<=2763 && abs(pcm[0]-10)<=1 && abs(pcm[1000]+196)<=1);
/* Reset, then a small output: the skipped tag frame fits, the next frame does not. */
unsigned char control[8];
unsigned char batch[128];
unsigned char *end=job_control(batch,instance,UINT64_C(1)<<13,control,0,control,sizeof(control),NULL);
uint32_t id; assert(ajm_batch_start(ctx,batch,(uint32_t)(end-batch),0,NULL,&id)==0 && ajm_batch_wait(ctx,id,0,NULL)==0);
int16_t small[16];
r=run(ctx,instance,mp3,size,small,sizeof(small),multi,&stream,NULL);
assert(r==RESULT_NOT_ENOUGH_ROOM && stream.output_written==0 && stream.input_consumed==(int32_t)frame_size_of(mp3));
/* False-positive FGH byte pattern in an audio frame must not trigger OFL or drop samples (#1091). */
static unsigned char fake_mp3[1<<16];
memcpy(fake_mp3,mp3,size);
/* In frame 10 (offset 4178, size 418), inject 0xB4 indicator with matching CRC-8 at byte 390. */
unsigned char fgh[10]={0xB4, 0x03, 0xE8, 0x00, 0x00, 0x56, 0x22, 0x00, 0x00, 0};
uint8_t crc=0xFF;
for (int i=0;i<9;++i) for (int j=7;j>=0;--j) crc=(uint8_t)(((crc>>7)&1)!=((fgh[i]>>j)&1) ? crc*2 : (crc*2)^0x45);
fgh[9]=crc;
memcpy(fake_mp3+4178+390,fgh,10);
Mp3Frame audio_frame;
assert(ajm_mp3_parse_frame(fake_mp3+4178,418,1,&audio_frame)==0);
assert(audio_frame.ofl_type==0 && audio_frame.encoder_delay==0);
memset(control,0,sizeof(control));
end=job_control(batch,instance,UINT64_C(1)<<13,control,0,control,sizeof(control),NULL);
assert(ajm_batch_start(ctx,batch,(uint32_t)(end-batch),0,NULL,&id)==0 && ajm_batch_wait(ctx,id,0,NULL)==0);
r=run(ctx,instance,fake_mp3,size,pcm,sizeof(pcm),multi,&stream,&gapless);
assert((r & ~RESULT_PARTIAL_INPUT)==0);
assert(stream.output_written==22050*2*2 && stream.total_decoded_samples==22050);
/* Garbage is a codec error result, not a crash. */
static const unsigned char junk[512]={0x12,0x34};
r=run(ctx,instance,junk,sizeof(junk),pcm,sizeof(pcm),UINT64_C(1)<<47,&stream,NULL);
assert(r & RESULT_CODEC_ERROR);
assert(ajm_instance_destroy(ctx,instance)==0 && ajm_finalize(ctx)==0);
puts("mp3 test: ok");
return 0;
}