mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 08:15:59 -10:00
* avoid undefined behavior from UNREACHABLE on reachable code paths
UNREACHABLE expands to __assume(false) / __builtin_unreachable() in
release builds, which is undefined behavior if the code is ever actually
reached. Many UNREACHABLE sites only guard against unexpected-but-
possible runtime states (values from tables, mission/save files, network
packets, scripting, or user input), so reaching them should degrade
gracefully rather than invoke UB.
Replace UNREACHABLE with Assertion(false, ...) at all such sites. It
still pops an error in debug builds, but in release it is a harmless
no-op that simply falls through to the following code. Sites that are
genuinely unreachable by local invariants (e.g. a switch over a bounded
random number) are left as UNREACHABLE.
A few converted sites had no safe fall-through -- an uninitialized read,
null dereference, or null call would have followed -- so they also get a
minimal recovery: initialize the value, guard the dereference, return
early, or skip the offending item.
Where the converted site was a simple guard of the form
if (x) { Assertion(false, ...); return; }
hoist the check onto the invariant instead:
Assertion(!x, ...);
if (x) { return; }
This states the expectation directly and shows the offending value in
the debug popup, while keeping the same graceful runtime degradation.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* addendum
After consultation with Lafiel, use UNREACHABLE in situations that are effectively impossible even if not provably impossible. UNREACHABLE = "this shouldn't ever happen"; Assertion = "if this ever happens, it's a coding error". Also fix a few other sites and change a couple to Warnings.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
470 lines
13 KiB
C++
470 lines
13 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(59, 36, 255)
|
|
channels = stream->codecpar->ch_layout.nb_channels;
|
|
props.channel_layout = stream->codecpar->ch_layout.u.mask;
|
|
props.sample_rate = stream->codecpar->sample_rate;
|
|
props.format = (AVSampleFormat)stream->codecpar->format;
|
|
#elif 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
|
|
#if LIBAVCODEC_VERSION_INT > AV_VERSION_INT(59, 36, 255)
|
|
AVChannelLayout ch_layout;
|
|
|
|
av_channel_layout_default(&ch_layout, channels);
|
|
props.channel_layout = ch_layout.u.mask;
|
|
#else
|
|
props.channel_layout = av_get_default_channel_layout(channels);
|
|
#endif
|
|
}
|
|
|
|
return props;
|
|
}
|
|
|
|
AudioProperties getAdjustedAudioProps(const AudioProperties& baseProps)
|
|
{
|
|
AudioProperties adjusted;
|
|
adjusted.sample_rate = baseProps.sample_rate; // Don't adjust sample rate
|
|
|
|
int channels = 2;
|
|
adjusted.channel_layout = baseProps.channel_layout;
|
|
|
|
#if LIBAVCODEC_VERSION_INT > AV_VERSION_INT(59, 36, 255)
|
|
AVChannelLayout ch_layout;
|
|
|
|
av_channel_layout_from_mask(&ch_layout, baseProps.channel_layout);
|
|
|
|
channels = ch_layout.nb_channels;
|
|
#else
|
|
channels = av_get_channel_layout_nb_channels(baseProps.channel_layout);
|
|
#endif
|
|
|
|
if (channels > 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 %d!", bytes_per_sample);
|
|
adjusted.format = AV_SAMPLE_FMT_NONE;
|
|
break;
|
|
}
|
|
|
|
return adjusted;
|
|
}
|
|
|
|
SwrContext* getSWRContext(const AudioProperties& base, const AudioProperties& adjusted)
|
|
{
|
|
SwrContext* swr = nullptr;
|
|
#if LIBAVCODEC_VERSION_INT > AV_VERSION_INT(59, 36, 255)
|
|
AVChannelLayout base_layout, adjusted_layout;
|
|
|
|
av_channel_layout_from_mask(&base_layout, base.channel_layout);
|
|
av_channel_layout_from_mask(&adjusted_layout, adjusted.channel_layout);
|
|
|
|
swr_alloc_set_opts2(&swr, &adjusted_layout, adjusted.format, adjusted.sample_rate, &base_layout,
|
|
base.format, base.sample_rate, 0, nullptr);
|
|
#else
|
|
swr = swr_alloc_set_opts(swr, adjusted.channel_layout, adjusted.format, adjusted.sample_rate, base.channel_layout,
|
|
base.format, base.sample_rate, 0, nullptr);
|
|
#endif
|
|
|
|
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) {
|
|
#if LIBAVCODEC_VERSION_INT > AV_VERSION_INT(57, 24, 255)
|
|
avcodec_free_context(&m_audioCodecCtx);
|
|
#else
|
|
avcodec_close(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];
|
|
|
|
#if LIBAVCODEC_VERSION_INT > AV_VERSION_INT(57, 24, 255)
|
|
audio_codec = avcodec_find_decoder(m_audioStream->codecpar->codec_id);
|
|
#else
|
|
audio_codec = avcodec_find_decoder(m_audioStream->codec->codec_id);
|
|
#endif
|
|
|
|
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) {
|
|
#ifndef NDEBUG
|
|
// see if the file exists with a different extension
|
|
res = cf_find_file_location_ext(filename, NUM_AUDIO_EXT, audio_ext_list, CF_TYPE_ANY);
|
|
if (res.found) {
|
|
Warning(LOCATION, "File %s was not found with its specified extension, but another file %s exists in the modpack. Please update the extension and adjust audio specifications if necessary.", filename, res.name_ext.c_str());
|
|
}
|
|
#endif
|
|
|
|
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
|
|
{
|
|
#if LIBAVCODEC_VERSION_INT > AV_VERSION_INT(59, 36, 255)
|
|
AVChannelLayout ch_layout;
|
|
|
|
av_channel_layout_from_mask(&ch_layout, m_audioProps.channel_layout);
|
|
|
|
return ch_layout.nb_channels;
|
|
#else
|
|
return av_get_channel_layout_nb_channels(m_audioProps.channel_layout);
|
|
#endif
|
|
}
|
|
|
|
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
|