mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-11 04:31:35 -10:00
This is the first step towards moving all audio decoding to FFmpeg which will reduce the amount of external code we will need to include. This only replaces the AudioStream code with an FFmpeg based solution which should make the code easier to review.
341 lines
9.2 KiB
C++
341 lines
9.2 KiB
C++
//
|
|
//
|
|
|
|
#include "WaveFile.h"
|
|
#include "FFmpegAudioReader.h"
|
|
#include "sound/ds.h"
|
|
|
|
// -- from parselo.cpp --
|
|
extern const char* stristr(const char* str, const char* substr);
|
|
|
|
namespace {
|
|
using namespace ffmpeg;
|
|
|
|
AudioProperties getAudioProps(AVStream* stream) {
|
|
AudioProperties props;
|
|
|
|
#if LIBAVCODEC_VERSION_INT > AV_VERSION_INT(57, 24, 255)
|
|
props.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
|
|
props.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 = (uint64_t) av_get_default_channel_layout(props.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;
|
|
}
|
|
adjusted.channels = av_get_channel_layout_nb_channels(adjusted.channel_layout);
|
|
|
|
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;
|
|
case 2:
|
|
adjusted.format = AV_SAMPLE_FMT_S16;
|
|
break;
|
|
case 4:
|
|
adjusted.format = AV_SAMPLE_FMT_FLT;
|
|
break;
|
|
default:
|
|
Assertion(false, "Unhandled switch value!");
|
|
adjusted.format = AV_SAMPLE_FMT_NONE;
|
|
break;
|
|
}
|
|
|
|
return adjusted;
|
|
}
|
|
|
|
SwrContext* getSWRContext(const AudioProperties& base, const AudioProperties& adjusted) {
|
|
auto swr = swr_alloc();
|
|
|
|
av_opt_set_int(swr, "in_channel_layout", base.channel_layout, 0);
|
|
av_opt_set_int(swr, "in_sample_rate", base.sample_rate, 0);
|
|
av_opt_set_int(swr, "in_sample_fmt", base.format, 0);
|
|
|
|
av_opt_set_int(swr, "out_channel_layout", adjusted.channel_layout, 0);
|
|
av_opt_set_int(swr, "out_sample_rate", adjusted.sample_rate, 0);
|
|
av_opt_set_int(swr, "out_sample_fmt", adjusted.format, 0);
|
|
|
|
swr_init(swr);
|
|
|
|
return swr;
|
|
}
|
|
}
|
|
|
|
namespace ffmpeg
|
|
{
|
|
WaveFile::WaveFile() : m_resampleCtx(nullptr) {
|
|
// Init data members
|
|
m_audioProps = AudioProperties();
|
|
m_ctx.reset();
|
|
|
|
m_al_format = AL_FORMAT_MONO8;
|
|
|
|
m_decodeFrame = av_frame_alloc();
|
|
}
|
|
|
|
WaveFile::~WaveFile() {
|
|
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();
|
|
|
|
}
|
|
|
|
// Open
|
|
bool WaveFile::Open(const char *pszFilename, bool keep_ext)
|
|
{
|
|
using namespace libs::ffmpeg;
|
|
|
|
size_t FileSize, FileOffset;
|
|
char fullpath[MAX_PATH];
|
|
char filename[MAX_FILENAME_LEN];
|
|
|
|
// NOTE: we assume that the extension has already been stripped off if it was supposed to be!!
|
|
strcpy_s( filename, pszFilename );
|
|
|
|
try {
|
|
int rc = -1;
|
|
// if we are supposed to load the file as passed...
|
|
if (keep_ext) {
|
|
for (int i = 0; i < NUM_AUDIO_EXT; i++) {
|
|
if (stristr(pszFilename, audio_ext_list[i])) {
|
|
rc = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// not a supported extension format ... somebody screwed up their tbls :)
|
|
if (rc < 0) {
|
|
throw FFmpegException("Unknown file extension.");
|
|
}
|
|
|
|
cf_find_file_location(pszFilename, CF_TYPE_ANY, sizeof(fullpath) - 1, fullpath, &FileSize, &FileOffset);
|
|
}
|
|
// ... otherwise we just find the best match
|
|
else {
|
|
rc = cf_find_file_location_ext(filename,
|
|
NUM_AUDIO_EXT,
|
|
audio_ext_list,
|
|
CF_TYPE_ANY,
|
|
sizeof(fullpath) - 1,
|
|
fullpath,
|
|
&FileSize,
|
|
&FileOffset);
|
|
|
|
if (rc < 0) {
|
|
throw FFmpegException("Unknown file extension.");
|
|
}
|
|
|
|
// set proper filename for later use (assumes that it doesn't already have an extension)
|
|
strcat_s(filename, audio_ext_list[rc]);
|
|
}
|
|
|
|
auto cfp = cfopen_special(fullpath, "rb", FileSize, FileOffset, CF_TYPE_ANY);
|
|
|
|
if (cfp == NULL)
|
|
throw FFmpegException("Failed to open file.");
|
|
|
|
m_ctx = FFmpegContext::createContext(cfp);
|
|
auto ctx = m_ctx->ctx();
|
|
|
|
AVCodec* audio_codec = nullptr;
|
|
m_audioStreamIndex = av_find_best_stream(ctx, AVMEDIA_TYPE_AUDIO, -1, -1, &audio_codec, 0);
|
|
if (m_audioStreamIndex < 0) {
|
|
throw FFmpegException("Failed to find audio stream in file.");
|
|
}
|
|
m_audioStream = ctx->streams[m_audioStreamIndex];
|
|
|
|
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);
|
|
|
|
m_audioProps = getAdjustedAudioProps(m_baseAudioProps);
|
|
|
|
m_resampleCtx = getSWRContext(m_baseAudioProps, m_audioProps);
|
|
|
|
m_al_format = openal_get_format(av_get_bytes_per_sample(m_audioProps.format) * 8, m_audioProps.channels);
|
|
|
|
if (m_al_format == AL_INVALID_VALUE) {
|
|
throw FFmpegException("Invalid audio format.");
|
|
}
|
|
} catch (const FFmpegException& e) {
|
|
nprintf(("SOUND", "SOUND ==> Could not open wave file %s for streaming. Reason: %s\n", filename, e.what()));
|
|
return false;
|
|
}
|
|
|
|
// Cue for streaming
|
|
Cue();
|
|
|
|
nprintf(("SOUND", "AUDIOSTR => Successfully opened: %s\n", filename));
|
|
|
|
// If we are here it means that everything went fine
|
|
return true;
|
|
}
|
|
|
|
|
|
bool WaveFile::Cue()
|
|
{
|
|
auto err = av_seek_frame(m_ctx->ctx(), m_audioStreamIndex, 0, 0);
|
|
avcodec_flush_buffers(m_audioCodecCtx);
|
|
|
|
return err >= 0;
|
|
}
|
|
|
|
size_t WaveFile::handleDecodedFrame(AVFrame* av_frame, uint8_t* out_buffer, size_t buffer_size) {
|
|
const auto sample_size = (av_get_bytes_per_sample(m_audioProps.format) * m_audioProps.channels);
|
|
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->data,
|
|
av_frame->nb_samples);
|
|
|
|
|
|
return (size_t) (written * sample_size);
|
|
}
|
|
|
|
size_t WaveFile::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) * m_audioProps.channels);
|
|
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 WaveFile::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);
|
|
}
|
|
|
|
FFmpegAudioReader reader(m_ctx->ctx(), m_audioCodecCtx, m_audioStreamIndex);
|
|
|
|
while (reader.readFrame(m_decodeFrame)) {
|
|
// Got a new frame
|
|
auto advance = handleDecodedFrame(m_decodeFrame, pbDest + buffer_pos, cbSize - buffer_pos);
|
|
|
|
if (advance == 0) {
|
|
// No new data, return what we currently have
|
|
return (int) 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 WaveFile::getSampleByteSize() const {
|
|
return av_get_bytes_per_sample(m_audioProps.format) * m_audioProps.channels;
|
|
}
|
|
|
|
int WaveFile::getSampleRate() const {
|
|
return m_audioProps.sample_rate;
|
|
}
|
|
int WaveFile::getTotalSamples() const {
|
|
auto duration = m_audioStream->time_base;
|
|
duration.num *= (int)m_audioStream->duration;
|
|
|
|
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;
|
|
}
|
|
|
|
}
|