Files
fs2open.github.com/code/sound/ffmpeg/FFmpegWaveFile.cpp
T
Goober5000andClaude Opus 4.8 bed228d264 avoid undefined behavior from UNREACHABLE on reachable code paths (#7555)
* 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]>
2026-07-09 11:20:25 +09:00

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