From ef6530070e18ed89a43fdb2e551244154c7aac61 Mon Sep 17 00:00:00 2001 From: Christopher Snowhill Date: Sun, 30 May 2021 14:33:51 -0700 Subject: [PATCH] Updated VGMStream to r1050-3731-g136a7f7c --- .../vgmstream/src/coding/speex_decoder.c | 6 +- Frameworks/vgmstream/vgmstream/src/meta/awb.c | 13 +- Frameworks/vgmstream/vgmstream/src/meta/isb.c | 53 ++-- Frameworks/vgmstream/vgmstream/src/meta/mp4.c | 241 +++++++++++++----- .../vgmstream/vgmstream/src/meta/ogg_vorbis.c | 3 +- .../vgmstream/vgmstream/src/meta/wwise.c | 11 +- 6 files changed, 238 insertions(+), 89 deletions(-) diff --git a/Frameworks/vgmstream/vgmstream/src/coding/speex_decoder.c b/Frameworks/vgmstream/vgmstream/src/coding/speex_decoder.c index be8b4f25e..0b908ad65 100644 --- a/Frameworks/vgmstream/vgmstream/src/coding/speex_decoder.c +++ b/Frameworks/vgmstream/vgmstream/src/coding/speex_decoder.c @@ -179,8 +179,10 @@ void free_speex(speex_codec_data* data) { if (!data) return; - speex_decoder_destroy(data->state); - speex_bits_destroy(&data->bits); + if (data->state) { + speex_decoder_destroy(data->state); + speex_bits_destroy(&data->bits); + } free(data->samples); free(data); diff --git a/Frameworks/vgmstream/vgmstream/src/meta/awb.c b/Frameworks/vgmstream/vgmstream/src/meta/awb.c index 5d3293d94..ece4b8f48 100644 --- a/Frameworks/vgmstream/vgmstream/src/meta/awb.c +++ b/Frameworks/vgmstream/vgmstream/src/meta/awb.c @@ -1,7 +1,7 @@ #include "meta.h" #include "../coding/coding.h" -typedef enum { ADX, HCA, VAG, RIFF, CWAV, DSP, CWAC } awb_type; +typedef enum { ADX, HCA, VAG, RIFF, CWAV, DSP, CWAC, M4A } awb_type; static void load_awb_name(STREAMFILE* sf, STREAMFILE* sf_acb, VGMSTREAM* vgmstream, int waveid); @@ -120,6 +120,11 @@ VGMSTREAM* init_vgmstream_awb_memory(STREAMFILE* sf, STREAMFILE* sf_acb) { type = CWAC; extension = "dsp"; } + else if (read_u32be(subfile_offset+0x00,sf) == 0x00000018 && + read_u32be(subfile_offset+0x04,sf) == 0x66747970) { /* chunk size + "ftyp" (type 19) */ + type = M4A; + extension = "m4a"; + } else { VGM_LOG("AWB: unknown codec\n"); goto fail; @@ -158,6 +163,12 @@ VGMSTREAM* init_vgmstream_awb_memory(STREAMFILE* sf, STREAMFILE* sf_acb) { vgmstream = init_vgmstream_dsp_cwac(temp_sf); if (!vgmstream) goto fail; break; +#ifdef VGM_USE_FFMPEG + case M4A: /* Imperial SaGa Eclipse (Browser) */ + vgmstream = init_vgmstream_mp4_aac_ffmpeg(temp_sf); + if (!vgmstream) goto fail; + break; +#endif default: goto fail; } diff --git a/Frameworks/vgmstream/vgmstream/src/meta/isb.c b/Frameworks/vgmstream/vgmstream/src/meta/isb.c index c98cbf1a6..ed489dcdc 100644 --- a/Frameworks/vgmstream/vgmstream/src/meta/isb.c +++ b/Frameworks/vgmstream/vgmstream/src/meta/isb.c @@ -3,10 +3,10 @@ /* .ISB - Creative ISACT (Interactive Spatial Audio Composition Tools) middleware [Psychonauts (PC), Mass Effect (multi)] */ -VGMSTREAM * init_vgmstream_isb(STREAMFILE *sf) { - VGMSTREAM * vgmstream = NULL; - off_t start_offset = 0, name_offset = 0; - size_t stream_size = 0, name_size = 0; +VGMSTREAM* init_vgmstream_isb(STREAMFILE* sf) { + VGMSTREAM* vgmstream = NULL; + off_t start_offset = 0, name_offset = 0, nfld_offset = 0; + size_t stream_size = 0, name_size = 0, nfld_size = 0; int loop_flag = 0, channels = 0, sample_rate = 0, codec = 0, pcm_bytes = 0, bps = 0; int total_subsongs, target_subsong = sf->stream_index; uint32_t (*read_u32me)(off_t,STREAMFILE*); @@ -50,9 +50,6 @@ VGMSTREAM * init_vgmstream_isb(STREAMFILE *sf) { uint32_t chunk_size = read_u32me(offset + 0x04,sf); offset += 0x08; - //VGM_LOG("offset=%lx, sub=%c%c%c%c\n", offset,(chunk_type>>24 & 0xFF), (chunk_type>>16 & 0xFF), (chunk_type>>8 & 0xFF), (chunk_type & 0xFF)); - - switch(chunk_type) { case 0x4C495354: /* "LIST" */ if (read_u32ce(offset, sf) == get_id32be("samp")) { @@ -65,6 +62,8 @@ VGMSTREAM * init_vgmstream_isb(STREAMFILE *sf) { } else if (read_u32ce(offset, sf) == get_id32be("fldr")) { /* subfolder with another LIST inside, for example "stingers" > N smpl (seen in some music_bank) */ + off_t current_nfld_offset = 0; + size_t current_nfld_size = 0; suboffset = offset + 0x04; submax_offset = offset + chunk_size; @@ -73,7 +72,12 @@ VGMSTREAM * init_vgmstream_isb(STREAMFILE *sf) { uint32_t subchunk_size = read_u32me(suboffset + 0x04,sf); suboffset += 0x08; - if (subchunk_type == get_id32be("LIST")) { + if (subchunk_type == get_id32be("titl")) { + /* should go first in fldr*/ + current_nfld_offset = suboffset; + current_nfld_size = subchunk_size; + } + else if (subchunk_type == get_id32be("LIST")) { uint32_t subsubchunk_type = read_u32ce(suboffset, sf); if (subsubchunk_type == get_id32be("samp")) { @@ -81,6 +85,8 @@ VGMSTREAM * init_vgmstream_isb(STREAMFILE *sf) { if (target_subsong == total_subsongs && header_offset == 0) { header_offset = suboffset; header_size = chunk_size; + nfld_offset = current_nfld_offset; + nfld_size = current_nfld_size; } } else if (subsubchunk_type == get_id32be("fldr")) { @@ -101,9 +107,7 @@ VGMSTREAM * init_vgmstream_isb(STREAMFILE *sf) { break; } - //if (offset + chunk_size+0x01 <= max_offset && chunk_size % 0x02) - // chunk_size += 0x01; - offset += chunk_size; + offset += chunk_size; /* no chunk 1-byte padding */ } if (target_subsong < 0 || target_subsong > total_subsongs || total_subsongs < 1) goto fail; @@ -157,8 +161,6 @@ VGMSTREAM * init_vgmstream_isb(STREAMFILE *sf) { break; } - //if (offset + chunk_size+0x01 <= max_offset && chunk_size % 0x02) - // chunk_size += 0x01; offset += chunk_size; } @@ -180,10 +182,27 @@ VGMSTREAM * init_vgmstream_isb(STREAMFILE *sf) { vgmstream->num_streams = total_subsongs; vgmstream->stream_size = stream_size; - if (name_offset) { /* UTF16 but only uses lower bytes */ - if (name_size > STREAM_NAME_SIZE) - name_size = STREAM_NAME_SIZE; - read_string_utf16(vgmstream->stream_name,name_size, name_offset, sf, big_endian); + if (name_offset) { + /* UTF16 but only uses lower bytes */ + char name[256]; + char nfld[256]; + + /* should read string or set '\0' is no size is set/incorrect */ + if (name_size >= sizeof(name)) + name_size = sizeof(name) - 1; + read_string_utf16(name, name_size, name_offset, sf, big_endian); + + if (nfld_size >= sizeof(nfld)) + nfld_size = sizeof(nfld) - 1; + read_string_utf16(nfld, nfld_size, nfld_offset, sf, big_endian); + + if (nfld[0] && name[0]) { + snprintf(vgmstream->stream_name,STREAM_NAME_SIZE, "%s/%s", nfld, name); + } + else if (name[0]) { + snprintf(vgmstream->stream_name,STREAM_NAME_SIZE, "%s", name); + } + /* there is also a "titl" for the bank, but it's just the filename so probably unwanted */ } switch(codec) { diff --git a/Frameworks/vgmstream/vgmstream/src/meta/mp4.c b/Frameworks/vgmstream/vgmstream/src/meta/mp4.c index c16ec2aaa..473845c63 100644 --- a/Frameworks/vgmstream/vgmstream/src/meta/mp4.c +++ b/Frameworks/vgmstream/vgmstream/src/meta/mp4.c @@ -60,13 +60,13 @@ int mp4_file_close( void* handle ) MP4FileProvider mp4_file_provider = { mp4_file_open, mp4_file_seek, mp4_file_read, mp4_file_write, mp4_file_close, mp4_file_get_size }; #ifdef VGM_USE_FDKAAC -VGMSTREAM * init_vgmstream_mp4_aac_offset(STREAMFILE *streamFile, uint64_t start, uint64_t size); +VGMSTREAM * init_vgmstream_mp4_aac_offset(STREAMFILE *sf, uint64_t start, uint64_t size); -VGMSTREAM * init_vgmstream_mp4_aac(STREAMFILE *streamFile) { - return init_vgmstream_mp4_aac_offset( streamFile, 0, streamFile->get_size(streamFile) ); +VGMSTREAM * init_vgmstream_mp4_aac(STREAMFILE *sf) { + return init_vgmstream_mp4_aac_offset( sf, 0, sf->get_size(sf) ); } -VGMSTREAM * init_vgmstream_mp4_aac_offset(STREAMFILE *streamFile, uint64_t start, uint64_t size) { +VGMSTREAM * init_vgmstream_mp4_aac_offset(STREAMFILE *sf, uint64_t start, uint64_t size) { VGMSTREAM * vgmstream = NULL; char filename[PATH_LIMIT]; @@ -81,7 +81,7 @@ VGMSTREAM * init_vgmstream_mp4_aac_offset(STREAMFILE *streamFile, uint64_t start if ( !aac_file ) goto fail; - aac_file->if_file.streamfile = streamFile; + aac_file->if_file.streamfile = sf; aac_file->if_file.start = start; aac_file->if_file.offset = start; aac_file->if_file.size = size; @@ -124,9 +124,9 @@ VGMSTREAM * init_vgmstream_mp4_aac_offset(STREAMFILE *streamFile, uint64_t start aac_file->samples_per_frame = stream_info->frameSize; aac_file->samples_discard = 0; - streamFile->get_name( streamFile, filename, sizeof(filename) ); + sf->get_name( sf, filename, sizeof(filename) ); - aac_file->if_file.streamfile = streamFile->open(streamFile, filename, STREAMFILE_DEFAULT_BUFFER_SIZE); + aac_file->if_file.streamfile = sf->open(sf, filename, STREAMFILE_DEFAULT_BUFFER_SIZE); if (!aac_file->if_file.streamfile) goto fail; vgmstream = allocate_vgmstream( stream_info->numChannels, 1 ); @@ -162,64 +162,64 @@ fail: #ifdef VGM_USE_FFMPEG -static int find_atom_be(STREAMFILE *streamFile, uint32_t atom_id, off_t start_offset, off_t *out_atom_offset, size_t *out_atom_size); +typedef struct { + int32_t num_samples; + int loop_flag; + int32_t loop_start; + int32_t loop_end; + int32_t encoder_delay; +} mp4_header; + +static void parse_mp4(STREAMFILE* sf, mp4_header* mp4); -VGMSTREAM * init_vgmstream_mp4_aac_ffmpeg(STREAMFILE *streamFile) { - VGMSTREAM * vgmstream = NULL; +VGMSTREAM* init_vgmstream_mp4_aac_ffmpeg(STREAMFILE* sf) { + VGMSTREAM* vgmstream = NULL; off_t start_offset = 0; - int loop_flag = 0; - int32_t loop_start_sample = 0, loop_end_sample = 0; - size_t filesize; - off_t atom_offset; - size_t atom_size; - - ffmpeg_codec_data *ffmpeg_data = NULL; + mp4_header mp4 = {0}; + size_t file_size; + ffmpeg_codec_data* ffmpeg_data = NULL; - /* check extension, case insensitive */ - /* .bin: Final Fantasy Dimensions (iOS), Final Fantasy V (iOS) - * .msd: UNO (iOS) */ - if (!check_extensions(streamFile,"mp4,m4a,m4v,lmp4,bin,msd")) + /* checks */ + /* .bin: Final Fantasy Dimensions (iOS), Final Fantasy V (iOS) + * .msd: UNO (iOS) */ + if (!check_extensions(sf,"mp4,m4a,m4v,lmp4,bin,lbin,msd")) goto fail; - filesize = streamFile->get_size(streamFile); - - /* check header */ - if ( read_32bitBE(start_offset+0x04,streamFile) != 0x66747970) /* atom size @0x00 + "ftyp" @0x04 */ + if ((read_u32be(0x00,sf) & 0xFFFFFF00) != 0) /* first atom BE size (usually ~0x18) */ + goto fail; + if (!is_id32be(0x04,sf, "ftyp")) goto fail; - ffmpeg_data = init_ffmpeg_offset(streamFile, start_offset, filesize); - if ( !ffmpeg_data ) goto fail; + file_size = get_streamfile_size(sf); + + ffmpeg_data = init_ffmpeg_offset(sf, start_offset, file_size); + if (!ffmpeg_data) goto fail; + + parse_mp4(sf, &mp4); - /* Tales of Hearts iOS has loop info in the first "free" atom */ - if (find_atom_be(streamFile, 0x66726565, start_offset, &atom_offset, &atom_size)) { /* "free" */ - if (read_32bitBE(atom_offset,streamFile) == 0x4F700002 - && (atom_size == 0x38 || atom_size == 0x40)) { /* make sure it's ToHr "free" */ - /* 0x00: id? 0x04/8: s_rate; 0x10: num_samples (without padding, same as FFmpeg's) */ - /* 0x14/18/1c: 0x238/250/278? 0x20: ? 0x24: start_pad */ - loop_flag = read_32bitBE(atom_offset+0x28,streamFile); - if (loop_flag) { /* atom ends if no loop flag */ - loop_start_sample = read_32bitBE(atom_offset+0x2c,streamFile); - loop_end_sample = read_32bitBE(atom_offset+0x30,streamFile); - } - } - } /* build the VGMSTREAM */ - vgmstream = allocate_vgmstream(ffmpeg_data->channels,loop_flag); + vgmstream = allocate_vgmstream(ffmpeg_data->channels, mp4.loop_flag); if (!vgmstream) goto fail; + + vgmstream->meta_type = meta_MP4; + vgmstream->sample_rate = ffmpeg_data->sampleRate; + vgmstream->num_samples = mp4.num_samples; + if (vgmstream->num_samples == 0) + vgmstream->num_samples = ffmpeg_data->totalSamples; + vgmstream->loop_start_sample = mp4.loop_start; + vgmstream->loop_end_sample = mp4.loop_end; + vgmstream->codec_data = ffmpeg_data; vgmstream->coding_type = coding_FFmpeg; vgmstream->layout_type = layout_none; - vgmstream->meta_type = meta_MP4; - - vgmstream->sample_rate = ffmpeg_data->sampleRate; - vgmstream->num_samples = ffmpeg_data->totalSamples; - vgmstream->loop_start_sample = loop_start_sample; - vgmstream->loop_end_sample = loop_end_sample; + vgmstream->num_streams = ffmpeg_data->streamCount; /* may contain N tracks */ vgmstream->channel_layout = ffmpeg_get_channel_layout(vgmstream->codec_data); + if (mp4.encoder_delay) + ffmpeg_set_skip_samples(vgmstream->codec_data, mp4.encoder_delay); return vgmstream; @@ -232,31 +232,138 @@ fail: return NULL; } -/* Almost the same as streamfile.c's find_chunk but for "atom" chunks, which have chunk_size first because Apple, returns 0 on failure */ -static int find_atom_be(STREAMFILE *streamFile, uint32_t atom_id, off_t start_offset, off_t *out_atom_offset, size_t *out_atom_size) { - size_t filesize; - off_t current_atom = start_offset; - int full_atom_size = 1; - int size_big_endian = 1; +/* read useful MP4 chunks */ +static void parse_mp4(STREAMFILE* sf, mp4_header* mp4) { + off_t offset, suboffset, max_offset, max_suboffset; - filesize = get_streamfile_size(streamFile); - /* read chunks */ - while (current_atom < filesize) { - off_t chunk_size = size_big_endian ? - read_32bitBE(current_atom+0,streamFile) : - read_32bitLE(current_atom+0,streamFile); - uint32_t chunk_type = read_32bitBE(current_atom+4,streamFile); - if (chunk_type == atom_id) { - if (out_atom_size) *out_atom_size = chunk_size; - if (out_atom_offset) *out_atom_offset = current_atom+8; - return 1; + /* MOV format chunks, called "atoms", size goes first because Apple */ + offset = 0x00; + max_offset = get_streamfile_size(sf); + while (offset < max_offset) { + uint32_t size = read_u32be(offset + 0x00,sf); + uint32_t type = read_u32be(offset + 0x04,sf); + //offset += 0x08; + + /* just in case */ + if (size == 0) + break; + + switch(type) { + case 0x66726565: /* "free" */ + /* Tales of Hearts R (iOS) has loop info in the first "free" atom */ + if (read_u32be(offset + 0x08,sf) == 0x4F700002 && (size == 0x38 || size == 0x40)) { + /* 0x00: id / "Op" */ + /* 0x02: channels */ + /* 0x04/8: sample rate */ + /* 0x0c: null? */ + /* 0x10: num_samples (without padding, same as FFmpeg's) */ + /* 0x14/18/1c/20: offsets to stream info (stts/stsc/stsz/stco) */ + mp4->encoder_delay = read_u32be(offset + 0x08 + 0x24,sf); /* Apple's 2112 */ + mp4->loop_flag = read_u32be(offset + 0x08 + 0x28,sf); + if (mp4->loop_flag) { /* atom ends if no loop flag */ + mp4->loop_start = read_u32be(offset + 0x08 + 0x2c,sf); + mp4->loop_end = read_u32be(offset + 0x08 + 0x30,sf); + } + /* could stop reading since FFmpeg will too */ + } + break; + + case 0x6D6F6F76: { /* "moov" (header) */ + suboffset = offset += 0x08; + max_suboffset = offset + size; + while (suboffset < max_suboffset) { + uint32_t subsize = read_u32be(suboffset + 0x00,sf); + uint32_t subtype = read_u32be(suboffset + 0x04,sf); + + /* padded in ToRR */ + if (subsize == 0) + break; + + switch(subtype) { + case 0x75647461: /* "udta" */ + /* CRI subchunk [Imperial SaGa Eclipse (Browser)] + * incidentally "moov" header comes after data ("mdat") in CRI's files */ + if (subsize >= 0x28 && is_id32be(suboffset + 0x08 + 0x04,sf, "criw")) { + off_t criw_offset = suboffset + 0x08 + 0x08; + + mp4->loop_start = read_s32be(criw_offset + 0x00,sf); + mp4->loop_end = read_s32be(criw_offset + 0x04,sf); + mp4->encoder_delay = read_s32be(criw_offset + 0x08,sf); /* Apple's 2112 */ + mp4->num_samples = read_s32be(criw_offset + 0x0c,sf); + mp4->loop_flag = (mp4->loop_end > 0); + /* next 2 fields are null */ + } + break; + + default: + break; + } + + suboffset += subsize; + } + + break; + } + + default: + break; } - current_atom += full_atom_size ? chunk_size : 4+4+chunk_size; + offset += size; /* atoms don't seem to need to padding byte, unlike RIFF */ } - - return 0; } +/* CRI's encryption info (for lack of a better place) [Final Fantasy Digital Card Game (Browser)] + * + * Like other CRI stuff their MP4 can be encrypted, from file's beginning (including headers). + * This is more or less how data is decrypted (supposedly, from decompilations), for reference: + */ +#if 0 +void criAacCodec_SetDecryptionKey(uint64_t keycode, uint16_t* key) { + if (!keycode) + return; + uint16_t k0 = 4 * ((keycode >> 0) & 0x0FFF) | 1; + uint16_t k1 = 2 * ((keycode >> 12) & 0x1FFF) | 1; + uint16_t k2 = 4 * ((keycode >> 25) & 0x1FFF) | 1; + uint16_t k3 = 2 * ((keycode >> 38) & 0x3FFF) | 1; + + key[0] = k0 ^ k1; + key[1] = k1 ^ k2; + key[2] = k2 ^ k3; + key[3] = ~k3; + + /* criatomexacb_generate_aac_decryption_key is slightly different, unsure which one is used: */ + //key[0] = k0 ^ k3; + //key[1] = k2 ^ k3; + //key[2] = k2 ^ k3; + //key[3] = ~k3; +} + +void criAacCodec_DecryptData(const uint16_t* key, uint8_t* data, uint32_t size) { + if (data_size) + return; + uint16_t seed0 = ~key[3]; + uint16_t seed1 = seed0 ^ key[2]; + uint16_t seed2 = seed1 ^ key[1]; + uint16_t seed3 = seed2 ^ key[0]; + + uint16_t xor = 2 * seed0 | 1; + uint16_t add = 2 * seed0 | 1; /* not seed1 */ + uint16_t mul = 4 * seed2 | 1; + + for (int i = 0; i < data_size; i++) { + + if (!(uint16_t)i) { /* every 0x10000, without modulo */ + mul = (4 * seed2 + seed3 * (mul & 0xFFFC)) & 0xFFFD | 1; + add = (2 * seed0 + seed1 * (add & 0xFFFE)) | 1; + } + xor = xor * mul + add; + + *data ^= (xor >> 8) & 0xFF; + ++data; + } +} +#endif + #endif diff --git a/Frameworks/vgmstream/vgmstream/src/meta/ogg_vorbis.c b/Frameworks/vgmstream/vgmstream/src/meta/ogg_vorbis.c index d85d80ac7..3df90c7a4 100644 --- a/Frameworks/vgmstream/vgmstream/src/meta/ogg_vorbis.c +++ b/Frameworks/vgmstream/vgmstream/src/meta/ogg_vorbis.c @@ -137,8 +137,9 @@ VGMSTREAM* init_vgmstream_ogg_vorbis(STREAMFILE* sf) { * .rof: The Rhythm of Fighters (Mobile) * .acm: Planescape Torment Enhanced Edition (PC) * .sod: Zone 4 (PC) + * .msa: Metal Slug Attack (Mobile) * .aif/laif/aif-Loop: Psychonauts (PC) raw extractions (named) */ - if (check_extensions(sf,"ogg,logg,adx,rof,acm,sod,aif,laif,aif-Loop")) { + if (check_extensions(sf,"ogg,logg,adx,rof,acm,sod,msa,aif,laif,aif-Loop")) { is_ogg = 1; } else if (check_extensions(sf,"um3")) { is_um3 = 1; diff --git a/Frameworks/vgmstream/vgmstream/src/meta/wwise.c b/Frameworks/vgmstream/vgmstream/src/meta/wwise.c index f6777fa73..8127cf20a 100644 --- a/Frameworks/vgmstream/vgmstream/src/meta/wwise.c +++ b/Frameworks/vgmstream/vgmstream/src/meta/wwise.c @@ -471,7 +471,7 @@ VGMSTREAM* init_vgmstream_wwise(STREAMFILE* sf) { break; } - case OPUS: { /* fully standard Ogg Opus [Girl Cafe Gun (Mobile)] */ + case OPUS: { /* fully standard Ogg Opus [Girl Cafe Gun (Mobile), Gears 5 (PC)] */ if (ww.block_align != 0 || ww.bits_per_sample != 0) goto fail; /* extra: size 0x12 */ @@ -486,6 +486,15 @@ VGMSTREAM* init_vgmstream_wwise(STREAMFILE* sf) { ww.data_size = ww.file_size - start_offset; } + /* mutant .wem with metadata (voice strings/etc) at data start [Gears 5 (PC)] */ + if (ww.meta_offset) { + /* 0x00: original setup_offset? (0x00 for Opus) */ + uint32_t meta_skip = read_u32(ww.meta_offset + 0x04, sf); + + ww.data_offset += meta_skip; + ww.data_size -= meta_skip; + } + vgmstream->codec_data = init_ffmpeg_offset(sf, ww.data_offset, ww.data_size); if (!vgmstream->codec_data) goto fail; vgmstream->coding_type = coding_FFmpeg;