mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-13 06:21:29 -10:00
* add subfolder support to cfile * sort files in VP archives before indexing them * fix indexing of sub-subfolders * remove unused 8B22K & 16B11K sound path types * add more items to skip subfolder scanning * fix windows build issues * fix unit tests by allowing 0-byte files to be indexed * fix file indexing on windows * add test for cfile subfolders * remove specific 'markup' and 'css' interface subfolders They aren't directly referenced anywhere by pathtype and the new subfolder code will continue to index and load files from them * add player binds to list of always-root locations * remove file-based localization support An alternative is left in place for old fonts, as that was the only place it appeared to be used. This method of file localization conflicts with subfolder support however so if it's needed in the future then changes to the subfolder code will be necessary. * add subfolder support to cf_find_file_location_ext() * add subfolder tests for cf_find_file_location*() * add shlwapi to needed win32 libs * small bit of cleanup * add subfolder support to cf_get_file_list() * add tests for cfile subfolder lists * report files that might be shadowed * fix issue with lists when a filter is used Also fixes discrepancy in older cf_get_file_list() when a sub path is specified (should be used in all cases, not just glob). * address review suggestions
413 lines
11 KiB
C++
413 lines
11 KiB
C++
//
|
|
//
|
|
|
|
#include "FFmpegWaveFile.h"
|
|
|
|
#include "FFmpegAudioReader.h"
|
|
|
|
#include "sound/ds.h"
|
|
|
|
// -- from parselo.cpp --
|
|
extern const char* stristr(const char* str, const char* substr);
|
|
|
|
namespace {
|
|
using namespace sound;
|
|
using namespace sound::ffmpeg;
|
|
|
|
AudioProperties getAudioProps(AVStream* stream)
|
|
{
|
|
AudioProperties props;
|
|
|
|
int channels;
|
|
#if LIBAVCODEC_VERSION_INT > AV_VERSION_INT(57, 24, 255)
|
|
channels = stream->codecpar->channels;
|
|
props.channel_layout = stream->codecpar->channel_layout;
|
|
props.sample_rate = stream->codecpar->sample_rate;
|
|
props.format = (AVSampleFormat)stream->codecpar->format;
|
|
#else
|
|
channels = stream->codec->channels;
|
|
props.channel_layout = stream->codec->channel_layout;
|
|
props.sample_rate = stream->codec->sample_rate;
|
|
props.format = stream->codec->sample_fmt;
|
|
#endif
|
|
|
|
if (props.channel_layout == 0) {
|
|
// Use a default channel layout value
|
|
props.channel_layout = av_get_default_channel_layout(channels);
|
|
}
|
|
|
|
return props;
|
|
}
|
|
|
|
AudioProperties getAdjustedAudioProps(const AudioProperties& baseProps)
|
|
{
|
|
AudioProperties adjusted;
|
|
adjusted.sample_rate = baseProps.sample_rate; // Don't adjust sample rate
|
|
|
|
adjusted.channel_layout = baseProps.channel_layout;
|
|
if (av_get_channel_layout_nb_channels(baseProps.channel_layout) > 2) {
|
|
adjusted.channel_layout = AV_CH_LAYOUT_STEREO;
|
|
}
|
|
|
|
int max_bytes_per_sample;
|
|
switch (Ds_sound_quality) {
|
|
case DS_SQ_HIGH:
|
|
max_bytes_per_sample = Ds_float_supported ? 4 : 2;
|
|
break;
|
|
|
|
case DS_SQ_MEDIUM:
|
|
max_bytes_per_sample = 2;
|
|
break;
|
|
|
|
default:
|
|
max_bytes_per_sample = 1;
|
|
break;
|
|
}
|
|
|
|
// Determine the right format. Downsample if sound quality is low but don't increase size of samples if the source
|
|
// doesn't support it
|
|
auto bytes_per_sample = std::min(av_get_bytes_per_sample(baseProps.format), max_bytes_per_sample);
|
|
|
|
switch (bytes_per_sample) {
|
|
case 1:
|
|
adjusted.format = AV_SAMPLE_FMT_U8;
|
|
break;
|
|
case 2:
|
|
adjusted.format = AV_SAMPLE_FMT_S16;
|
|
break;
|
|
case 4:
|
|
adjusted.format = AV_SAMPLE_FMT_FLT;
|
|
break;
|
|
default:
|
|
UNREACHABLE("Unhandled switch value!");
|
|
adjusted.format = AV_SAMPLE_FMT_NONE;
|
|
break;
|
|
}
|
|
|
|
return adjusted;
|
|
}
|
|
|
|
SwrContext* getSWRContext(const AudioProperties& base, const AudioProperties& adjusted)
|
|
{
|
|
SwrContext* swr = nullptr;
|
|
swr = swr_alloc_set_opts(swr, adjusted.channel_layout, adjusted.format, adjusted.sample_rate, base.channel_layout,
|
|
base.format, base.sample_rate, 0, nullptr);
|
|
|
|
if (swr_init(swr) < 0) {
|
|
return nullptr;
|
|
}
|
|
|
|
return swr;
|
|
}
|
|
} // namespace
|
|
|
|
namespace sound {
|
|
namespace ffmpeg {
|
|
FFmpegWaveFile::FFmpegWaveFile()
|
|
{
|
|
// Init data members
|
|
m_audioProps = AudioProperties();
|
|
m_ctx.reset();
|
|
|
|
m_decodeFrame = av_frame_alloc();
|
|
}
|
|
|
|
FFmpegWaveFile::~FFmpegWaveFile()
|
|
{
|
|
av_frame_free(&m_decodeFrame);
|
|
|
|
if (m_audioCodecCtx) {
|
|
avcodec_close(m_audioCodecCtx);
|
|
#if LIBAVCODEC_VERSION_INT > AV_VERSION_INT(57, 24, 255)
|
|
avcodec_free_context(&m_audioCodecCtx);
|
|
#endif
|
|
m_audioCodecCtx = nullptr;
|
|
}
|
|
|
|
if (m_resampleCtx) {
|
|
swr_free(&m_resampleCtx);
|
|
}
|
|
|
|
// Free memory
|
|
m_ctx.reset();
|
|
}
|
|
|
|
const AVCodec* sound::ffmpeg::FFmpegWaveFile::prepareOpened()
|
|
{
|
|
using namespace libs::ffmpeg;
|
|
auto ctx = m_ctx->ctx();
|
|
|
|
const AVCodec* audio_codec = nullptr;
|
|
|
|
m_audioStreamIndex = av_find_best_stream(ctx, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);
|
|
if (m_audioStreamIndex < 0) {
|
|
throw FFmpegException("Failed to find audio stream in file.");
|
|
}
|
|
m_audioStream = ctx->streams[m_audioStreamIndex];
|
|
|
|
audio_codec = avcodec_find_decoder(m_audioStream->codecpar->codec_id);
|
|
|
|
if ( !audio_codec ) {
|
|
throw FFmpegException("Failed to find decoder for audio stream in file.");
|
|
}
|
|
|
|
int err;
|
|
#if LIBAVCODEC_VERSION_INT > AV_VERSION_INT(57, 24, 255)
|
|
m_audioCodecCtx = avcodec_alloc_context3(audio_codec);
|
|
|
|
// Copy codec parameters from input stream to output codec context
|
|
err = avcodec_parameters_to_context(m_audioCodecCtx, m_audioStream->codecpar);
|
|
if (err < 0) {
|
|
char errorStr[512];
|
|
av_strerror(err, errorStr, sizeof(errorStr));
|
|
throw FFmpegException(errorStr);
|
|
}
|
|
#else
|
|
m_audioCodecCtx = m_audioStream->codec;
|
|
#endif
|
|
|
|
err = avcodec_open2(m_audioCodecCtx, audio_codec, nullptr);
|
|
if (err < 0) {
|
|
char errorStr[512];
|
|
av_strerror(err, errorStr, sizeof(errorStr));
|
|
throw FFmpegException(errorStr);
|
|
}
|
|
|
|
m_baseAudioProps = getAudioProps(m_audioStream);
|
|
|
|
setAdjustedAudioProperties(getAdjustedAudioProps(m_baseAudioProps));
|
|
|
|
return audio_codec;
|
|
}
|
|
|
|
// Open
|
|
bool FFmpegWaveFile::Open(const char* pszFilename, bool keep_ext)
|
|
{
|
|
using namespace libs::ffmpeg;
|
|
|
|
char filename[MAX_FILENAME_LEN];
|
|
|
|
// NOTE: the extension will be removed and set to the actual extension of keep_ext is false, otherwise it's not
|
|
// changed
|
|
strcpy_s(filename, pszFilename);
|
|
|
|
try {
|
|
CFILE* cfp = nullptr;
|
|
// if we are supposed to load the file as passed...
|
|
if (keep_ext) {
|
|
auto known_format = false;
|
|
for (int i = 0; i < NUM_AUDIO_EXT; i++) {
|
|
if (stristr(pszFilename, audio_ext_list[i])) {
|
|
known_format = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// not a supported extension format ... somebody screwed up their tbls :)
|
|
if (!known_format) {
|
|
throw FFmpegException("Unknown file extension.");
|
|
}
|
|
|
|
auto res = cf_find_file_location(pszFilename, CF_TYPE_ANY);
|
|
|
|
if (!res.found) {
|
|
throw FFmpegException("File not found.");
|
|
}
|
|
|
|
cfp = cfopen_special(res, "rb", CF_TYPE_ANY);
|
|
} else {
|
|
// ... otherwise we just find the best match
|
|
auto res = cf_find_file_location_ext(filename, NUM_AUDIO_EXT, audio_ext_list, CF_TYPE_ANY);
|
|
|
|
if (!res.found) {
|
|
throw FFmpegException("File not found with any known extension.");
|
|
}
|
|
|
|
// set proper filename for later use
|
|
strcpy_s(filename, res.name_ext.c_str());
|
|
|
|
cfp = cfopen_special(res, "rb", CF_TYPE_ANY);
|
|
}
|
|
|
|
if (cfp == NULL) {
|
|
throw FFmpegException("Failed to open file.");
|
|
}
|
|
|
|
m_ctx = FFmpegContext::createContext(cfp);
|
|
|
|
const AVCodec* audio_codec = prepareOpened();
|
|
|
|
nprintf(("SOUND", "SOUND => %s => Using codec %s (%s)\n", filename, audio_codec->long_name, audio_codec->name));
|
|
} catch (const FFmpegException& e) {
|
|
mprintf(("SOUND ==> Could not open wave file %s for streaming. Reason: %s\n", filename, e.what()));
|
|
return false;
|
|
}
|
|
|
|
// Cue for streaming
|
|
Cue();
|
|
m_frameReader.reset(new FFmpegAudioReader(m_ctx->ctx(), m_audioCodecCtx, m_audioStreamIndex));
|
|
|
|
nprintf(("SOUND", "SOUND => Successfully opened: %s\n", filename));
|
|
|
|
// If we are here it means that everything went fine
|
|
return true;
|
|
}
|
|
|
|
// OpenMem
|
|
bool FFmpegWaveFile::OpenMem(const uint8_t* snddata, size_t snd_len)
|
|
{
|
|
using namespace libs::ffmpeg;
|
|
|
|
try {
|
|
m_ctx = FFmpegContext::createContextMem(snddata, snd_len);
|
|
|
|
const AVCodec* audio_codec = prepareOpened();
|
|
|
|
nprintf(("SOUND", "SOUND => %s => Using codec %s (%s)\n", "soundfile-in-memory", audio_codec->long_name, audio_codec->name));
|
|
} catch (const FFmpegException& e) {
|
|
mprintf(("SOUND ==> Could not open wave file %s for streaming. Reason: %s\n", "soundfile-in-memory", e.what()));
|
|
return false;
|
|
}
|
|
|
|
// Cue for streaming
|
|
Cue();
|
|
m_frameReader.reset(new FFmpegAudioReader(m_ctx->ctx(), m_audioCodecCtx, m_audioStreamIndex));
|
|
|
|
nprintf(("SOUND", "SOUND => Successfully opened: %s\n", "soundfile-in-memory"));
|
|
|
|
// If we are here it means that everything went fine
|
|
return true;
|
|
}
|
|
|
|
bool FFmpegWaveFile::Cue()
|
|
{
|
|
auto err = av_seek_frame(m_ctx->ctx(), m_audioStreamIndex, 0, AVSEEK_FLAG_BYTE);
|
|
|
|
if (err >= 0) {
|
|
avcodec_flush_buffers(m_audioCodecCtx);
|
|
avformat_flush(m_ctx->ctx());
|
|
}
|
|
|
|
return err >= 0;
|
|
}
|
|
|
|
void FFmpegWaveFile::setAdjustedAudioProperties(const AudioProperties& props)
|
|
{
|
|
if (m_resampleCtx) {
|
|
swr_free(&m_resampleCtx);
|
|
}
|
|
m_audioProps = props;
|
|
m_resampleCtx = getSWRContext(m_baseAudioProps, m_audioProps);
|
|
|
|
Assertion(m_resampleCtx != nullptr, "Resample context creation failed! This should not happen!");
|
|
}
|
|
|
|
size_t FFmpegWaveFile::handleDecodedFrame(AVFrame* av_frame, uint8_t* out_buffer, size_t buffer_size)
|
|
{
|
|
const auto sample_size = (av_get_bytes_per_sample(m_audioProps.format) * getNumChannels());
|
|
|
|
int dest_num_samples = static_cast<int>(buffer_size / sample_size);
|
|
auto written = swr_convert(m_resampleCtx, &out_buffer, dest_num_samples, (const uint8_t**)av_frame->extended_data,
|
|
av_frame->nb_samples);
|
|
|
|
return (size_t)(written * sample_size);
|
|
}
|
|
|
|
size_t FFmpegWaveFile::getBufferedData(uint8_t* buffer, size_t buffer_size)
|
|
{
|
|
// First determine if there is buffered data
|
|
auto buffered = swr_get_out_samples(m_resampleCtx, 0);
|
|
|
|
if (buffered > 0) {
|
|
const auto sample_size = (av_get_bytes_per_sample(m_audioProps.format) * getNumChannels());
|
|
int dest_num_samples = static_cast<int>(buffer_size / sample_size);
|
|
auto written = swr_convert(m_resampleCtx, &buffer, dest_num_samples, nullptr, 0);
|
|
|
|
auto advance = (size_t)(written * sample_size);
|
|
Assertion(advance <= buffer_size,
|
|
"Buffer overrun!!! Decoding has written more data into the buffer than available!");
|
|
|
|
return advance;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int FFmpegWaveFile::Read(uint8_t* pbDest, size_t cbSize)
|
|
{
|
|
size_t buffer_pos = 0;
|
|
buffer_pos += getBufferedData(pbDest, cbSize);
|
|
if (buffer_pos == cbSize) {
|
|
return static_cast<int>(buffer_pos);
|
|
}
|
|
|
|
while (m_frameReader->readFrame(m_decodeFrame)) {
|
|
// Got a new frame
|
|
auto advance = handleDecodedFrame(m_decodeFrame, pbDest + buffer_pos, cbSize - buffer_pos);
|
|
|
|
buffer_pos += advance;
|
|
Assertion(buffer_pos <= cbSize,
|
|
"Buffer overrun!!! Decoding has written more data into the buffer than available!");
|
|
|
|
if (buffer_pos == cbSize) {
|
|
return static_cast<int>(buffer_pos);
|
|
}
|
|
}
|
|
|
|
// If we are here then the audio stream is finished, write any buffered data
|
|
buffer_pos += getBufferedData(pbDest + buffer_pos, cbSize - buffer_pos);
|
|
|
|
if (buffer_pos == 0) {
|
|
// End of stream has been reached
|
|
return -1;
|
|
}
|
|
|
|
return static_cast<int>(buffer_pos);
|
|
}
|
|
|
|
int FFmpegWaveFile::getTotalSamples() const
|
|
{
|
|
auto duration = av_mul_q(av_make_q(static_cast<int>(m_audioStream->duration), 1), m_audioStream->time_base);
|
|
auto samples_r = av_mul_q(av_make_q(m_audioProps.sample_rate, 1), duration);
|
|
|
|
// Compute the actual sample number and round the result up
|
|
auto samples = 1 + ((samples_r.num - 1) / samples_r.den);
|
|
|
|
return samples;
|
|
}
|
|
|
|
int FFmpegWaveFile::getNumChannels() const { return av_get_channel_layout_nb_channels(m_audioProps.channel_layout); }
|
|
|
|
AudioFileProperties FFmpegWaveFile::getFileProperties()
|
|
{
|
|
AudioFileProperties props{};
|
|
props.num_channels = getNumChannels();
|
|
props.duration =
|
|
av_q2d(av_mul_q(av_make_q(static_cast<int>(m_audioStream->duration), 1), m_audioStream->time_base));
|
|
props.total_samples = getTotalSamples();
|
|
props.sample_rate = m_audioProps.sample_rate;
|
|
props.bytes_per_sample = av_get_bytes_per_sample(m_audioProps.format);
|
|
return props;
|
|
}
|
|
void FFmpegWaveFile::setResamplingProperties(const ResampleProperties& resampleProps)
|
|
{
|
|
using namespace libs::ffmpeg;
|
|
|
|
auto current = m_audioProps;
|
|
|
|
if (getNumChannels() == resampleProps.num_channels) {
|
|
// No need to resample
|
|
return;
|
|
}
|
|
|
|
if (resampleProps.num_channels == 1) {
|
|
current.channel_layout = AV_CH_LAYOUT_MONO;
|
|
|
|
setAdjustedAudioProperties(current);
|
|
} else {
|
|
throw FFmpegException("Number of channels to resample to is not supported!");
|
|
}
|
|
}
|
|
|
|
} // namespace ffmpeg
|
|
} // namespace sound
|